Blocks篇:4.Blocks的存儲域
在上一節(jié)中我們知道,在Block捕獲不同種類的變量時,生成的Block對象的類型(isa指針)分為三種:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
此三種類型的Block對象分別存儲在棧區(qū)、全局(數(shù)據(jù)區(qū))和堆區(qū)
我們知道,由于Block對象的函數(shù)體定義在Block實例化的生命周期外部,故其執(zhí)行時早已不在原作用域內(nèi)。況且,由于在函數(shù)中,定義的Block對象也是局部變量,超出作用域也會被自動回收。所以,要保證Block超出原作用域仍然可以存在的方式,就是將其轉(zhuǎn)化為全局Block,或者復(fù)制到堆內(nèi)存中,這樣才可以保證其內(nèi)存可控并正確執(zhí)行Block的函數(shù)。在ARC環(huán)境下,LLVM在編譯期已經(jīng)可以在絕大多數(shù)情況下正確處理這種情況。通過測試,編碼時定義的局部Block變量(即_NSConcreteStackBlock對象),在運行時可以得到如下結(jié)果:
| 捕獲變量情況 | 運行期生成的Block對象 |
|---|---|
| 無 | _NSConcreteGlobalBlock |
| 全局或靜態(tài)變量 | _NSConcreteGlobalBlock |
| 普通局部變量 | _NSConcreteMallocBlock |
但是,發(fā)現(xiàn)在一種情況下,ARC不會自動處理,需要我們對Block對象進(jìn)行手動轉(zhuǎn)換。
1.ARC下的Blocks陷阱
先看代碼:
// main.m
typedef void(^VoidBlock)(void);
/** 返回包含Block對象的集合的函數(shù) */
NSArray *getBlocksArray () {
int myVal = 2;
// 內(nèi)部的Block對象均為__NSConcreteStackBlock對象
return [[NSArray alloc] initWithObjects:
^{NSLog(@"block1~%d", myVal);},
^{NSLog(@"block2~%d", myVal);},
nil
];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 獲取該數(shù)組
NSArray *blocksArray = getBlocksArray();
// 取出Block對象
VoidBlock voidBlock = blocksArray[0];
// 執(zhí)行
voidBlock();
}
return 0;
}
執(zhí)行情況,我們可以直接得到個漂亮的“EXC_BAD_ACCESS”。

在圖中已經(jīng)看出,這種情況下,編譯器并沒有將捕獲有變量的Block拷貝至堆中。故在準(zhǔn)備執(zhí)行時,Block對象已經(jīng)被釋放(第一個被轉(zhuǎn)成__NSConcreteMallocBlock的原因是由于NSArray的init方法會自動保留對象,進(jìn)而發(fā)生了Block的copy操作)。當(dāng)執(zhí)行完畢后,由于數(shù)組對象的釋放,在對其內(nèi)部元素依次釋放時訪問了野指針,導(dǎo)致崩潰。
所以,在集合中使用Block對象時,為了保證其安全性,我們可以有兩種方式:
- 手動將Block復(fù)制到堆中:
NSArray *getBlocksArray () {
int myVal = 2;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"block1~%d", myVal);} copy],
[^{NSLog(@"block2~%d", myVal);} copy],
nil
];
}
由于Block是OC對象,故對其發(fā)送copy消息可以直接將其轉(zhuǎn)換為__NSConcreteMallocBlock對象。
- 在初始化Block時,利用ARC的特性,對Block進(jìn)行顯式聲明,以獲取“__strong”修飾的Block,自動生成__NSConcreteMallocBlock對象:
NSArray *getBlocksArray () {
int myVal = 2;
// 生成了強引用Block變量,自動分配到了堆內(nèi)存中
VoidBlock block1 = ^{NSLog(@"block1~%d", myVal);};
VoidBlock block2 = ^{NSLog(@"block2~%d", myVal);};
return [[NSArray alloc] initWithObjects:
block1,
block2,
nil
];
}
注意:例外情況
在系統(tǒng)帶有Block參數(shù)的API中(如GCD或是Animation相關(guān)等等),無需手動對Block進(jìn)行復(fù)制(其內(nèi)部實現(xiàn)已經(jīng)包含了復(fù)制操作)。
2.Blocks的保留操作
2.1 Blocks的保留解析
我們知道,在生成__strong修飾的Block對象時,其實隱含的對生成的對象進(jìn)行了retain操作。此操作實際為:
VoidBlock blockObj = ...;
// 對Block進(jìn)行保留操作
objc_retainBlock(blockObj);
...
在NSObject.mm中,我們找到了此方法的實現(xiàn):
// NSObject.mm
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
因此,對Block進(jìn)行retain其實也就是進(jìn)行了copy操作,進(jìn)而在堆上生成了Block。
2.2 Blocks的copy操作
現(xiàn)在,我們知道了對棧中的Block進(jìn)行復(fù)制或保留操作,會在堆內(nèi)存上生成對應(yīng)的Block對象。但對于其他兩者呢?
| copy對應(yīng)Block | 效果 |
|---|---|
| _NSConcreteGlobalBlock | 無作用 |
| _NSConcreteMallocBlock | 引用計數(shù) + 1 |
對于堆內(nèi)存中的Block對象,其實是遵循了引用計數(shù)的內(nèi)存管理方式。因此,在使用Block對象時,也要注意引用循環(huán)問題。