眾所周知,block可以封裝一個(gè)匿名函數(shù)為對(duì)象,并捕獲上下文所需的數(shù)據(jù),并傳給目標(biāo)對(duì)象在適當(dāng)?shù)臅r(shí)候回調(diào)。正因?yàn)閷⒊橄蟮暮瘮?shù)具體化為一個(gè)可以存儲(chǔ)管理的對(duì)象,block可以很容易被建立,管理,回調(diào),銷毀,也能很好的管理其執(zhí)行所需要的數(shù)據(jù),再加上即用即走和對(duì)代碼邏輯上下文完整等優(yōu)點(diǎn),被大多數(shù)開發(fā)者廣泛使用。雖然使用者很多,但還是有不少人對(duì)其實(shí)現(xiàn)和編譯器背后如何支持還有一些疑惑,通過(guò)閱讀本文相信你對(duì)block將會(huì)有一個(gè)比較清晰的認(rèn)知。在解決一些棘手的內(nèi)存問(wèn)題的時(shí)候?qū)?huì)更加得心應(yīng)手。
block的本質(zhì)
首先寫一個(gè)簡(jiǎn)單的block
int main(int argc, char * argv[]) {
void(^blockA)(void) = ^{};
blockA();
return 0;
}
使用簡(jiǎn)單的clang main.m -rewrite-objc得到C++的main.cpp文件
關(guān)注我們感興趣的部分,還是做個(gè)注釋吧(64bit),熟悉這些偏移量比較重要,對(duì)分析問(wèn)題很有幫助,block基礎(chǔ)大小是32byte。
extern "C" _declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" _declspec(dllexport) void *_NSConcreteStackBlock[32];
struct __block_impl {
void *isa; //8byte,isa指針,很重要的標(biāo)志,意味著block很可能是個(gè)OC的類
int Flags;//4byte,包含的引用個(gè)數(shù)
int Reserved;//4byte
void *FuncPtr;//8byte,回調(diào)函數(shù)指針
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;//8byte,block的描述,輔助工具
//如果有捕獲外部變量的話會(huì)定義在這里
...
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//自定義block函數(shù)體
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//這里Block_size=32byte
int main(int argc, char * argv[]) {
//定義函數(shù)指針,然后賦值上面靜態(tài)函數(shù),具體的代碼實(shí)現(xiàn)被移到了上面的函數(shù)中
void(*blockA)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//調(diào)用和傳參數(shù)
((void (*)(__block_impl *))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA);
return 0;
}
我們可以拷貝這些代碼來(lái)運(yùn)行,但有幾點(diǎn)需要注意的不要引入OC頭文件,否則_NSConcreteStackBlock會(huì)重復(fù)定義,我們只需要定義同樣的全局的變量來(lái)替代它或者刪掉就可以了;main中第一句代碼會(huì)報(bào)錯(cuò)taking the address of a temporary object of type '__main_block_impl_0', 這是因?yàn)檫@里調(diào)用了構(gòu)造函數(shù) _main_block_impl_0,這會(huì)生成一個(gè)臨時(shí)返回值,在c++ 11語(yǔ)法里面這個(gè)返回值是個(gè)右值引用,是無(wú)法進(jìn)行取地址運(yùn)算的。所以這里改寫一下就可以運(yùn)行了,代碼運(yùn)行起來(lái)其調(diào)用過(guò)程就比較好辦了,這里就不具體細(xì)說(shuō)了。
__main_block_impl_0 block_struct = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
void (*blockA)() = (void (*)())&block_struct;
void (*block_func)(struct main_block_impl_0 *__cself) = (void (*)(struct __main_block_impl_0 *))((__block_impl *)blockA)->FuncPtr;
block_func((struct __main_block_impl_0 *)(*blockA));
注意:我這里改寫的這個(gè)代碼是存在一些問(wèn)題的,因?yàn)閎lock_struct是棧上的,所以一旦賦值給強(qiáng)引用時(shí)會(huì)copy一份放到堆上(GlobalBlock除外),調(diào)用block的時(shí)候可能已經(jīng)超出block_struct的生命周期了。
接著看代碼
在代碼中我們看到了OC類的標(biāo)志isa指針,并且指向_NSConcreteStackBlock,但具體是不是那么回事還是需要證明一下。畢竟編譯和運(yùn)行時(shí)還是有些不一樣。
在bockA();這句加斷點(diǎn);在編譯器debug窗口左側(cè)有當(dāng)前調(diào)用棧幀可見變量,找到blockA,發(fā)現(xiàn) __isa=__NSGlobalBlock__和上面改寫代碼中的impl.isa = &_NSConcreteStackBlock還是有出入的,我們選擇相信運(yùn)行時(shí)。
選中__isa右鍵菜單View Memery of "__isa"可以瀏覽當(dāng)前內(nèi)存的值,我這里isa指向的地址是0x1003b8048,然后可以看到這里值是70 94 59 0b 01 00 00 00(8byte),小端機(jī)器,實(shí)際的數(shù)據(jù)是010B599470,觀察代碼void *_NSConcreteGlobalBlock[32]發(fā)現(xiàn)這是一個(gè)地址,對(duì)應(yīng)的地址就是0x10B599470,管它是不是OC對(duì)象,我們?cè)趌ldb下po 0x10B599470輸出一下是__NSGlobalBlock__,呵,可能有譜,再追蹤這個(gè)地址

可以看到以下內(nèi)存數(shù)據(jù)前8個(gè)byte是010B5994F0,順便輸出一下也打印了__NSGlobalBlock__,再看發(fā)現(xiàn)這個(gè)地址就在附近,里面記錄的第一個(gè)數(shù)據(jù)是010780DE58,我們知道OC對(duì)象第一個(gè)數(shù)據(jù)就是isa指針,將其構(gòu)建成地址0x10780de58,輸出一下,打印了NSObject,這里可以理出關(guān)系blockA.isa->__NSGlobalBlock__.isa->NSObject,也就是說(shuō)__NSGlobalBlock__的元類是NSObject,這基本可以證明__NSGlobalBlock__應(yīng)該是個(gè)OC類型。
但我們希望得到最直接的證明就是一直找superclass直至找到NSObject。
找到objc_class的定義,發(fā)現(xiàn)其繼承自objc_object
struct objc_class : objc_object {
Class superclass;//Class就是objc_class *
...
}
struct objc_object {
private:
isa_t isa;
}
objc_object只有一個(gè)isa_t的數(shù)據(jù)
union isa_t {
Class cls;
uintptr_t bits;//是個(gè)unsigned long
...//帶位域的struct,這里不關(guān)注
}
由此可見objc_class的前8byte是isa指針,第二個(gè)8byte是superclass指針。
我這里一次是0x010a6112a0(__NSGlobalBlock),0x10a611110(NSBlock),0x10780dea8(NSObject)
對(duì)照上面的NSObject,其地址0x10780de58,有點(diǎn)蒙,到底哪個(gè)對(duì)?其實(shí)都算對(duì)。
這里我定義了一個(gè)NSObject的對(duì)象,利用其運(yùn)行時(shí)isa和superclass的數(shù)據(jù),做了一個(gè)兩者的關(guān)系圖。

還記得這個(gè)繼承體系圖,和上面結(jié)果一致。

從相關(guān)資料可以了解,OC中除了NSProxy外,其他的類都是NSObject的子類,包括元類,這個(gè)NSObject就是下圖中的0x10780dea8。
OK,至此證明block是個(gè)OC對(duì)象,其繼承自NSBlock,NSObject。
上面的環(huán)也可以解釋一些經(jīng)典的問(wèn)題,比如(寫本文的時(shí)候查資料時(shí)剛好遇到,就貼上來(lái)了):
Class cls1 = [NSObject class];//0x10780dea8 id cls2 = (id)[NSObject class];//0x10780dea8 BOOL r1 = [cls2 isKindOfClass:cls1];//isa找到0x10780de58,再找到0x10780dea8比較 BOOL r2 = [cls2 isMemberOfClass:cls1];//isa找到0x10780de58比較 //User不存在這個(gè)環(huán),也就不會(huì)出現(xiàn)這個(gè)現(xiàn)象 Class cls3 = [User class]; id cls4 = (id)[User class]; BOOL r3 = [cls4 isKindOfClass:cls3]; BOOL r4 = [cls4 isMemberOfClass:cls3]; NSLog(@"%d %d %d %d",r1, r2, r3, r4);//結(jié)果是 1 0 0 0
之所以費(fèi)力的證明Block是個(gè)OC對(duì)象,是因?yàn)檫@可以更好的認(rèn)知Block,得到很多的信息和用法。我們或許可以像用普通的OC對(duì)象一樣使用Block,可以方便得被ARC管理,不用擔(dān)心內(nèi)存泄露或者非法訪問(wèn)。weak,autorelease等等也都可以使用,還可以放在集合里面,可以被別的對(duì)象持有,當(dāng)然也可以持有別的對(duì)象,了解到這一點(diǎn)對(duì)于我們分析block的相關(guān)的內(nèi)存管理和循環(huán)引用意義重大。
但在重寫的C++的代碼中我們看不到編譯器幫我們插入的release,retain這樣的代碼,所以我們不得不用別的辦法來(lái)了解Block具體是否被ARC管理的。
Block本身的內(nèi)存管理
首先要明確一件事:Block本身內(nèi)存的管理和Block捕獲的對(duì)象的內(nèi)存管理是兩個(gè)問(wèn)題。這里我們先討論前者。
前面遺留了一個(gè)問(wèn)題就是,代碼里面isa指針明明指向了_NSConcreteStackBlock,怎么到了運(yùn)行時(shí)的時(shí)候就變成了__NSGlobalBlock__?
我們?cè)僮鲆粋€(gè)實(shí)驗(yàn),將代碼改為
void afunc() {
__unsafe_unretained void(^blockA)(void) = ^{};
blockA();
}
int main(int argc, char * argv[]) {
afunc();
return 0;
}
在blockA()處下個(gè)斷點(diǎn),查看debug數(shù)據(jù),發(fā)現(xiàn)isa指針確實(shí)指向的是__NSGlobalBlock__,也就是說(shuō)在這之前就被更新了,目前我還沒(méi)有找到這個(gè)更新時(shí)機(jī)。
我們發(fā)現(xiàn)調(diào)用blockA()可以成功,沒(méi)有crash,我嘗試取了一下retainCount發(fā)現(xiàn)是1,去掉__unsafe_unretained也一樣。
注意:通過(guò)kvc可以獲取對(duì)象的引用計(jì)數(shù),如果一個(gè)函數(shù)來(lái)打印對(duì)象的引用計(jì)數(shù),這函數(shù)的參數(shù)聲明是有講究的
void printRetainCount(__unsafe_unretained id o) {
void *p = (__bridge void *)o;
NSLog(@"%p:%d",p,[o valueForKey:@"retainCount"]);
}
參數(shù)需要用
__unsafe_unretained來(lái)修飾,最好不要用強(qiáng)引用,這會(huì)導(dǎo)致引用計(jì)數(shù)器+1,更不能用weak,這會(huì)導(dǎo)致每次使用weak對(duì)象的時(shí)候,retainCount都會(huì)增加,這個(gè)坑一不小心就會(huì)忽略,導(dǎo)致獲取的數(shù)據(jù)可能不準(zhǔn)確,關(guān)于這個(gè)問(wèn)題具體情況以后有機(jī)會(huì)再討論。
修改代碼增加全局__weak void(^blockB)(void) = nil;,并在afunc()對(duì)其賦值,在main()中調(diào)用blockB(),發(fā)現(xiàn)也可以調(diào)用成功,并沒(méi)有crash。通過(guò)__NSGlobalBlock__這個(gè)名字大概可以猜測(cè)出這個(gè)是一個(gè)全局的block,其生命周期全局有效,即使主動(dòng)調(diào)用copy,也不會(huì)入堆,似乎不受ARC控制。對(duì)照源碼可知,globalblock其實(shí)并不依賴外部數(shù)據(jù),只要有代碼入口就可以使用,甚至不需要知道block,只有有函數(shù)入口地址就可以直接調(diào)用,而另外兩種都需要通過(guò)block去調(diào)用,而不能直接調(diào)用block內(nèi)函數(shù)指針(當(dāng)然要是自己準(zhǔn)備各種參數(shù)也是可以的)。
將代碼修改為:
void afunc() {
int a = 100;
__unsafe_unretained void(^blockA)(void) = ^{
int b = a;
};
blockA();
}
int main(int argc, char * argv[]) {
afunc();
return 0;
}
在blockA()處下個(gè)斷點(diǎn),查看debug數(shù)據(jù),發(fā)現(xiàn)isa指針指向的是__NSStackBlock__,去掉 __unsafe_unretained后isa指針變成了__NSMallocBlock__。
我們發(fā)現(xiàn)調(diào)用blockA()可以成功,沒(méi)有crash,我嘗試取了一下retainCount發(fā)現(xiàn)是1,去掉__unsafe_unretained也一樣,??,跟一般的OC對(duì)象不太一樣?
再次修改代碼和上面一下增加__weak void(^blockB)(void) = nil,在main中調(diào)用blockB(),發(fā)現(xiàn)其crash了,證明blockB已經(jīng)無(wú)效了。再次將__unsafe_unretained修改為__autoreleasing,發(fā)現(xiàn)其可以調(diào)用成功,所以證明block此時(shí)被autoreleasepool接管了,看上去ARC還是有作用的。
那么在ARC下,如果增加一個(gè)強(qiáng)引用指向block會(huì)不會(huì)導(dǎo)致retainCount增加呢?通過(guò)實(shí)驗(yàn)發(fā)現(xiàn)不會(huì),依舊是1,這一點(diǎn)又和普通的對(duì)象不太一樣。
難道這就是真相,no,這無(wú)法解釋之前觀察到的各種現(xiàn)象。我多次運(yùn)行,多次調(diào)用并打印block的地址,發(fā)現(xiàn)其地址都一樣。
Printing description of *(blockA).__FuncPtr:
(void (*)(NSString *, int)) __FuncPtr = 0x0000000100f66570 (Block`__afunc_block_invoke at main.m:58)
仔細(xì)一看,發(fā)現(xiàn)其打印的地址和__FuncPtr地址一樣,那么同理取的block的retainCount也就可有能不正確,去objc源碼中搜索了一下發(fā)現(xiàn)其實(shí)現(xiàn)為
-(NSUInteger)retainCount {
return (_rc_ivar + 2) >> 1;
}
也就是說(shuō),只要對(duì)象沒(méi)有被釋放,那么其retainCount至少是1。換句話說(shuō),如果某個(gè)對(duì)象沒(méi)有_rc_ivar,或者_rc_ivar=0,那么其結(jié)果都是1,所以這里通過(guò)KVC取retainCount在block這里并不可靠,因?yàn)锳RC機(jī)制下并不允許訪問(wèn)retainCount,所以其可靠性在有些情況還是會(huì)受到質(zhì)疑的,不足以作為判斷標(biāo)準(zhǔn)。但是我們發(fā)現(xiàn)一個(gè)問(wèn)題,就是分配在棧上的block出了作用域已經(jīng)無(wú)效了,那也就是說(shuō)block應(yīng)該在一定程度上受到ARC機(jī)制的約束,這需要進(jìn)一步求證。
還記isa_a定義么,接下來(lái)我們?nèi)]一下完整源碼:
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
}
我們這次只關(guān)注其中shiftcls和extra_rc,前者是存放isa指針存儲(chǔ)數(shù)據(jù)的真正空間,后者是存放對(duì)象額外引用計(jì)數(shù)的,如果這里19 bits還不夠的話,就使用sidetable來(lái)記錄。也就是說(shuō),絕大部分情況下,引用計(jì)數(shù)器是存在對(duì)象的isa里面的,所以我們只需要去查看isa的內(nèi)存值,解析最后19bits的值就可以得到引用計(jì)數(shù)。
打個(gè)斷點(diǎn),在這里選擇debug窗口左側(cè)的variables view窗口,選中某個(gè)block指針,右鍵 view memery of "*blockA",可能不能瀏覽,所以view memery of "blockA",我這里是內(nèi)存地址0x16ee9f8f8存儲(chǔ)了以下數(shù)據(jù)
90 54 44 C0 01 00 00 00
將其構(gòu)建出地址0x01c0555490,在Address中輸入該值,跳轉(zhuǎn)到新地址,我這里結(jié)果如下:
88 FF A7 B3 01 00 00 00 02 00 00 C3 00 00 00 00 70 65 F6 00 01 00 00 00
看到最后一個(gè)8個(gè)byte,有點(diǎn)眼熟,就是之前打印的__FuncPtr = 0x0000000100f66570。那前倆byte存的是啥呢?第一個(gè)byte明顯是一個(gè)指針,打印一下,就是__NSMallocBlock__,那剩下的8byte呢?從block的數(shù)據(jù)結(jié)構(gòu)了解到其對(duì)應(yīng)的就是
int Flags;//4byte,包含的引用個(gè)數(shù)
int Reserved;//4byte
后者是0,前者是有值而且會(huì)變化,我嘗試再給block一個(gè)強(qiáng)引用,發(fā)現(xiàn)02 00 00 C3變成了04 00 00 C3,再賦值就變成了06 00 00 C3,所以這個(gè)06應(yīng)該就是引用計(jì)數(shù)器,而且也符合retainCount的運(yùn)算邏輯,從內(nèi)存布局上看,19bits的存儲(chǔ)位置應(yīng)該在一個(gè)8-byte的末尾,也就是包含02這段空間,但只是不太了解為啥isa被分成了兩個(gè)64bits存儲(chǔ)。
同理我嘗試了僅僅在stack上的block,其數(shù)據(jù)位00 00 00 C2,計(jì)數(shù)器為0。
同理我嘗試了global的block數(shù)據(jù)位00 00 00 50,計(jì)數(shù)器為0。
結(jié)果符合預(yù)期,除了進(jìn)入堆上block會(huì)受ARC約束,其他的block都不需要ARC參與就可以完成內(nèi)存管理。
小結(jié)
- Block如果不捕獲外界變量,就沒(méi)有上下文依賴,編譯器會(huì)將其標(biāo)記為global類型(當(dāng)然可能編譯器標(biāo)記為stack,運(yùn)行時(shí)優(yōu)化glabal常量);否則編譯器會(huì)在創(chuàng)建時(shí)將其標(biāo)記為stack,當(dāng)運(yùn)行時(shí)對(duì)象被強(qiáng)引用時(shí)或者主動(dòng)調(diào)用copy會(huì)被標(biāo)記為malloc類型。
- global和stack的block都不需要ARC參與內(nèi)存管理。malloc的block將受到ARC管理,包括autorelease和weak。
Block參數(shù)傳遞
前面的小節(jié)研究了Block的本質(zhì)和其本身的內(nèi)存管理,我們幾乎可以把他當(dāng)做普通對(duì)象來(lái)使用,同時(shí)其擁有唯一的成員函數(shù),其執(zhí)行所要依賴的數(shù)據(jù)來(lái)源有兩個(gè),一個(gè)是當(dāng)前上下文環(huán)境的各種變量,另外就是調(diào)用方的傳參。block傳參和函數(shù)傳參并沒(méi)有什么不同,這里就不做具體討論。
Block如何捕獲外界變量
之前為了重寫簡(jiǎn)單我并沒(méi)有引入OC基礎(chǔ)框架,而要將一般的OC代碼轉(zhuǎn)成C++,比如以下代碼就引用了NSString:
typedef void (^ABlock)(void);
ABlock afunc() {
NSString *a = @"this is a demo";
void(^blockA)(void) = ^{
NSString *b = a;
};
return blockA;
}
int main(int argc, char *argv[]) {
ABlock aBlock = afunc();
aBlock();
return 0;
}
對(duì)于這類引用了OC類型的代碼,需要使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.10 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m可以將OC代碼轉(zhuǎn)成C++代碼。
這里需要指定編譯的sdk庫(kù)(我使用的是iPhoneSimulator.sdk),否則會(huì)出現(xiàn)“UIKit/UIKit.h” file not found,還需要指定-fobjc-arc開啟ARC的編譯開關(guān)和-fobjc-runtime=macosx-10.10,否則會(huì)出現(xiàn)“cannot create __weak reference in file using manual reference counting”類似的錯(cuò)誤。
編譯真機(jī)的話需要指定支持的CPU架構(gòu)和庫(kù)等(折騰了挺久才試出這些參數(shù),??)clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-10.0 -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk main.m
編譯器會(huì)根據(jù)捕獲的原始變量的不同情況,定義不同類型的變量來(lái)存儲(chǔ)這些數(shù)據(jù)。
根據(jù)變量定義類型,這里我分成以下幾類:
- 以下是捕獲一個(gè)基本類型臨時(shí)變量i的c++代碼
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//如果有捕獲外部變量的話會(huì)定義在這里
int i;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,編譯器定義了一個(gè)int i來(lái)對(duì)應(yīng)外界的int i,同時(shí)接收了外界i的值來(lái)初始化。
- 同理如果是局部指針這里將定義為對(duì)應(yīng)的指針,例如這里是 int *ip。
- 如果需要捕獲的是一個(gè)局部OC對(duì)象,其實(shí)和2中情況一致,不同之處在于ARC會(huì)介這個(gè)對(duì)象的管理。
- 對(duì)于全局變量,因?yàn)樵L問(wèn)是開放的,所以編譯器不需要做處理,直接使用該變量就行。
根據(jù)變量定義的附加修飾特性:
- 對(duì)于局部static變量,因?yàn)樵L問(wèn)不開放,所以會(huì)被編譯器升級(jí)為指針,例如:static int staticInt = 100,會(huì)定義一個(gè)int *staticInt來(lái)捕獲staticInt的地址,以便以后修改。
- 對(duì)于__weak修飾的對(duì)象引用,這個(gè)重點(diǎn)說(shuō)明。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSString *__weak weakSelf;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到對(duì)于__weak修飾的引用,編譯器也在Block中定義一個(gè)一模一樣的引用,然后通過(guò)構(gòu)造函數(shù)通過(guò)參數(shù)傳入初始化結(jié)構(gòu)體(c++中struct和class絕大部分情況是等效的),這是意味著什么呢?我們知道所有函數(shù)參數(shù)傳遞就一種方式——copy,這里的參數(shù)捕獲間接套用了該性質(zhì)。一句話來(lái)說(shuō),對(duì)象還是那個(gè)對(duì)象,但引用已經(jīng)不是那個(gè)引用了。

這里我畫了一個(gè)簡(jiǎn)圖,實(shí)際上每個(gè)weakSelf都是不一樣的,只是其指示的內(nèi)容是同樣的。
- __block修飾的對(duì)象,這里改寫為c++代碼出錯(cuò),我沒(méi)能解決這個(gè)問(wèn)題。所以就只有推測(cè)了,其做法應(yīng)該和局部的static變量捕獲差不多,都會(huì)定義一個(gè)同類型的指針或者引用,以便可以在block中訪問(wèn)該變量修改變量。
小結(jié)
- 參數(shù)捕獲和參數(shù)傳遞,前者發(fā)生在block定義初始化的時(shí)候,是對(duì)當(dāng)前現(xiàn)場(chǎng)的一種保存,后者發(fā)生在調(diào)用的時(shí)候傳參,其存儲(chǔ)上的區(qū)別是前者是成員變量持續(xù)存儲(chǔ),后者是臨時(shí)變量。相同之處就是獲取方式完全一致,都是函數(shù)參數(shù)傳遞。
- 編譯器會(huì)對(duì)待不同類型的參數(shù)捕獲處理方式都一樣,全部淺拷貝;對(duì)于不同修飾參數(shù)則不太一樣,會(huì)根據(jù)不同的情況來(lái)決定是否升級(jí)為指針捕獲;OC對(duì)象將會(huì)引入ARC機(jī)制去管理。
Block循環(huán)引用及解決辦法
如果能明確認(rèn)識(shí)到block就是個(gè)對(duì)象,那么造成循環(huán)引用的原因就不難理解了,block可以持有對(duì)象也可以被對(duì)象持有,如果兩者直接或者間接包含同一對(duì)象時(shí)就成了環(huán),實(shí)際上就是object->block(->…)->object。
那么為什么用weak strong dance就可以解決這個(gè)問(wèn)題呢?看下面這個(gè)典型例子。
__weak typeof(self) weakSelf = self;
void (^block)(void) = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
};
通過(guò)前面的C++的代碼分析,答案已經(jīng)很清晰了,這里就再解釋一次:
我們知道block外部定義了一個(gè)weakSelf(為了方便說(shuō)明,可以認(rèn)為是weakSelf1),而在block內(nèi)部并沒(méi)有直接使用這個(gè)weakSelf1(就是沒(méi)有使用這個(gè)weakSelf1這個(gè)硬編碼或者說(shuō)其對(duì)應(yīng)的地址),而是另外定義了一個(gè)對(duì)應(yīng)的構(gòu)造函數(shù)參數(shù)__weak weakSelf(weakSelf2),通過(guò)指針copy傳參的方式,weakSelf2指向了weakSelf1指向的內(nèi)容,同時(shí)block內(nèi)部的成員變量 __weak weakSelf(weakSelf3)通過(guò)weakSelf賦值也指向了weakSelf1指向的內(nèi)容。所以從始至終這些weakSelf都不是同一個(gè)東西。至于strongSelf就簡(jiǎn)單了,對(duì)象賦值給強(qiáng)引用會(huì)導(dǎo)致retainCount+1,還記我之前文章里面的觀點(diǎn)么,ARC是用棧管理引用,用引用生命周期管理對(duì)象,所有strongSelf生命周期結(jié)束,自然retainCount-1。
所以在block還沒(méi)有執(zhí)行的時(shí)候,self的生命周期不受block影響,如果執(zhí)行的時(shí)候self已經(jīng)被釋放, weakSelf3=nil,也不會(huì)導(dǎo)致問(wèn)題,但是如果weakSelf3還有值,strongSelf就會(huì)導(dǎo)致retainCount+1。有很多人認(rèn)為,無(wú)論如何必須等到block執(zhí)行完或者銷毀self才會(huì)釋放是不正確的。仔細(xì)對(duì)照block和delegate就會(huì)發(fā)現(xiàn)兩者在這方面其實(shí)本質(zhì)是一樣的的,如果delegate不使用weak也一樣可能循環(huán)引用。還是那句話,內(nèi)存中通信就一個(gè)招,拿到地址,所以無(wú)論是直接的delegate,block,target-action,還是間接的Notification,或者其他的玩法都一樣。
注意:__strong不能省略。
當(dāng)然并不是說(shuō)見到block就需要weak strong dance,對(duì)于以下情況就可以不使用(從調(diào)用方和回調(diào)方分析)
- 如果能確定block的調(diào)用方并不會(huì)長(zhǎng)期持有block,比如傳給B一個(gè)A持有的block,B并不存儲(chǔ),而是立刻回調(diào),常見的就是把block當(dāng)函數(shù)參數(shù)傳遞。
- 如果確定block調(diào)用方會(huì)在必要的時(shí)候去除強(qiáng)引用,比如:dispatch_async,其雖然會(huì)被隊(duì)列強(qiáng)引用,但在block回調(diào)的時(shí)候,
_dispatch_call_block_and_release會(huì)在執(zhí)行完release,這也不會(huì)導(dǎo)致循環(huán)引用。 - block創(chuàng)建方不會(huì)直接或間接強(qiáng)引用block。
- 對(duì)于絕不可能持有block的對(duì)象,可以放心捕獲,比如NSString,NSDate,NSURL等等,但對(duì)于一些可能存儲(chǔ)block需要小心,比如:NSArray,NSDictionary,自定義的對(duì)象(self)。
如果你是創(chuàng)建方,不想去分析也不知道調(diào)用方干了什么,建議就無(wú)腦weak strong dance,幾乎可以就可以解決問(wèn)題了。如果你是調(diào)用方,會(huì)麻煩一些,需要具體問(wèn)題具體分析。
Block捕獲對(duì)象的內(nèi)存管理
這分成三個(gè)方面,如果只是基本類型,那就不需要操心;如果是C指針,那指向?qū)ο蟮纳芷谛枰_發(fā)者手動(dòng)管理;如果是個(gè)OC對(duì)象,內(nèi)存管理由ARC代勞,只需要注意一些特殊情況就好。前兩者不做討論,研究一下后者。
typedef void(^ABlock)();
void pc(__unsafe_unretained id o) {
void *p = (__bridge void *)o;
NSLog(@"%@ %p:%@",o, p, [o valueForKey:@"retainCount"]);
}
@interface BlockDemo : NSObject
@end
@implementation BlockDemo
static int global = 1000;
- (ABlock)afunc:(NSString *)string {
pc(self);
pc(string);
ABlock b;
ABlock c;
__weak typeof(self) weakSelf = self;
b = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
pc(strongSelf);
pc(string);
};
c = b;
b();
pc(self);
pc(string);
return b;
}
@end
int main(int argc, char * argv[]) {
NSString *string = [[NSString alloc] initWithUTF8String:"this is a demo"];
pc(string);
BlockDemo *block = [BlockDemo new];
ABlock a = [block afunc:string];
a();
}
此時(shí)輸出日志如下:
2018-05-15 11:44:46.626942+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:1
2018-05-15 11:44:46.627326+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:1
2018-05-15 11:44:46.627515+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2
2018-05-15 11:44:46.627588+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:2
2018-05-15 11:44:46.627719+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4
2018-05-15 11:44:46.627786+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:1
2018-05-15 11:44:46.627810+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4
2018-05-15 11:44:46.627995+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:2
2018-05-15 11:44:46.628072+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2
可以看到BlockDemo只在main中被持有+1,然后調(diào)用時(shí)被strongSelf持有+1,weakSelf并沒(méi)導(dǎo)致引用計(jì)數(shù)器增加,與輸出日志相符。
進(jìn)入afunc:時(shí),string引用計(jì)數(shù)為2,一個(gè)發(fā)生在main,一個(gè)發(fā)生在afunc:的參數(shù)中聲明中。調(diào)用b()時(shí),retainCount=4,這是因?yàn)檫@是存在一個(gè)StackBlock和一個(gè)MallocBlock,這兩個(gè)都會(huì)有一個(gè)引用指向string。在afunc:函數(shù)末尾打印string是4也是同樣的原因;當(dāng)afunc:執(zhí)行完后,StackBlock已經(jīng)釋放,返回block給main中的a,此時(shí)調(diào)用a(),輸出2,其中引用來(lái)自于MallocBlock,符合預(yù)期。
c=b這句賦值,只引起block計(jì)數(shù)器增加,而不會(huì)導(dǎo)致捕獲OC對(duì)象引用計(jì)數(shù)器增加,符合預(yù)期。
我們?cè)趌ldb設(shè)置倆符號(hào)斷點(diǎn):
breakpoint set -n _Block_copy
breakpoint set -n _Block_release
可以發(fā)現(xiàn)_Block_copy和普通對(duì)象retain時(shí)機(jī)調(diào)用類似。
需要注意的問(wèn)題
-
block捕獲變量防止循環(huán)引用容易漏掉一些情況,在捕獲時(shí)需要多注意,舉個(gè)例子,直接捕獲成員變量。
假設(shè)在一個(gè)對(duì)象方法里面,比如ViewController void (^block)(void) = ^{ //這里是等效于self->_name,編譯器編碼為self+offset(_name),依然會(huì)導(dǎo)致強(qiáng)引用 NSString *name = _name; };
-
直接修改捕獲的變量不能成功,因?yàn)槔锿獾膬蓚€(gè)array不是一個(gè)array,需要加上
__block,變量捕獲通過(guò)函數(shù)傳參的方式實(shí)現(xiàn),而傳參全是copy。NSArray *array = @[@1,@2]; void (^block)(void) = ^{ array = @[]; };?
附加內(nèi)容
之前從宏觀層面了解了block和其捕獲對(duì)象的生命周期,但具體是怎樣還是不太清晰,有興趣的話可以看下面一段內(nèi)容,具體了解block是怎么玩的,匯編較長(zhǎng),看起來(lái)也比較繞,沒(méi)興趣的話可以忽略,看看其中的一些要點(diǎn)。
源碼:
typedef void (^ABlock)(void);
ABlock afunc() {
NSString *a = @"demo";
void(^blockA)(void) = ^{
NSString *b = a;
};
return blockA;
}
int main(int argc, char *argv[]) {
ABlock aBlock = afunc();
aBlock();
return 0;
}
匯編:
.section __TEXT,__text,regular,pure_instructions
.ios_version_min 11, 0
.file 1 "/Users/Wei/File/program/Block" "/Users/Wei/File/program/Block/Block/main.m"
.globl _afunc ; -- Begin function afunc
.p2align 2
_afunc: ; @afunc
Lfunc_begin0:
.loc 1 33 0 ; /Users/Wei/File/program/Block/Block/main.m:33:0
.cfi_startproc
; BB#0:
sub sp, sp, #112 ; =112
stp x29, x30, [sp, #96] ; 8-byte Folded Spill
add x29, sp, #96 ; =96
Lcfi0:
.cfi_def_cfa w29, 16
Lcfi1:
.cfi_offset w30, -8
Lcfi2:
.cfi_offset w29, -16
Ltmp0:
.loc 1 34 15 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:34:15
adrp x0, l__unnamed_cfstring_@PAGE
add x0, x0, l__unnamed_cfstring_@PAGEOFF
bl _objc_retain
stur x0, [x29, #-8]
add x0, sp, #40 ; =40
.loc 1 36 11 ; /Users/Wei/File/program/Block/Block/main.m:36:11
add x30, x0, #32 ; =32
.loc 1 36 27 is_stmt 0 ; /Users/Wei/File/program/Block/Block/main.m:36:27
adrp x8, __NSConcreteStackBlock@GOTPAGE
ldr x8, [x8, __NSConcreteStackBlock@GOTPAGEOFF]
str x8, [sp, #40]
mov w9, #-1040187392
str w9, [sp, #48]
mov w9, #0
str w9, [sp, #52]
adrp x8, ___afunc_block_invoke@PAGE
add x8, x8, ___afunc_block_invoke@PAGEOFF
str x8, [sp, #56]
adrp x8, ___block_descriptor_tmp@PAGE
add x8, x8, ___block_descriptor_tmp@PAGEOFF
str x8, [sp, #64]
ldur x8, [x29, #-8]
str x0, [sp, #32] ; 8-byte Folded Spill
mov x0, x8
str x30, [sp, #24] ; 8-byte Folded Spill
bl _objc_retain
str x0, [sp, #72]
.loc 1 36 11 ; /Users/Wei/File/program/Block/Block/main.m:36:11
ldr x0, [sp, #32] ; 8-byte Folded Reload
bl _objc_retainBlock
stur x0, [x29, #-16]
.loc 1 44 12 is_stmt 1 ; /Users/Wei/File/program/Block/Block/main.m:44:12
ldur x0, [x29, #-16]
bl _objc_retainBlock
sub x8, x29, #16 ; =16
mov x30, #0
.loc 1 45 1 ; /Users/Wei/File/program/Block/Block/main.m:45:1
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x8
mov x1, x30
str x30, [sp, #8] ; 8-byte Folded Spill
bl _objc_storeStrong
ldr x0, [sp, #24] ; 8-byte Folded Reload
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
sub x0, x29, #8 ; =8
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
ldr x0, [sp, #16] ; 8-byte Folded Reload
ldp x29, x30, [sp, #96] ; 8-byte Folded Reload
add sp, sp, #112 ; =112
b _objc_autoreleaseReturnValue
Ltmp1:
Lfunc_end0:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __afunc_block_invoke
___afunc_block_invoke: ; @__afunc_block_invoke
Lfunc_begin1:
.loc 1 36 0 ; /Users/Wei/File/program/Block/Block/main.m:36:0
.cfi_startproc
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
Lcfi3:
.cfi_def_cfa w29, 16
Lcfi4:
.cfi_offset w30, -8
Lcfi5:
.cfi_offset w29, -16
stur x0, [x29, #-8]//sp+24的位置
Ltmp2:
.loc 1 36 28 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:28
mov x8, x0
str x8, [sp, #16]
Ltmp3:
.loc 1 37 20 ; /Users/Wei/File/program/Block/Block/main.m:37:20
ldr x8, [x0, #32] //x0是block首地址,x0+32是捕獲的第一個(gè)變量位置,就是NSString
mov x0, x8
bl _objc_retain
mov x8, #0
add x30, sp, #8 ; =8
str x0, [sp, #8] //將其存在了棧上sp+8的位置,就是b變量
Ltmp4:
.loc 1 38 5 ; /Users/Wei/File/program/Block/Block/main.m:38:5
mov x0, x30
mov x1, x8
bl _objc_storeStrong
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
Ltmp5:
Lfunc_end1:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __copy_helper_block_
___copy_helper_block_: ; @__copy_helper_block_
Lfunc_begin2:
.loc 1 38 0 ; /Users/Wei/File/program/Block/Block/main.m:38:0
.cfi_startproc
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
Lcfi6:
.cfi_def_cfa w29, 16
Lcfi7:
.cfi_offset w30, -8
Lcfi8:
.cfi_offset w29, -16
mov x8, #0
stur x0, [x29, #-8] //目標(biāo)地址
str x1, [sp, #16] //block
Ltmp6:
.loc 1 36 27 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:27
ldr x0, [sp, #16]//block
ldur x1, [x29, #-8]//目標(biāo)地址
mov x9, x1
add x9, x9, #32 //目標(biāo)地址 ; =32
ldr x0, [x0, #32] //對(duì)象a
str x8, [x1, #32]
str x0, [sp, #8] ; 8-byte Folded Spill
mov x0, x9
ldr x1, [sp, #8] //對(duì)象a ; 8-byte Folded Reload
bl _objc_storeStrong
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
Ltmp7:
Lfunc_end2:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __destroy_helper_block_
___destroy_helper_block_: ; @__destroy_helper_block_
Lfunc_begin3:
.loc 1 36 0 ; /Users/Wei/File/program/Block/Block/main.m:36:0
.cfi_startproc
; BB#0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 8-byte Folded Spill
add x29, sp, #16 ; =16
Lcfi9:
.cfi_def_cfa w29, 16
Lcfi10:
.cfi_offset w30, -8
Lcfi11:
.cfi_offset w29, -16
mov x8, #0
str x0, [sp, #8]
Ltmp8:
.loc 1 36 27 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:27
ldr x0, [sp, #8]
add x0, x0, #32 ; =32
mov x1, x8
bl _objc_storeStrong
ldp x29, x30, [sp, #16] ; 8-byte Folded Reload
add sp, sp, #32 ; =32
ret
Ltmp9:
Lfunc_end3:
.cfi_endproc
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
Lfunc_begin4:
.loc 1 47 0 ; /Users/Wei/File/program/Block/Block/main.m:47:0
.cfi_startproc
; BB#0:
sub sp, sp, #64 ; =64
stp x29, x30, [sp, #48] ; 8-byte Folded Spill
add x29, sp, #48 ; =48
Lcfi12:
.cfi_def_cfa w29, 16
Lcfi13:
.cfi_offset w30, -8
Lcfi14:
.cfi_offset w29, -16
stur wzr, [x29, #-4]
stur w0, [x29, #-8]
stur x1, [x29, #-16]
Ltmp10:
.loc 1 50 16 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:50:16
bl _afunc
.loc 1 50 12 is_stmt 0 ; /Users/Wei/File/program/Block/Block/main.m:50:12
; InlineAsm Start
mov x29, x29 ; marker for objc_retainAutoreleaseReturnValue
; InlineAsm End
bl _objc_retainAutoreleasedReturnValue
str x0, [sp, #24]
.loc 1 51 5 is_stmt 1 ; /Users/Wei/File/program/Block/Block/main.m:51:5
ldr x0, [sp, #24]
mov x1, x0
ldr x0, [x0, #16]
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x1
ldr x1, [sp, #16] ; 8-byte Folded Reload
blr x1
mov x0, #0
add x1, sp, #24 ; =24
.loc 1 62 5 ; /Users/Wei/File/program/Block/Block/main.m:62:5
stur wzr, [x29, #-4]
.loc 1 63 1 ; /Users/Wei/File/program/Block/Block/main.m:63:1
str x0, [sp, #8] ; 8-byte Folded Spill
mov x0, x1
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
ldur w0, [x29, #-4]
ldp x29, x30, [sp, #48] ; 8-byte Folded Reload
add sp, sp, #64 ; =64
ret
Ltmp11:
Lfunc_end4:
.cfi_endproc
我們需要從上層函數(shù)入手,只有了解了傳入的參數(shù)具體分析,才比較容易了解代碼功能,不然就頭疼了,這里就是先分析main函數(shù)。
main:
- bl _afunc,沒(méi)有參數(shù)直接跳轉(zhuǎn),從源碼可知返回了一個(gè)block對(duì)象在x0中,bl _objc_retainAutoreleasedReturnValue表明其被autoreleasepool管理。
- 接下將x0存在了sp+24這里,再下一句沒(méi)有啥意義。
- 將x0賦值給x1,挪出空間,加載x0+16的值到x0,找到最開始struct __block_impl的內(nèi)存布局,發(fā)現(xiàn)這個(gè)地址存放的是回調(diào)函數(shù)的指針。
- 接下來(lái)通過(guò)[sp, #16]中轉(zhuǎn),將x1和x0內(nèi)容互換,至此x0是block首地址,x1是回調(diào)函數(shù)地址;blr x1跳轉(zhuǎn)到x1;2,3,4步加一起就是源碼里面的aBlock()。
- 最后那段就是在release之前的block對(duì)象。
_afunc:
最前面棧增長(zhǎng)了112個(gè)byte,這里局部變量較多所以,棧分配的較大。
- 直接看Ltmp0:,前面一個(gè)_objc_retain是引用了字符串"demo"
. stur x0, [x29, #-8],將x0("demo")存在了x29-8這個(gè)位置,也就是sp+88的位置。
然后x0=sp+40,x30=sp+72
-
接下來(lái)兩句加載"__NSConcreteStackBlock"符號(hào)對(duì)應(yīng)的地址,然后將其存在sp+40這個(gè)地址,而x0目前是指向這個(gè)地址的。
struct __block_impl { void *isa; //8byte,isa指針,很重要的標(biāo)志,意味著block很可能是個(gè)OC的類 int Flags;//4byte,包含的引用個(gè)數(shù) int Reserved;//4byte void *FuncPtr;//8byte,回調(diào)函數(shù)指針 }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc;//8byte,block的描述 //如果有捕獲外部變量的話會(huì)定義在這里 id a; };這里貼一個(gè)內(nèi)存布局,sp+40就是
__main_block_impl_0首地址,也是isa地址,這里就是“StackBlock”類似符號(hào)。 接下來(lái)就簡(jiǎn)單了,依次存儲(chǔ)了Flags(sp+48),Reserved(sp+52)各4個(gè)byte。
存儲(chǔ)FuncPtr指針,就是匯編符號(hào)
___afunc_block_invoke對(duì)應(yīng)的地址,sp+56的8個(gè)byte。arm64指針占8個(gè)byte。存儲(chǔ)Desc,這也是個(gè)指針,存儲(chǔ)的是
___block_descriptor_tmp對(duì)應(yīng)的地址,其記錄了___copy_helper_block,___destory_helper_block和method signature,block size等信息,對(duì)應(yīng)sp+64的8個(gè)byte。接下來(lái)加載x29-8的內(nèi)容到x8就是"demo"字符串。然后將x0暫存在sp+32,同時(shí)將x0=x8,然后存儲(chǔ)x30到sp+24
調(diào)用_objc_retain,參數(shù)x0,所以結(jié)果是retain了“demo”一次。之后將x0存儲(chǔ)在了sp+72這里,就是
struct __main_block_impl_0中的id a,id a是我隨意寫的,但實(shí)際定義也應(yīng)該差不多的。需要注意的是,這里的調(diào)用是因?yàn)閯?chuàng)建了一個(gè)StackBlock,其也是要使用"demo"這個(gè)數(shù)據(jù)的,所以retain了一次。但MallocBlock的引用計(jì)數(shù)則由___copy_helper_block來(lái)管理。)然后將sp+32的內(nèi)容加載到x0,也就是sp+40即
__main_block_impl_0的首地址。調(diào)用_objc_retainBlock,去蘋果源碼中找一下:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
其調(diào)用了_Block_copy,這個(gè)函數(shù)在Block.h中聲明的,我沒(méi)有找到相關(guān)的實(shí)現(xiàn)源碼,不過(guò)可以證明的是對(duì)于block來(lái)說(shuō)retain和copy效果一致。
-
再次調(diào)用了
_objc_retainBlock這里的兩次調(diào)用一次是賦值給blockA,一次是return blockA造成的。
-
后面有三個(gè)
_objc_storeStrong,x1=0,都是在做release操作,具體過(guò)程比較繁瑣,我就直接給結(jié)論了,其中第一個(gè)release一次blockA,第二release一次“demo”字符串(提示:sp+24存的是x30,x30=sp+40+32,這里存的是“demo”),第三個(gè)也是release一次"demo"(sub x0, x29, #8,提示一下,x29=sp+112-96,所以這句也是x0=sp+24)通過(guò)這里的分析,對(duì)于Block捕獲對(duì)象,ARC怎么作用的,相應(yīng)的數(shù)據(jù)結(jié)構(gòu):block retain會(huì)被copy,捕獲OC類型參數(shù)的時(shí)候會(huì)retain參數(shù),參數(shù)的傳遞方式——拷貝。
___afunc_block_invoke:
- ldr x8, [x0, #32],x0是block的首地址,x0+32就是變量a的地址,mov x0,x8,bl _objc_retain,retain了a這個(gè)對(duì)象,和源碼功能一致。
- 后面就是在release這個(gè)對(duì)象,源碼里面也確實(shí)沒(méi)有別的操作了。
___copy_helper_block:
這個(gè)這里找不到調(diào)用方,所以傳遞的參數(shù)就無(wú)法知道。先嘗試分析一下:看它使用了x0,x1倆寄存器,應(yīng)該有倆參數(shù)。所以順序分析可能就不太好使了,但我們發(fā)現(xiàn)這里就只調(diào)用了_objc_storeStrong,這個(gè)函數(shù)就比較熟悉了,第一個(gè)參數(shù)是id *,第二個(gè)參數(shù)是id,那我們就倒著分析。x0=x9,x9=x9+32,x9=x1,x1=(x29-8)=x0,所以x0=x0+32,再看x1=(sp+8)=x0=[x0+32]=(sp+16)=x1,所以x1=[x1+32](其中小括號(hào)是內(nèi)存暫存,方括號(hào)是加載該內(nèi)存地址的數(shù)據(jù))。見到“+32”偏移量就熟悉了,這就是之前a的地址,整個(gè)函數(shù)的功能就是retain并且store一下block中捕獲的變量a,如果有多個(gè)引用將會(huì)有多次這種操作,但不適用于基本數(shù)據(jù)類型。
理論分析確實(shí)很麻煩,但這里提供另外一種辦法就是運(yùn)行下斷點(diǎn)breakpoint set -n __copy_helper_block_,打印x0,x1
可以看到在_Block_copy中調(diào)用這個(gè)函數(shù),打印一下
(lldb) po $x0
<__NSStackBlock__: 0x1c0250680>
(lldb) po $x1
<__NSStackBlock__: 0x16afeb8f8>
這里也可以通過(guò)直接瀏覽的方式

在_Block_copy調(diào)用時(shí)其還是在棧block,不同的是雖然x0(目標(biāo)地址)的isa還是指向StackBlock,但實(shí)際內(nèi)容已經(jīng)是MallocBlock,比如x0已經(jīng)產(chǎn)生引用計(jì)數(shù)了。(我研究了一下_Block_copy匯編代碼,其會(huì)malloc一段新的內(nèi)存,將數(shù)據(jù)填充過(guò)去,同時(shí)修改Flags的值,Reserved字段全被賦值為了0,x0是新地址,x1是舊地址,然后跳轉(zhuǎn)__copy_helper_block_,做OC參數(shù)的retain操作)
___destroy_helper_block:
這個(gè)調(diào)用_objc_storeStrong,x1=x8=0,很明顯是在做release。
總結(jié)一下:
- 每一個(gè)block背后都有一個(gè)struct做數(shù)據(jù)支撐,與一般的對(duì)象的結(jié)構(gòu)組織和行為模式基本一致。一般的對(duì)象是一份數(shù)據(jù)結(jié)構(gòu)可以對(duì)應(yīng)多個(gè)方法,而block卻是一個(gè)方法對(duì)應(yīng)多個(gè)數(shù)據(jù),導(dǎo)致其占用資源較多。
- Block的OC類型參數(shù)捕獲時(shí),如果只是棧Block,則直接插入retain和release解決對(duì)象引用的問(wèn)題。如果Block對(duì)象被拷貝到堆上,則需要通過(guò)調(diào)用
_Block_copy通過(guò)對(duì)應(yīng)的的___copy_helper_block和___destroy_helper_block函數(shù)來(lái)支撐捕獲對(duì)象的生命周期管理。 - __main_block_desc_0還會(huì)同時(shí)保存的方法簽名(這里是v8@?0),還有block的大小,捕獲參數(shù)個(gè)數(shù)會(huì)造成這個(gè)大小的改變。
最后說(shuō)一下Block零碎的東西
-
在Block_private.h文件中發(fā)現(xiàn),除了我們熟知的三種block意外還有三種運(yùn)行時(shí)的block
BLOCK_EXPORT void * _NSConcreteAutoBlock[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);目前還不知道具體用途。
-
BLOCK_EXPORT size_t Block_size(void *aBlock); BLOCK_EXPORT const char * _Block_signature(void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); BLOCK_EXPORT const char * _Block_layout(void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);這三個(gè)函數(shù)我比較感興趣,但卻是在Block_private.h中,但不礙事,知道名字了,就好辦了。
我們知道.h頭文件的一個(gè)重要作用就是編譯指示,啥意思呢?簡(jiǎn)單來(lái)說(shuō)就是告訴編譯器當(dāng)前環(huán)境的其他編譯模塊有某些符號(hào),到時(shí)候編譯器自己去尋找并鏈接。所以我們只需要在使用前做以下聲明就行
extern const char *_Block_signature(id block); extern size_t Block_size(id block); extern const char* _Block_layout(id ablock);我調(diào)用了一下,發(fā)現(xiàn)_Block_layout輸出是個(gè)null,不清楚有啥作用。
Block_size調(diào)用結(jié)果是40,因?yàn)椴东@了一個(gè)NSString*(8byte)+ Block基礎(chǔ)的32byte,正好。
_Block_signature輸出v20@?0@"NSString"8i16,我的Block原型是typedef void (^ABlock)(NSString *s, int i);其中v是void,20是總共需要內(nèi)存大小,@?是Block的encode,0是第一個(gè)默認(rèn)參數(shù)block結(jié)構(gòu)體從偏移量0開始,@"NSString"8,則指NSString從偏移量8開始,最后是i從偏移量16開始占4位。
有了這么詳細(xì)的簽名,動(dòng)態(tài)調(diào)用或者動(dòng)態(tài)替換實(shí)現(xiàn)就方便了,也許用它還能搞一波事情。
為什么要花這么多時(shí)間去分析匯編,了解的那么詳細(xì)。其實(shí)一般的情況下是用不上的,但是如果遇到了線上crash,棘手的內(nèi)存問(wèn)題,要debug,這時(shí)候這些知識(shí)就會(huì)很有用了,你了解的越多,你解決問(wèn)題的方式就越多,就更容易解決問(wèn)題。當(dāng)然也不是說(shuō)什么都需要去仔細(xì)了解,也不是了解的越多越好,這個(gè)就需要根據(jù)自己的興趣和需求去決定了。但是對(duì)于基礎(chǔ)知識(shí),確實(shí)可以多時(shí)間和精力去完善之,有了這些才能高屋建瓴得心應(yīng)手。
感謝閱讀。