Block存儲域
</br>
1、全局塊出現的2種場景
impl.isa = &_NSConcreteGlobalBlock;
(1) 記述全局變量的地方有Block語法時
void (^blk)(void) = ^{ printf("Global Block\n"); };
int main() {
...
}
(2)Block語法的表達式中不使用截獲的自動變量時
for (int rate = 0; rate < 10; ++rate) {
(void) (^blk)(void) = ^{int count){ return count; };
}
2、棧塊
impl.isa = &_NSConcreteStackBlock;
除了上述全局塊的場景外,Block語法生成的Block對象是_NSConcreteStackBlock,因此設置在棧上。
配置在全局變量上的Block,從變量作用域外也可以通過指針安全地使用。但是設置在棧上的Block,如果其所屬的變量作用域結束,該Block就被廢棄,如同一般的自動變量。當然,Block中的__block變量也同時被廢棄。
3、堆塊
impl.isa = &_NSConcreteMallocBlock;
為了解決棧塊在其變量作用域結束之后被廢棄(釋放)的問題,我們需要把Block復制到堆中,延長其生命周期。(說起延長生命周期,讓我想起在一個非alloc/new/copy/mutableCopy方法中創建對象后,把它添加到autoreleasepool的做法。)
開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧復制到堆,如果有,自動生成將Block從棧上復制到堆上的代碼。Block的復制操作執行的是copy實例方法。
看一個返回值為Block類型的函數
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) { return rate * count; };
}
分析可知:上面的函數返回的Block是配置在棧上的,所以返回函數調用方時,Block變量作用域就結束了,Block會被廢棄。但在ARC有效,這種情況編譯器會自動完成復制。
?轉換為C++代碼看其是如何做到的:
blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0 (__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
第一條語句,將通過Block語法生成的配置在棧上的Block,賦值給變量tmp。
第二條語句,通過運行時庫可知,objc_retainBlock函數實際上是_Block_copy函數。_Block_copy函數負責將棧上的Block賦值到堆上,復制后將堆上的地址作為指針賦值給變量tmp。
第三條語句,將堆上的Block作為Objective-C對象,注冊到autoreleasepool中,然后返回該對象。
編譯器不能進行判斷的情況,需要編程人員調用copy方法手動復制:
- 向方法或函數的參數中傳遞Block時
如果在方法或函數中適當地復制了傳遞過來的參數,就不必在調用該方法或函數前手動復制。以下方法或函數不用手動復制:
- Cocoa框架的方法且方法名中含有usingBlock,如NSArray類的enumerateObjectsUsingBlock實例方法。
- Grand Central Dispatch的API,如dispatch_async函數。
!注意:將Block從棧上復制到堆上相當消耗CPU,所以當Block設置在棧上也能夠使用時,就不要復制了,因為此時的復制只是在浪費CPU資源。
Block的復制操作執行的是copy實例方法。不同類型的Block使用copy方法的效果如下表
根據表得知,Block在堆中copy會造成引用計數增加,這與其他Objective-C對象是一樣的。雖然Block在棧中也是以對象的身份存在,但是棧塊沒有引用計數,因為不需要,我們都知道棧區的內存由編譯器自動分配釋放。
至此,總結棧上的Block什么時候會復制到堆上:
- 調用Block的copy實例方法時
- Block作為函數返回值返回時
- 將Block賦值給附有__strong修飾符id類型的類或Block類型變量時
- 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中傳遞Block時
其中,第一種情況需要程序猿手動調用,后面三種情況由編譯器自動完成(開啟ARC)。