神奇的Block
本文不做Block
的基本介紹和底層實現(xiàn)原理,有興趣的同學直接戳這篇文章,寫得灰常好,本文只在應(yīng)用層面上帶領(lǐng)讀者進行思考,并整理出一些結(jié)論.這些結(jié)論是我從書上和上網(wǎng)資料收集所得,并通過實踐進行驗證而來,希望能和高手們共同探討 :)
在看例子之前,至少要知道block有幾個類型.
- _NSConcreteGlobalBlock(全局塊)
- _NSConcreteStackBlock(棧塊)
- _NSConcreteMallocBlock(堆塊)
廢話不說,直接看例子.測試環(huán)境為ARC,就不做MRC的測試了.
精神病入門
例子一:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
blk_t block = ^{
printf("I'm just a block\n");
};
block();
return 0;
}
很簡單的一段代碼,執(zhí)行block
之后結(jié)果是I'm just a block
.但如果問你,這個block
是什么類型的block
,你會怎么回答?
在代碼中打一個斷點,通過打印block
的isa
,可以知道該block
是什么類型的.
第一步:打個斷點
第二步:打印isa
然后就能看到結(jié)果了:
看到結(jié)果,尼瑪居然是個全局塊
,可是我明明是在棧上創(chuàng)建的一個block
呀!
再來看一個例子,這時定義了一個局部變量,并在block
中使用了這個局部變量.
例子二:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
int i = 1;
blk_t block = ^{
printf("%d\n",i);
};
block();
return 0;
}
按照以上步驟再看看block
的isa
.
……我去,怎么成堆塊
了?
別急,再舉個??.
例子三:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
int i = 1;
__weak blk_t block = ^{
printf("%d\n",i);
};
block();
return 0;
}
雖然編譯器在__weak blk_t block = ^{
這行爆出了警告,但是程序還是能夠正常運行.block
并未因為一個弱引用立即釋放.然后看看結(jié)果:
終于看到
棧塊
了,全家福終于齊人了.
下面開始總結(jié)了.
在哪些情況下,Block
為_NSConcreteGlobalBlock
類對象?
-
記述全局變量的地方創(chuàng)建的
Block
,比如下面的例子.blk_t block = ^{ printf("I'm just a block"); }; int main(int argc, const char * argv[]) { block(); return 0; }
-
不截獲自動變量的時候.
即
例子一
這種情況下.雖然是在棧上創(chuàng)建的一個block
,但由于閉包內(nèi)不截獲外部的自動變量(局部變量),將會被編譯器編譯為_NSConcreteGlobalBlock
.
再來總結(jié)一下第二個例子.之所以是一個堆塊,是因為編譯器為塊進行了copy
操作(實質(zhì)上是調(diào)用_Block_copy函數(shù)).以下方式會讓塊從棧復(fù)制到堆上.
-
調(diào)用
Block
的copy
實例方法.[^{ printf("a heap block"); } copy]; // 對block調(diào)用copy,會把棧上的block復(fù)制到堆上.
-
將
Block
賦值給附有__strong
修飾符id
類型的類或Block
類型成員變量時.也就是說,有個__strong修飾的變量指向這個block就會讓編譯器為block調(diào)用copy方法.
例子二就是將Block賦值給了一個
__strong
(默認都是strong)修飾的Block
類型成員變量——blk_t block
.因此,在例子三中,我將強引用變成弱引用,創(chuàng)建了一個棧上的block.雖然編譯器會有警告,因為編譯器在這里可能還不知道那個塊也是棧上的,而這個棧上的塊,顯然不會立即釋放.
-
Block
作為函數(shù)返回值時例如:
blk_t return_A_Block(){ int val = 10; return ^{NSLog(@"%d",val);}; } int main(int argc, const char * argv[]) { NSLog(@"%@",return_A_Block()); return 0; }
打印所得是一個堆塊.
Block
的副本
Block的類 | 副本源的配置存儲域 | 復(fù)制效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
_NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
精神病進階
例子四:
typedef void (^blk_t) (id obj);
int main(int argc, const char * argv[]) {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"%ld",[array count]);
};
}
// array超出了作用域,在括號外已經(jīng)不能被使用了
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印臺輸出結(jié)果:
可以看到,在超出了作用域后,array
依舊能夠被訪問到.
例子五:
int main(int argc, const char * argv[]) {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
id __weak array2 = array;
blk = ^(id obj){
[array2 addObject:obj];
NSLog(@"%ld",[array2 count]);
};
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印臺結(jié)果:
大相徑庭的結(jié)果.
在例子四中,Block
中截獲了外部的自動變量,并且根據(jù)上面說過的結(jié)論,編譯器為我們調(diào)用了copy
方法,這個Block
是個堆塊.
我們將例子四稍加改寫,將塊改為棧塊(即不讓編譯器為我們調(diào)用copy
方法):
int main(int argc, const char * argv[]) {
__weak blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"%ld",[array count]);
};
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印出來的結(jié)果和例子五一致.
從該例子得出的結(jié)論是:
- 只有調(diào)用了
Block
的copy
方法,才能持有截獲的附有__strong
修飾符的對象類型的自動變量值.
基于這個結(jié)論,我們還可以得出,在ARC環(huán)境下,定義block
類型的屬性時,可以用strong
,并不是非得用copy
才是正確的.
// 兩者效果一樣
@property (strong, nonatomic) blk_t *block;
@property (copy, nonatomic) blk_t *block;
在例子五中,雖然截獲的自動變量是__weak
修飾符修飾的對象類型.但是作用域過后array
被釋放,nil
被賦值給了array2
,并不能持有對象.這讓我們想起了平時為了防止循環(huán)引用,我們會用一個弱指針指向self
,并讓block
捕獲弱指針而不是讓block
持有self
.
注意,即使你不使用
self.object
訪問實例變量,而是通過_object
訪問,也同樣會造成循環(huán)引用.因為無論用什么形式訪問實例變量,經(jīng)過編譯后,最終都會轉(zhuǎn)換成self+變量內(nèi)存偏移的形式
來進行訪問,還是會造成循環(huán)引用.
那么,截獲和__block修飾有何不同呢?
block本質(zhì)也是一個結(jié)構(gòu)體,截獲的對象會成為結(jié)構(gòu)體成員的一部分.
例如:
int main(int argc, const char * argv[]) {
id obj;
^{
obj;
};
return 0;
}
其中生成的block會是這樣子的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
id __strong obj;
};
注:在C語言中,結(jié)構(gòu)體不能含有附有
__strong
修飾的變量.因為編譯器不知道應(yīng)何時進行C語言結(jié)構(gòu)體的初始化和廢棄操作,不能很好的管理內(nèi)存.而OC卻可以,它能夠準確的把握block從棧復(fù)制到堆以及堆上的block被廢棄的時機.
如果是通過__block
修飾的一個變量呢?
int main(int argc, const char * argv[]) {
__block int a;
^{
a = 10;
};
return 0;
}
其中生成的block會是這樣子的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
此時,被__block
修飾的變量變成了一個結(jié)構(gòu)體(__Block_byref_a_0
類型).至于這個結(jié)構(gòu)體又長什么樣,就不貼代碼了,只需知道a
被包裝到了這個結(jié)構(gòu)體中,成為其中一個成員變量,其他成員變量描述了該結(jié)構(gòu)體的一些信息.
- 當
Block
被拷貝到堆上的時候,附有__strong
修飾的變量因為Block
結(jié)構(gòu)體內(nèi)有強指針持有,使得該指針所指向的對象在作用域外還有引用計數(shù),因此存活著. - 當
Block
被拷貝到堆上的時候,被__block
修飾的變量被包裝
到了一個新的結(jié)構(gòu)體中,被block
結(jié)構(gòu)體持有,該結(jié)構(gòu)體跟隨Block
也被拷貝到堆上了. - 截獲的方式并不能修改截獲的變量本身,而
__block
修飾的方式卻可以,因為它本質(zhì)是復(fù)制了一份該變量.
根據(jù)結(jié)論,可以知道用__block
修飾的方式也能夠避免循環(huán)引用.只要在塊中將需要避免循環(huán)引用的?變量置為nil
.
如:
- (id)init{
self = [super init];
__block id tmp = self;
blk_t blk = ^{
tmp.name = @"ye";
tmp = nil;
}
return self;
}
如果最后不置為nil,那么
self
持有block結(jié)構(gòu)體
,block結(jié)構(gòu)體
持有__block變量結(jié)構(gòu)體
,__block變量結(jié)構(gòu)體
持有self
,只有在最后將_block變量結(jié)構(gòu)體
中的self
置空,才能手動破除循環(huán).這個方式比weak方式優(yōu)點的地方在于可控制對象的持有期間.