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