Block相关内容梳理
什么是block
Block是將函數及其上下文封裝起來的對象。
源碼分析
編譯器是如何實現block的?
新建Objective-C文件命名為MyClass,在.m文件中實現如下代碼:
#import "MyClass.h" @implementation MyClass - (void)block{int b = 10;int (^MyBlock)(int parames);MyBlock = ^int(int a){return a*b;};MyBlock(2); } @end使用命令行,在MyClass.m文件目錄下,運行命令
clang -rewrite-objc MyClass.m
clang 提供一個命令,可以將 Objetive-C 的源碼改寫成 c 語言的
在當前目錄下可以看到,clang輸出了一個MyClass.cpp的文件。
關鍵代碼如下:
__MyClass__block_block_impl_0就是該block的實現。
什么是block的調用
block調用即是函數的調用。
截獲變量
capture過來的變量
int b = 10;int (^MyBlock)(int parames);MyBlock = ^int(int a){return a*b;};b = 100;NSLog(@"看一下結果%d",MyBlock(2));輸出結果 20
截獲變量要看是對什么樣的變量進行截獲
有局部變量(基本數據類型、對象類型)、全局變量、靜態局部變量、靜態全局變量。
截獲變量的特性是什么?
對于基本數據類型的局部變量截獲其值。
對于對象類型的局部變量連同所有權修飾符一起截獲。
以指針形式截獲靜態局部變量。
不截獲全局變量和靜態全局變量
源碼分析
看下面代碼
#import "MyClass.h"@implementation MyClass // 全局變量 int global_var = 4; // 靜態全局變量 static int static_global_var = 5;- (void)myBlockMethod{// 基本數據類型的局部變量int b = 10;// 靜態局部變量static int static_var = 3;// 對象類型的局部變量 所有權修飾符__unsafe_unretained __strong__unsafe_unretained id unsafe_obj = nil;__strong id strong_obj = nil;void(^MyBlock)(void);MyBlock = ^{NSLog(@"局部變量<基本數據類型> var=%d",b);NSLog(@"局部變量<靜態變量> static_var=%d",static_var);NSLog(@"局部變量<__unsafe_unretained 修飾的對象類型> var=%@",unsafe_obj);NSLog(@"局部變量 __strong 修飾的對象類型> var=%@",strong_obj);NSLog(@"全局變量 %d",global_var);NSLog(@"靜態全局變量 %d",static_global_var);};MyBlock(); }@end運行命令 clang -rewrite-objc -fobjc-arc MyClass.m
-fobjc-arc 支持ARC -fno-objc-arc 不支持ARC
關鍵代碼如下:
我們看下下面這段代碼的運行結果是什么
int b = 2;int (^MyBlock)(int param);MyBlock = ^int(int a){return a*b;};b = 4;NSLog(@"結果: %d",MyBlock(7));結果如下:結果: 14
為什么是14而不是28,因為對于基本數據類型的局部變量截獲其值,也就是2.
然后我們在 局部變量前面加上static
static int b = 2;int (^MyBlock)(int param);MyBlock = ^int(int a){return a*b;};b = 4;NSLog(@"結果: %d",MyBlock(7));結果如下:結果: 28
為什么是28呢,因為對靜態局部變量是以指針形式截獲的。int b = 4;時地址上值已經被修改成4了
如果我們想在block內部對b的值進行修改,我們就要用到__block.
__block修飾符
我們什么時候要用到__block修飾符呢?
一般情況下,對被截獲變量進行賦值操作需添加__block修飾符。
需要注意的是 賦值 ≠ 使用
下面看一些關于__block的一些筆試題
NSMutableArray *arr = [[NSMutableArray alloc] init];void (^MyBlock)(NSString *str);MyBlock = ^(NSString *str){[arr addObject:str];};MyBlock(@"客戶甲");NSLog(@"客戶列表 %@",arr);arr里的值是
description of arr: <__NSArrayM 0x6000028de490>( 客戶甲 )這里不需要__block,因為這里只是使用arr,并沒有賦值。
看下面這種情況
這里才是對arr的賦值,所以這時候需要使用__block。
對變量進行賦值具體有什么特點?
對變量進行賦值時,局部變量,不管是基本類型的局部變量還是對象類型的局部變量,都需要使用__block修飾符。
靜態類型的局部變量本身就是以指針類型截獲的,所以不需要。
全局變量和靜態全局變量不截獲,所以也不需要。
思考一下下面的代碼結果是什么
結果是10還是20呢?結果: 20
好奇怪為什么不是10呢?
__block修飾符所起的作用來回答這個問題。
__block修飾的變量最后變成了對象。使用clang命令查看一下源碼:
b變成了struct __Block_byref_b_0對象。而對b = 4;進行賦值實際上就是(b.__forwarding->b) = 4;
該段代碼的運行是在棧上進行的,棧上有一個__block修飾的變量,且有一個.__forwarding的指針指向它。
Block的三種類型
在Objective-C中Block一共有三種類型:
-
_NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量;
-
_NSConcreteStackBlock 保存在棧中的block,當函數返回時被銷毀;
-
_NSConcreteMallocBlock 保存在堆中的block,當引用計數器為0時被銷毀
Block的內存管理
內存從高到低依次分為棧區、堆區、全局區、常量區、代碼區。
需要知道的是全局的靜態block保存在已初始化的全局靜態區。
Block的copy操作
我們在何時需要對block進行copy操作?那么就要知道copy的效果是什么樣的。
| _NSConcreteGlobalBlock | 數據區 | 什么也不做 |
| _NSConcreteStackBlock | 棧 | 堆 |
| _NSConcreteMallocBlock | 堆 | 增加引用計數 |
我們在站上有一個Block,在這個block當中,有一個__block修飾的變量,當我們對棧上的Block進行copy的時候,會在堆上生成一個和棧上一樣對應的Block和__block變量,分占兩塊空間,左側是在棧上,右側是在堆上,隨著變量作用域的結束,站上的Block被釋放,但是堆上對應的Block和__block變量仍然存在。
請關注下面問題:
當我們對棧上的Block進行copy操作之后,假如說是在MRC環境下,是否會引起內存泄漏;答案是肯定。解析:假如說我們進行了copy操作之后,同時堆上面的這個Block沒有其他成員變量指向它,那么和我們去alloc出來一個對象而沒有去調用release操作是一樣的。會產生內存泄漏。
究竟對棧上的__block進行copy操作,之后發生了什么呢?
我們在棧上有一個__block變量,__block下有一個__forwarding指針,棧上的__forwarding指針是指向它自身的;當對棧上的__block變量進行copy之后,實際在堆上面會產生一個__block變量,和棧上的是完全一致的,只不過分占兩塊內存空間。且棧上的__forwarding指針實際上指向的是堆上面的__block變量,堆上的__forwarding指針指向的是其自身。
(b.__forwarding->b) = 4;
所以,當我們對棧上__block修飾的變量b做出修改,假如說我們已經對b做了copy操作,那么我們修改的不是棧上面的__block變量b對應的值,而是通過棧上的__forwarding指針,找到它指向的堆上的__block變量b對應的值。
__forwarding指針是用來干什么的?
棧上的__forwarding是指向自身的,那么為什么需要這個指針呢?那么換句話說,我們沒有這個指針的情況下,可以直接通過對成員變量的訪問來對b進行賦值,那__forwading指針是不是多余了?從上面我們可以知道,當對棧上的__block變量進行copy之后,棧上的__forwading指針實際上指向堆上面的__block變量。并不是多余的。總結,不論在任何內存位置,我們都可以通過__forwading指針順利的訪問同一個__block變量。
Block的循環引用
看代碼一:
_arr = [NSMutableArray arrayWithObject:@"張三"];MyBlock = ^NSString*(NSString*str){return [NSString stringWithFormat:@"你好 %@",_arr[0]];};MyBlock(@"5");代碼會報出警告??
Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
在這個block中,有一個對self的循環引用 。MyBlock和_arr都是當前對象self的成員變量,對_array一般用strong關鍵字來修飾,對block一般用copy關鍵字來修飾。當前對象通過copy屬性關鍵字聲明的MyBlock,所以當前對象對MyBlock有一個強引用在的。而這個Block的表達式中又使用到了當前對象的_arr成員變量,截獲變量對于對象類型的局部變量是連同其所有權修飾符一起截獲的。_arr是通過__strong修飾的,所以在block中就有了一個strongly類型的指針指向原來的對象
如何避免這個問題?修正代碼:
采用避免產生循環引用的方式來解除它的循環引用。
那么為什么通過__weak修飾的成員變量就可以解除循環引用呢?因為block對其所截獲的變量,如果是對象類型的,是連同其所有權修飾符一起截獲。外部是__weak修飾符,所以里面也是__weak。
看代碼二:
__block MyClass* blockSelf = self;MyBlock = ^int(int num){// b = 5return num*blockSelf.b;};MyBlock(2);在這個棧上面,有一個__block修飾的變量來指向self,即對象本身,同時當前對象的成員變量MyBlock創建時,表達式中有使用到self.b,這里用的是blockSelf,而不是直接使用當前對象。
這段代碼在MRC下,不會產生循環引用。在ARC下,會產生循環引用,引起內存泄漏。
代碼中存在一個大環引用:
self對象持有Block,Block中又使用了__block變量,而這個__block 變量又指向原有的這個對象。這種循環引用就是大環引用。
怎么解除這個循環引用呢?
我們一般使用斷環的方式來解除這種循環引用。斷開__block變量對原對象的持有,就可以規避循環引用。
修改后的代碼:
對blockSelf進行一個nil賦值,就可以規避這個循環引用。但是這個解決方案有一個弊端,假如我們很長一段時間,或者一直都沒調用這個block的話,那么這個循環引用的環就會一直存在。
小結
什么是Block?
Block是對函數及其上下文封裝起來的對象。
為什么Block會產生循環引用?
第一方面,自循環引用:如果說當前Block對當前對象的某一成員變量進行截獲,那么這個Block會對對應變量有一個強引用,而當前對象對Block也有一個強引用,就產生了一個自循環引用方式的循環引用問題,我們通過聲明一個__weak修飾的變量,來消除循環引用。
第二方面,我們定義一個__block修飾的變量,也會產生循環引用,但是是區別場景的。在ARC下會產生循環引用,有內存泄漏;在MRC下是沒有問題的。我們可以在ARC下通過斷環的方式避免循環引用,但是有一個弊端,如果block一直得不到調用,那么循環引用就無法解除,環就一直存在。
怎么理解Block截獲變量的特性?
對于基本數據類型的局部變量,是對值進行截獲。
對于對象類型的局部變量,是連同其所有權修飾符一起截獲;
對于靜態類型的局部變量,是以指針形式截獲的;
對全局變量和靜態全局變量不截獲。
你都遇到過哪些循環引用?你有事怎樣解決的?
我們會遇到Block所引起的循環引用,比如
1、Block所捕獲的成員變量,也是當前對象的成員變量,而block也是當前對象的成員變量,就會造成自循環的循環引用。
解決:通過加__weak修飾符開解除這種自循環引用;
2、__block在ARC下也會產生循環引用,采用斷環的方式來避免循環引用。
總結
以上是生活随笔為你收集整理的Block相关内容梳理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: swift中的@objc
- 下一篇: 内存管理相关【内存布局内存管理方案】