Block詳細解讀

眾所周知,block可以封裝一個匿名函數(shù)為對象乏悄,并捕獲上下文所需的數(shù)據(jù)氮凝,并傳給目標(biāo)對象在適當(dāng)?shù)臅r候回調(diào)。正因為將抽象的函數(shù)具體化為一個可以存儲管理的對象萝映,block可以很容易被建立吴叶,管理,回調(diào)序臂,銷毀蚌卤,也能很好的管理其執(zhí)行所需要的數(shù)據(jù),再加上即用即走和對代碼邏輯上下文完整等優(yōu)點奥秆,被大多數(shù)開發(fā)者廣泛使用逊彭。雖然使用者很多,但還是有不少人對其實現(xiàn)和編譯器背后如何支持還有一些疑惑构订,通過閱讀本文相信你對block將會有一個比較清晰的認知侮叮。在解決一些棘手的內(nèi)存問題的時候?qū)拥眯膽?yīng)手。

block的本質(zhì)

首先寫一個簡單的block

int main(int argc, char * argv[]) {
    void(^blockA)(void) = ^{};
    blockA();
    return 0;
}

使用簡單的clang main.m -rewrite-objc得到C++的main.cpp文件
關(guān)注我們感興趣的部分悼瘾,還是做個注釋吧(64bit)签赃,熟悉這些偏移量比較重要,對分析問題很有幫助分尸,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很可能是個OC的類
  int Flags;//4byte,包含的引用個數(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的描述怎抛,輔助工具
  //如果有捕獲外部變量的話會定義在這里
  ...
  __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ù)芽淡,具體的代碼實現(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;
}

我們可以拷貝這些代碼來運行马绝,但有幾點需要注意的不要引入OC頭文件,否則_NSConcreteStackBlock會重復(fù)定義挣菲,我們只需要定義同樣的全局的變量來替代它或者刪掉就可以了富稻;main中第一句代碼會報錯taking the address of a temporary object of type '__main_block_impl_0', 這是因為這里調(diào)用了構(gòu)造函數(shù) _main_block_impl_0白胀,這會生成一個臨時返回值椭赋,在c++ 11語法里面這個返回值是個右值引用,是無法進行取地址運算的或杠。所以這里改寫一下就可以運行了哪怔,代碼運行起來其調(diào)用過程就比較好辦了,這里就不具體細說了向抢。

    __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));
       

注意:我這里改寫的這個代碼是存在一些問題的认境,因為block_struct是棧上的,所以一旦賦值給強引用時會copy一份放到堆上(GlobalBlock除外)挟鸠,調(diào)用block的時候可能已經(jīng)超出block_struct的生命周期了元暴。

接著看代碼

在代碼中我們看到了OC類的標(biāo)志isa指針,并且指向_NSConcreteStackBlock兄猩,但具體是不是那么回事還是需要證明一下茉盏。畢竟編譯和運行時還是有些不一樣。

在bockA();這句加斷點枢冤;在編譯器debug窗口左側(cè)有當(dāng)前調(diào)用棧幀可見變量鸠姨,找到blockA,發(fā)現(xiàn) __isa=__NSGlobalBlock__和上面改寫代碼中的impl.isa = &_NSConcreteStackBlock還是有出入的淹真,我們選擇相信運行時讶迁。

選中__isa右鍵菜單View Memery of "__isa"可以瀏覽當(dāng)前內(nèi)存的值,我這里isa指向的地址是0x1003b8048核蘸,然后可以看到這里值是70 94 59 0b 01 00 00 00(8byte)巍糯,小端機器,實際的數(shù)據(jù)是010B599470客扎,觀察代碼void *_NSConcreteGlobalBlock[32]發(fā)現(xiàn)這是一個地址祟峦,對應(yīng)的地址就是0x10B599470,管它是不是OC對象徙鱼,我們在lldb下po 0x10B599470輸出一下是__NSGlobalBlock__宅楞,呵针姿,可能有譜,再追蹤這個地址

屏幕快照 2018-05-03 18.16.44.png

可以看到以下內(nèi)存數(shù)據(jù)前8個byte是010B5994F0厌衙,順便輸出一下也打印了__NSGlobalBlock__距淫,再看發(fā)現(xiàn)這個地址就在附近,里面記錄的第一個數(shù)據(jù)是010780DE58婶希,我們知道OC對象第一個數(shù)據(jù)就是isa指針榕暇,將其構(gòu)建成地址0x10780de58,輸出一下喻杈,打印了NSObject彤枢,這里可以理出關(guān)系blockA.isa->__NSGlobalBlock__.isa->NSObject,也就是說__NSGlobalBlock__的元類是NSObject奕塑,這基本可以證明__NSGlobalBlock__應(yīng)該是個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只有一個isa_t的數(shù)據(jù)

union isa_t {
    Class cls;
    uintptr_t bits;//是個unsigned long
    ...//帶位域的struct,這里不關(guān)注
}

由此可見objc_class的前8byte是isa指針讨衣,第二個8byte是superclass指針换棚。

我這里一次是0x010a6112a0(__NSGlobalBlock)0x10a611110(NSBlock)反镇,0x10780dea8(NSObject)

對照上面的NSObject固蚤,其地址0x10780de58,有點蒙歹茶,到底哪個對夕玩?其實都算對。

這里我定義了一個NSObject的對象惊豺,利用其運行時isa和superclass的數(shù)據(jù)燎孟,做了一個兩者的關(guān)系圖。

屏幕快照 2018-05-03 18.28.27.png

還記得這個繼承體系圖尸昧,和上面結(jié)果一致揩页。

isa_superclass.jpg

從相關(guān)資料可以了解,OC中除了NSProxy外烹俗,其他的類都是NSObject的子類爆侣,包括元類,這個NSObject就是下圖中的0x10780dea8幢妄。

OK兔仰,至此證明block是個OC對象,其繼承自NSBlock蕉鸳,NSObject斋陪。

上面的環(huán)也可以解釋一些經(jīng)典的問題,比如(寫本文的時候查資料時剛好遇到,就貼上來了):

    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不存在這個環(huán)无虚,也就不會出現(xiàn)這個現(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

之所以費力的證明Block是個OC對象缔赠,是因為這可以更好的認知Block,得到很多的信息和用法友题。我們或許可以像用普通的OC對象一樣使用Block嗤堰,可以方便得被ARC管理,不用擔(dān)心內(nèi)存泄露或者非法訪問度宦。weak踢匣,autorelease等等也都可以使用,還可以放在集合里面戈抄,可以被別的對象持有离唬,當(dāng)然也可以持有別的對象,了解到這一點對于我們分析block的相關(guān)的內(nèi)存管理和循環(huán)引用意義重大划鸽。

但在重寫的C++的代碼中我們看不到編譯器幫我們插入的release输莺,retain這樣的代碼,所以我們不得不用別的辦法來了解Block具體是否被ARC管理的裸诽。

Block本身的內(nèi)存管理

首先要明確一件事:Block本身內(nèi)存的管理和Block捕獲的對象的內(nèi)存管理是兩個問題嫂用。這里我們先討論前者。

前面遺留了一個問題就是丈冬,代碼里面isa指針明明指向了_NSConcreteStackBlock嘱函,怎么到了運行時的時候就變成了__NSGlobalBlock__

我們再做一個實驗埂蕊,將代碼改為

void afunc() {
    __unsafe_unretained void(^blockA)(void) = ^{};
    blockA();
}

int main(int argc, char * argv[]) {
    afunc();
    return 0;
}

在blockA()處下個斷點往弓,查看debug數(shù)據(jù),發(fā)現(xiàn)isa指針確實指向的是__NSGlobalBlock__蓄氧,也就是說在這之前就被更新了函似,目前我還沒有找到這個更新時機。

我們發(fā)現(xiàn)調(diào)用blockA()可以成功匀们,沒有crash缴淋,我嘗試取了一下retainCount發(fā)現(xiàn)是1,去掉__unsafe_unretained也一樣泄朴。

注意:通過kvc可以獲取對象的引用計數(shù)重抖,如果一個函數(shù)來打印對象的引用計數(shù),這函數(shù)的參數(shù)聲明是有講究的

void printRetainCount(__unsafe_unretained id o) {
    void *p = (__bridge void *)o;
    NSLog(@"%p:%d",p,[o valueForKey:@"retainCount"]);
}

參數(shù)需要用__unsafe_unretained來修飾祖灰,最好不要用強引用钟沛,這會導(dǎo)致引用計數(shù)器+1,更不能用weak局扶,這會導(dǎo)致每次使用weak對象的時候恨统,retainCount都會增加叁扫,這個坑一不小心就會忽略,導(dǎo)致獲取的數(shù)據(jù)可能不準(zhǔn)確畜埋,關(guān)于這個問題具體情況以后有機會再討論莫绣。

修改代碼增加全局__weak void(^blockB)(void) = nil;,并在afunc()對其賦值悠鞍,在main()中調(diào)用blockB()对室,發(fā)現(xiàn)也可以調(diào)用成功,并沒有crash咖祭。通過__NSGlobalBlock__這個名字大概可以猜測出這個是一個全局的block掩宜,其生命周期全局有效,即使主動調(diào)用copy么翰,也不會入堆牺汤,似乎不受ARC控制。對照源碼可知浩嫌,globalblock其實并不依賴外部數(shù)據(jù)檐迟,只要有代碼入口就可以使用,甚至不需要知道block固该,只有有函數(shù)入口地址就可以直接調(diào)用锅减,而另外兩種都需要通過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()處下個斷點,查看debug數(shù)據(jù)握联,發(fā)現(xiàn)isa指針指向的是__NSStackBlock__桦沉,去掉 __unsafe_unretained后isa指針變成了__NSMallocBlock__

我們發(fā)現(xiàn)調(diào)用blockA()可以成功金闽,沒有crash纯露,我嘗試取了一下retainCount發(fā)現(xiàn)是1,去掉__unsafe_unretained也一樣代芜,??埠褪,跟一般的OC對象不太一樣?

再次修改代碼和上面一下增加__weak void(^blockB)(void) = nil挤庇,在main中調(diào)用blockB()钞速,發(fā)現(xiàn)其crash了,證明blockB已經(jīng)無效了嫡秕。再次將__unsafe_unretained修改為__autoreleasing,發(fā)現(xiàn)其可以調(diào)用成功,所以證明block此時被autoreleasepool接管了间驮,看上去ARC還是有作用的。

那么在ARC下牙甫,如果增加一個強引用指向block會不會導(dǎo)致retainCount增加呢?通過實驗發(fā)現(xiàn)不會调违,依舊是1窟哺,這一點又和普通的對象不太一樣。

難道這就是真相技肩,no脏答,這無法解釋之前觀察到的各種現(xià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)

仔細一看雳锋,發(fā)現(xiàn)其打印的地址和__FuncPtr地址一樣黄绩,那么同理取的block的retainCount也就可有能不正確,去objc源碼中搜索了一下發(fā)現(xiàn)其實現(xiàn)為

-(NSUInteger)retainCount {
    return (_rc_ivar + 2) >> 1;                                             
}     

也就是說玷过,只要對象沒有被釋放爽丹,那么其retainCount至少是1。換句話說辛蚊,如果某個對象沒有_rc_ivar粤蝎,或者_rc_ivar=0,那么其結(jié)果都是1袋马,所以這里通過KVC取retainCount在block這里并不可靠初澎,因為ARC機制下并不允許訪問retainCount,所以其可靠性在有些情況還是會受到質(zhì)疑的虑凛,不足以作為判斷標(biāo)準(zhǔn)碑宴。但是我們發(fā)現(xiàn)一個問題,就是分配在棧上的block出了作用域已經(jīng)無效了桑谍,那也就是說block應(yīng)該在一定程度上受到ARC機制的約束延柠,這需要進一步求證。

還記isa_a定義么锣披,接下來我們?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指針存儲數(shù)據(jù)的真正空間,后者是存放對象額外引用計數(shù)的雹仿,如果這里19 bits還不夠的話增热,就使用sidetable來記錄。也就是說盅粪,絕大部分情況下钓葫,引用計數(shù)器是存在對象的isa里面的,所以我們只需要去查看isa的內(nèi)存值票顾,解析最后19bits的值就可以得到引用計數(shù)础浮。

打個斷點帆调,在這里選擇debug窗口左側(cè)的variables view窗口,選中某個block指針豆同,右鍵 view memery of "*blockA"番刊,可能不能瀏覽,所以view memery of "blockA"影锈,我這里是內(nèi)存地址0x16ee9f8f8存儲了以下數(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

看到最后一個8個byte枣抱,有點眼熟,就是之前打印的__FuncPtr = 0x0000000100f66570辆床。那前倆byte存的是啥呢佳晶?第一個byte明顯是一個指針,打印一下讼载,就是__NSMallocBlock__轿秧,那剩下的8byte呢?從block的數(shù)據(jù)結(jié)構(gòu)了解到其對應(yīng)的就是

  int Flags;//4byte咨堤,包含的引用個數(shù)
  int Reserved;//4byte

后者是0菇篡,前者是有值而且會變化,我嘗試再給block一個強引用一喘,發(fā)現(xiàn)02 00 00 C3變成了04 00 00 C3驱还,再賦值就變成了06 00 00 C3,所以這個06應(yīng)該就是引用計數(shù)器津滞,而且也符合retainCount的運算邏輯铝侵,從內(nèi)存布局上看灼伤,19bits的存儲位置應(yīng)該在一個8-byte的末尾触徐,也就是包含02這段空間,但只是不太了解為啥isa被分成了兩個64bits存儲狐赡。

同理我嘗試了僅僅在stack上的block撞鹉,其數(shù)據(jù)位00 00 00 C2,計數(shù)器為0颖侄。

同理我嘗試了global的block數(shù)據(jù)位00 00 00 50鸟雏,計數(shù)器為0。

結(jié)果符合預(yù)期览祖,除了進入堆上block會受ARC約束孝鹊,其他的block都不需要ARC參與就可以完成內(nèi)存管理。

小結(jié)
  1. Block如果不捕獲外界變量展蒂,就沒有上下文依賴又活,編譯器會將其標(biāo)記為global類型(當(dāng)然可能編譯器標(biāo)記為stack苔咪,運行時優(yōu)化glabal常量);否則編譯器會在創(chuàng)建時將其標(biāo)記為stack柳骄,當(dāng)運行時對象被強引用時或者主動調(diào)用copy會被標(biāo)記為malloc類型团赏。
  2. global和stack的block都不需要ARC參與內(nèi)存管理。malloc的block將受到ARC管理耐薯,包括autorelease和weak舔清。

Block參數(shù)傳遞

前面的小節(jié)研究了Block的本質(zhì)和其本身的內(nèi)存管理,我們幾乎可以把他當(dāng)做普通對象來使用曲初,同時其擁有唯一的成員函數(shù)体谒,其執(zhí)行所要依賴的數(shù)據(jù)來源有兩個,一個是當(dāng)前上下文環(huán)境的各種變量臼婆,另外就是調(diào)用方的傳參营密。block傳參和函數(shù)傳參并沒有什么不同,這里就不做具體討論目锭。

Block如何捕獲外界變量

之前為了重寫簡單我并沒有引入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;
}

對于這類引用了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庫(我使用的是iPhoneSimulator.sdk),否則會出現(xiàn)“UIKit/UIKit.h” file not found奖唯,還需要指定-fobjc-arc開啟ARC的編譯開關(guān)和-fobjc-runtime=macosx-10.10惨缆,否則會出現(xiàn)“cannot create __weak reference in file using manual reference counting”類似的錯誤。

編譯真機的話需要指定支持的CPU架構(gòu)和庫等(折騰了挺久才試出這些參數(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

編譯器會根據(jù)捕獲的原始變量的不同情況坯墨,定義不同類型的變量來存儲這些數(shù)據(jù)。

根據(jù)變量定義類型病往,這里我分成以下幾類:

  1. 以下是捕獲一個基本類型臨時變量i的c++代碼
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //如果有捕獲外部變量的話會定義在這里
  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;
  }
};

可以看到捣染,編譯器定義了一個int i來對應(yīng)外界的int i,同時接收了外界i的值來初始化停巷。

  1. 同理如果是局部指針這里將定義為對應(yīng)的指針耍攘,例如這里是 int *ip。
  2. 如果需要捕獲的是一個局部OC對象畔勤,其實和2中情況一致蕾各,不同之處在于ARC會介這個對象的管理。
  3. 對于全局變量庆揪,因為訪問是開放的式曲,所以編譯器不需要做處理,直接使用該變量就行缸榛。

根據(jù)變量定義的附加修飾特性:

  1. 對于局部static變量吝羞,因為訪問不開放始鱼,所以會被編譯器升級為指針,例如:static int staticInt = 100脆贵,會定義一個int *staticInt來捕獲staticInt的地址医清,以便以后修改。
  2. 對于__weak修飾的對象引用卖氨,這個重點說明会烙。
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;
  }
};

可以看到對于__weak修飾的引用,編譯器也在Block中定義一個一模一樣的引用筒捺,然后通過構(gòu)造函數(shù)通過參數(shù)傳入初始化結(jié)構(gòu)體(c++中struct和class絕大部分情況是等效的)柏腻,這是意味著什么呢?我們知道所有函數(shù)參數(shù)傳遞就一種方式——copy系吭,這里的參數(shù)捕獲間接套用了該性質(zhì)五嫂。一句話來說,對象還是那個對象肯尺,但引用已經(jīng)不是那個引用了沃缘。

屏幕快照 2018-05-14 17.21.39.png

這里我畫了一個簡圖,實際上每個weakSelf都是不一樣的则吟,只是其指示的內(nèi)容是同樣的槐臀。

  1. __block修飾的對象,這里改寫為c++代碼出錯氓仲,我沒能解決這個問題水慨。所以就只有推測了,其做法應(yīng)該和局部的static變量捕獲差不多敬扛,都會定義一個同類型的指針或者引用晰洒,以便可以在block中訪問該變量修改變量。
小結(jié)
  1. 參數(shù)捕獲和參數(shù)傳遞啥箭,前者發(fā)生在block定義初始化的時候谍珊,是對當(dāng)前現(xiàn)場的一種保存,后者發(fā)生在調(diào)用的時候傳參捉蚤,其存儲上的區(qū)別是前者是成員變量持續(xù)存儲抬驴,后者是臨時變量。相同之處就是獲取方式完全一致缆巧,都是函數(shù)參數(shù)傳遞。
  2. 編譯器會對待不同類型的參數(shù)捕獲處理方式都一樣豌拙,全部淺拷貝陕悬;對于不同修飾參數(shù)則不太一樣,會根據(jù)不同的情況來決定是否升級為指針捕獲按傅;OC對象將會引入ARC機制去管理捉超。

Block循環(huán)引用及解決辦法

如果能明確認識到block就是個對象胧卤,那么造成循環(huán)引用的原因就不難理解了,block可以持有對象也可以被對象持有拼岳,如果兩者直接或者間接包含同一對象時就成了環(huán)枝誊,實際上就是object->block(->…)->object。

那么為什么用weak strong dance就可以解決這個問題呢惜纸?看下面這個典型例子叶撒。

__weak typeof(self) weakSelf = self;
void (^block)(void) = ^{
  __strong typeof(weakSelf) strongSelf = weakSelf;  
};

通過前面的C++的代碼分析,答案已經(jīng)很清晰了耐版,這里就再解釋一次:

我們知道block外部定義了一個weakSelf(為了方便說明祠够,可以認為是weakSelf1),而在block內(nèi)部并沒有直接使用這個weakSelf1(就是沒有使用這個weakSelf1這個硬編碼或者說其對應(yīng)的地址)粪牲,而是另外定義了一個對應(yīng)的構(gòu)造函數(shù)參數(shù)__weak weakSelf(weakSelf2)古瓤,通過指針copy傳參的方式,weakSelf2指向了weakSelf1指向的內(nèi)容腺阳,同時block內(nèi)部的成員變量 __weak weakSelf(weakSelf3)通過weakSelf賦值也指向了weakSelf1指向的內(nèi)容落君。所以從始至終這些weakSelf都不是同一個東西。至于strongSelf就簡單了亭引,對象賦值給強引用會導(dǎo)致retainCount+1叽奥,還記我之前文章里面的觀點么,ARC是用棧管理引用痛侍,用引用生命周期管理對象朝氓,所有strongSelf生命周期結(jié)束,自然retainCount-1主届。
所以在block還沒有執(zhí)行的時候赵哲,self的生命周期不受block影響,如果執(zhí)行的時候self已經(jīng)被釋放君丁, weakSelf3=nil枫夺,也不會導(dǎo)致問題,但是如果weakSelf3還有值绘闷,strongSelf就會導(dǎo)致retainCount+1橡庞。有很多人認為,無論如何必須等到block執(zhí)行完或者銷毀self才會釋放是不正確的印蔗。仔細對照block和delegate就會發(fā)現(xiàn)兩者在這方面其實本質(zhì)是一樣的的扒最,如果delegate不使用weak也一樣可能循環(huán)引用。還是那句話华嘹,內(nèi)存中通信就一個招吧趣,拿到地址,所以無論是直接的delegate,block强挫,target-action岔霸,還是間接的Notification,或者其他的玩法都一樣俯渤。

注意:__strong不能省略呆细。

當(dāng)然并不是說見到block就需要weak strong dance,對于以下情況就可以不使用(從調(diào)用方和回調(diào)方分析)

  1. 如果能確定block的調(diào)用方并不會長期持有block八匠,比如傳給B一個A持有的block絮爷,B并不存儲,而是立刻回調(diào)臀叙,常見的就是把block當(dāng)函數(shù)參數(shù)傳遞略水。
  2. 如果確定block調(diào)用方會在必要的時候去除強引用,比如:dispatch_async劝萤,其雖然會被隊列強引用渊涝,但在block回調(diào)的時候,_dispatch_call_block_and_release會在執(zhí)行完release床嫌,這也不會導(dǎo)致循環(huán)引用跨释。
  3. block創(chuàng)建方不會直接或間接強引用block。
  4. 對于絕不可能持有block的對象厌处,可以放心捕獲鳖谈,比如NSString,NSDate阔涉,NSURL等等缆娃,但對于一些可能存儲block需要小心,比如:NSArray瑰排,NSDictionary贯要,自定義的對象(self)。

如果你是創(chuàng)建方椭住,不想去分析也不知道調(diào)用方干了什么崇渗,建議就無腦weak strong dance,幾乎可以就可以解決問題了京郑。如果你是調(diào)用方宅广,會麻煩一些,需要具體問題具體分析些举。

Block捕獲對象的內(nèi)存管理

這分成三個方面跟狱,如果只是基本類型,那就不需要操心金拒;如果是C指針兽肤,那指向?qū)ο蟮纳芷谛枰_發(fā)者手動管理套腹;如果是個OC對象绪抛,內(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();    
}

此時輸出日志如下:

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)用時被strongSelf持有+1症副,weakSelf并沒導(dǎo)致引用計數(shù)器增加店雅,與輸出日志相符。

進入afunc:時贞铣,string引用計數(shù)為2闹啦,一個發(fā)生在main,一個發(fā)生在afunc:的參數(shù)中聲明中辕坝。調(diào)用b()時窍奋,retainCount=4,這是因為這是存在一個StackBlock和一個MallocBlock酱畅,這兩個都會有一個引用指向string琳袄。在afunc:函數(shù)末尾打印string是4也是同樣的原因;當(dāng)afunc:執(zhí)行完后纺酸,StackBlock已經(jīng)釋放窖逗,返回block給main中的a,此時調(diào)用a()餐蔬,輸出2碎紊,其中引用來自于MallocBlock,符合預(yù)期樊诺。

c=b這句賦值仗考,只引起block計數(shù)器增加,而不會導(dǎo)致捕獲OC對象引用計數(shù)器增加啄骇,符合預(yù)期痴鳄。

我們在lldb設(shè)置倆符號斷點:

breakpoint set -n _Block_copy
breakpoint set -n _Block_release

可以發(fā)現(xiàn)_Block_copy和普通對象retain時機調(diào)用類似。

需要注意的問題

  1. block捕獲變量防止循環(huán)引用容易漏掉一些情況缸夹,在捕獲時需要多注意痪寻,舉個例子,直接捕獲成員變量虽惭。

    假設(shè)在一個對象方法里面橡类,比如ViewController
    void (^block)(void) = ^{
        //這里是等效于self->_name,編譯器編碼為self+offset(_name)芽唇,依然會導(dǎo)致強引用
         NSString *name = _name; 
    };
    
  1. 直接修改捕獲的變量不能成功顾画,因為里外的兩個array不是一個array取劫,需要加上__block,變量捕獲通過函數(shù)傳參的方式實現(xiàn)研侣,而傳參全是copy谱邪。

    NSArray *array = @[@1,@2];
    void (^block)(void) = ^{
        array = @[];
    };
    

    ?

附加內(nèi)容

之前從宏觀層面了解了block和其捕獲對象的生命周期,但具體是怎樣還是不太清晰庶诡,有興趣的話可以看下面一段內(nèi)容惦银,具體了解block是怎么玩的,匯編較長末誓,看起來也比較繞扯俱,沒興趣的話可以忽略,看看其中的一些要點喇澡。
源碼:

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是捕獲的第一個變量位置,就是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]  //對象a
    str x8, [x1, #32]
    str x0, [sp, #8]            ; 8-byte Folded Spill
    mov  x0, x9
    ldr x1, [sp, #8] //對象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:

  1. bl _afunc榨惰,沒有參數(shù)直接跳轉(zhuǎn)拜英,從源碼可知返回了一個block對象在x0中,bl _objc_retainAutoreleasedReturnValue表明其被autoreleasepool管理琅催。
  2. 接下將x0存在了sp+24這里居凶,再下一句沒有啥意義。
  3. 將x0賦值給x1藤抡,挪出空間侠碧,加載x0+16的值到x0,找到最開始struct __block_impl的內(nèi)存布局缠黍,發(fā)現(xiàn)這個地址存放的是回調(diào)函數(shù)的指針弄兜。
  4. 接下來通過[sp, #16]中轉(zhuǎn),將x1和x0內(nèi)容互換瓷式,至此x0是block首地址替饿,x1是回調(diào)函數(shù)地址;blr x1跳轉(zhuǎn)到x1贸典;2视卢,3,4步加一起就是源碼里面的aBlock()廊驼。
  5. 最后那段就是在release之前的block對象据过。

_afunc:

最前面棧增長了112個byte惋砂,這里局部變量較多所以,棧分配的較大绳锅。

  1. 直接看Ltmp0:西饵,前面一個_objc_retain是引用了字符串"demo"

. stur x0, [x29, #-8],將x0("demo")存在了x29-8這個位置榨呆,也就是sp+88的位置罗标。

  1. 然后x0=sp+40庸队,x30=sp+72

  2. 接下來兩句加載"__NSConcreteStackBlock"符號對應(yīng)的地址积蜻,然后將其存在sp+40這個地址,而x0目前是指向這個地址的彻消。

    struct __block_impl {
      void *isa; //8byte竿拆,isa指針,很重要的標(biāo)志宾尚,意味著block很可能是個OC的類
      int Flags;//4byte丙笋,包含的引用個數(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的描述
      //如果有捕獲外部變量的話會定義在這里
      id a;
    };
    

    這里貼一個內(nèi)存布局御板,sp+40就是__main_block_impl_0首地址,也是isa地址牛郑,這里就是“StackBlock”類似符號怠肋。

  3. 接下來就簡單了,依次存儲了Flags(sp+48)淹朋,Reserved(sp+52)各4個byte笙各。

  4. 存儲FuncPtr指針,就是匯編符號___afunc_block_invoke對應(yīng)的地址础芍,sp+56的8個byte杈抢。arm64指針占8個byte。

  5. 存儲Desc仑性,這也是個指針惶楼,存儲的是___block_descriptor_tmp對應(yīng)的地址,其記錄了___copy_helper_block诊杆,___destory_helper_block和method signature歼捐,block size等信息,對應(yīng)sp+64的8個byte刽辙。

  6. 接下來加載x29-8的內(nèi)容到x8就是"demo"字符串窥岩。然后將x0暫存在sp+32,同時將x0=x8宰缤,然后存儲x30到sp+24

  7. 調(diào)用_objc_retain颂翼,參數(shù)x0晃洒,所以結(jié)果是retain了“demo”一次。之后將x0存儲在了sp+72這里朦乏,就是struct __main_block_impl_0 中的id a球及,id a是我隨意寫的,但實際定義也應(yīng)該差不多的呻疹。需要注意的是吃引,這里的調(diào)用是因為創(chuàng)建了一個StackBlock,其也是要使用"demo"這個數(shù)據(jù)的刽锤,所以retain了一次镊尺。但MallocBlock的引用計數(shù)則由___copy_helper_block來管理。)

  8. 然后將sp+32的內(nèi)容加載到x0并思,也就是sp+40即__main_block_impl_0的首地址庐氮。

  9. 調(diào)用_objc_retainBlock,去蘋果源碼中找一下:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

其調(diào)用了_Block_copy宋彼,這個函數(shù)在Block.h中聲明的弄砍,我沒有找到相關(guān)的實現(xiàn)源碼,不過可以證明的是對于block來說retain和copy效果一致输涕。

  1. 再次調(diào)用了_objc_retainBlock

    這里的兩次調(diào)用一次是賦值給blockA音婶,一次是return blockA造成的。

  2. 后面有三個_objc_storeStrong莱坎,x1=0衣式,都是在做release操作,具體過程比較繁瑣型奥,我就直接給結(jié)論了瞳收,其中第一個release一次blockA,第二release一次“demo”字符串(提示:sp+24存的是x30厢汹,x30=sp+40+32螟深,這里存的是“demo”),第三個也是release一次"demo"(sub x0, x29, #8烫葬,提示一下界弧,x29=sp+112-96,所以這句也是x0=sp+24)

    通過這里的分析搭综,對于Block捕獲對象垢箕,ARC怎么作用的,相應(yīng)的數(shù)據(jù)結(jié)構(gòu):block retain會被copy兑巾,捕獲OC類型參數(shù)的時候會retain參數(shù)条获,參數(shù)的傳遞方式——拷貝。

___afunc_block_invoke:

  1. ldr x8, [x0, #32]蒋歌,x0是block的首地址帅掘,x0+32就是變量a的地址委煤,mov x0,x8,bl _objc_retain修档,retain了a這個對象碧绞,和源碼功能一致。
  2. 后面就是在release這個對象吱窝,源碼里面也確實沒有別的操作了讥邻。

___copy_helper_block:

這個這里找不到調(diào)用方,所以傳遞的參數(shù)就無法知道院峡。先嘗試分析一下:看它使用了x0,x1倆寄存器兴使,應(yīng)該有倆參數(shù)。所以順序分析可能就不太好使了撕予,但我們發(fā)現(xiàn)這里就只調(diào)用了_objc_storeStrong鲫惶,這個函數(shù)就比較熟悉了,第一個參數(shù)是id *实抡,第二個參數(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](其中小括號是內(nèi)存暫存睛榄,方括號是加載該內(nèi)存地址的數(shù)據(jù))。見到“+32”偏移量就熟悉了场靴,這就是之前a的地址,整個函數(shù)的功能就是retain并且store一下block中捕獲的變量a咧欣,如果有多個引用將會有多次這種操作,但不適用于基本數(shù)據(jù)類型轨帜。

理論分析確實很麻煩魄咕,但這里提供另外一種辦法就是運行下斷點breakpoint set -n __copy_helper_block_窿撬,打印x0店枣,x1

可以看到在_Block_copy中調(diào)用這個函數(shù)来颤,打印一下

(lldb) po $x0
<__NSStackBlock__: 0x1c0250680>

(lldb) po $x1
<__NSStackBlock__: 0x16afeb8f8>

這里也可以通過直接瀏覽的方式

view_reg.png

_Block_copy調(diào)用時其還是在棧block囤躁,不同的是雖然x0(目標(biāo)地址)的isa還是指向StackBlock狸演,但實際內(nèi)容已經(jīng)是MallocBlock宵距,比如x0已經(jīng)產(chǎn)生引用計數(shù)了。(我研究了一下_Block_copy匯編代碼婿斥,其會malloc一段新的內(nèi)存民宿,將數(shù)據(jù)填充過去像鸡,同時修改Flags的值只估,Reserved字段全被賦值為了0蛔钙,x0是新地址夸楣,x1是舊地址豫喧,然后跳轉(zhuǎn)__copy_helper_block_,做OC參數(shù)的retain操作)

___destroy_helper_block:

這個調(diào)用_objc_storeStrong讲衫,x1=x8=0涉兽,很明顯是在做release枷畏。

總結(jié)一下:
  1. 每一個block背后都有一個struct做數(shù)據(jù)支撐拥诡,與一般的對象的結(jié)構(gòu)組織和行為模式基本一致。一般的對象是一份數(shù)據(jù)結(jié)構(gòu)可以對應(yīng)多個方法冗懦,而block卻是一個方法對應(yīng)多個數(shù)據(jù)披蕉,導(dǎo)致其占用資源較多没讲。
  2. Block的OC類型參數(shù)捕獲時食零,如果只是棧Block寂屏,則直接插入retain和release解決對象引用的問題迁霎。如果Block對象被拷貝到堆上考廉,則需要通過調(diào)用_Block_copy通過對應(yīng)的的___copy_helper_block___destroy_helper_block函數(shù)來支撐捕獲對象的生命周期管理昌粤。
  3. __main_block_desc_0還會同時保存的方法簽名(這里是v8@?0),還有block的大小涮坐,捕獲參數(shù)個數(shù)會造成這個大小的改變誓军。

最后說一下Block零碎的東西

  1. 在Block_private.h文件中發(fā)現(xiàn)昵时,除了我們熟知的三種block意外還有三種運行時的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);
    

    目前還不知道具體用途。

  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);
    

    這三個函數(shù)我比較感興趣壶熏,但卻是在Block_private.h中棒假,但不礙事淆衷,知道名字了渤弛,就好辦了她肯。

    我們知道.h頭文件的一個重要作用就是編譯指示晴氨,啥意思呢籽前?簡單來說就是告訴編譯器當(dāng)前環(huán)境的其他編譯模塊有某些符號枝哄,到時候編譯器自己去尋找并鏈接。所以我們只需要在使用前做以下聲明就行

    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輸出是個null粱侣,不清楚有啥作用齐婴。

    Block_size調(diào)用結(jié)果是40尔店,因為捕獲了一個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是第一個默認參數(shù)block結(jié)構(gòu)體從偏移量0開始夜惭,@"NSString"8诈茧,則指NSString從偏移量8開始敢会,最后是i從偏移量16開始占4位走触。

    有了這么詳細的簽名疤苹,動態(tài)調(diào)用或者動態(tài)替換實現(xiàn)就方便了卧土,也許用它還能搞一波事情尤莺。

為什么要花這么多時間去分析匯編颤霎,了解的那么詳細友酱。其實一般的情況下是用不上的缔杉,但是如果遇到了線上crash,棘手的內(nèi)存問題系羞,要debug椒振,這時候這些知識就會很有用了澎迎,你了解的越多嗡善,你解決問題的方式就越多罩引,就更容易解決問題袁铐。當(dāng)然也不是說什么都需要去仔細了解剔桨,也不是了解的越多越好洒缀,這個就需要根據(jù)自己的興趣和需求去決定了树绩。但是對于基礎(chǔ)知識饺饭,確實可以多時間和精力去完善之瘫俊,有了這些才能高屋建瓴得心應(yīng)手扛芽。

感謝閱讀胸哥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末空厌,一起剝皮案震驚了整個濱河市嘲更,隨后出現(xiàn)的幾起案子赋朦,更是在濱河造成了極大的恐慌宠哄,老刑警劉巖毛嫉,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暴区,死亡現(xiàn)場離奇詭異仙粱,居然都是意外死亡伐割,警方通過查閱死者的電腦和手機刃唤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辐真,“玉大人侍咱,你說我怎么就攤上這事楔脯∶镣ⅲ” “怎么了木柬?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵眉枕,是天一觀的道長速挑。 經(jīng)常有香客問我姥宝,道長伶授,這世上最難降的妖魔是什么糜烹? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任诸迟,我火速辦了婚禮阵苇,結(jié)果婚禮上绅项,老公的妹妹穿的比我還像新娘快耿。我一直安慰自己掀亥,他們只是感情好搪花,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布吮便。 她就那樣靜靜地躺著线衫,像睡著了一般授账。 火紅的嫁衣襯著肌膚如雪白热。 梳的紋絲不亂的頭發(fā)上屋确,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天攻臀,我揣著相機與錄音刨啸,去河邊找鬼。 笑死离例,一個胖子當(dāng)著我的面吹牛宫蛆,可吹牛的內(nèi)容都是我干的洒扎。 我是一名探鬼主播袍冷,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼煌恢!你這毒婦竟也來了瑰抵?” 一聲冷哼從身側(cè)響起二汛,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎婿着,沒想到半個月后竟宋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丘侠,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年秽澳,在試婚紗的時候發(fā)現(xiàn)自己被綠了担神。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妄讯。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡躬窜,死狀恐怖荣挨,靈堂內(nèi)的尸體忽然破棺而出默垄,到底是詐尸還是另有隱情,我是刑警寧澤甚纲,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布口锭,位于F島的核電站,受9級特大地震影響介杆,放射性物質(zhì)發(fā)生泄漏鹃操。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一这溅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悲靴,春花似錦臭胜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浇揩,卻和暖如春仪壮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背胳徽。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工积锅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人养盗。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓缚陷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親往核。 傳聞我的和親對象是個殘疾皇子箫爷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容