OC底層原理三十:block詳解

OC底層原理 學(xué)習(xí)大綱

本節(jié)將深入探索block底層原理

  1. 循環(huán)引用 & 解決方案
  2. block結(jié)構(gòu)分析
  3. 源碼探索
  4. block的參數(shù)處理(多次拷貝)

1. 循環(huán)引用 & 解決方案

  • 正常釋放:


    image.png
  • 循環(huán)引用:


    image.png
  • 解決循環(huán)引用的方法:
  1. weakSelf弱引用self,搭配strongSelf
  2. 使用__block修飾對(duì)象(必須在block中置空對(duì)象,且block必須被調(diào)用
  3. 對(duì)象self參數(shù),提供給代碼塊使用
  • 測試代碼:
typedef void(^HTBlock)(void);
typedef void(^HTBlock2)(ViewController *);

@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) HTBlock block;
@property (nonatomic, copy) HTBlock2 block2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.name = @"ht";
    
//    // 循環(huán)引用 [self持有block,block持有self]
//    self.block = ^(void){
//        NSLog(@"%@",self.name);
//    };
//    self.block();
//
//    // 沒有循環(huán)引用 (UIView的block持有self,self與UIView的block無關(guān))
//    [UIView animateWithDuration:1 animations:^{
//        NSLog(@"%@",self.name);
//    }];
    
    // 方法1: `weakSelf`弱引用`self`,搭配`strongSelf`
    // weak不會(huì)讓self的引用計(jì)數(shù)+1,所以不影響self的釋放。 而strongSelf持有的是weakSelf。
    // 當(dāng)self釋放時(shí),如果block執(zhí)行完了,strongSelf局部變量就會(huì)被釋放,此時(shí)weakSelf也被釋放。所以不會(huì)造成循環(huán)引用
//    __weak typeof(self) weakSelf = self;
//    self.block = ^{
//        __strong typeof(self) strongSelf = weakSelf;
//        NSLog(@"%@",strongSelf.name);
//    };
//    self.block();
    
//    // 方法2:使用`__block`修飾對(duì)象(`必須`在block中`置空對(duì)象`,且block必須`被調(diào)用`)
//    __block ViewController * vc = self;
//    self.block = ^(void){
//        NSLog(@"%@",vc.name);
//        vc = nil; // 必須手動(dòng)釋放。  因?yàn)関c持有了self,vc不釋放,self永遠(yuǎn)不可能釋放。
//    };
//    self.block(); // 必須調(diào)用block。 因?yàn)関c持有了self,不過不執(zhí)行block,vc永遠(yuǎn)沒有nil,self永遠(yuǎn)不會(huì)釋放
    
    // 方法3:`傳`對(duì)象`self`作`參數(shù)`,提供`給代碼塊使用`
    // 最佳的使用方式,因?yàn)椴粫?huì)影響self的正常釋放。(調(diào)用時(shí),引用計(jì)數(shù)會(huì)+1,但是調(diào)用完后,就會(huì)-1,不影響self的生命周期)
    self.block2 = ^(ViewController * vc) {
        NSLog(@"%@",vc.name);
    };
    self.block2(self);
}
@end

2. block結(jié)構(gòu)分析

2.1 block的結(jié)構(gòu)

  • main.m文件中添加測試代碼:
#import <stdio.h>

int main(int argc, const char * argv[]) {
    void(^block)(void) = ^{
        printf("HT");
    };
    block();
    return 0;
}

-clang編譯main.m文件:

clang -rewrite-objc main.m -o main.cpp
  • 打開main.cpp文件:
    image.png
  • 可以發(fā)現(xiàn)block實(shí)際上是一個(gè)結(jié)構(gòu)體,所以block支持%@打印。
  1. 函數(shù)聲明(創(chuàng)建):
  • block是個(gè)結(jié)構(gòu)體,初始化時(shí),存儲(chǔ)執(zhí)行代碼塊匿名函數(shù))和基礎(chǔ)描述信息。
  1. 函數(shù)調(diào)用:
  • 調(diào)用了FuncPtr,實(shí)際就是__main_block_func_0函數(shù),入?yún)?/code>是block自己(這是為了捕獲變量

2.2 block入?yún)⒎治?/h4>

2.2.1 直接加入變量(值拷貝)
  • 加入變量int a,重新編譯:
image.png
  • 發(fā)現(xiàn)編譯時(shí),就生成了對(duì)應(yīng)的變量。__main_block_func_0匿名函數(shù)多了一個(gè)局部變量a,這個(gè)a是讀取了cself內(nèi)存值,是值拷貝。是只讀變量

為了證明是a值拷貝,只讀變量,我們?cè)?code>測試代碼block中添加a++賦值代碼,編譯器立馬報(bào)錯(cuò)提示:(造成了代碼歧義

image.png

2.2.2 __block聲明變量(指針拷貝)

int a使用__block聲明:

image.png

與上面直接使用int a不同:

  • 上面?zhèn)魅?/code>的是a的值,執(zhí)行block函數(shù)時(shí),是進(jìn)行值拷貝,只讀。
  • __block修飾后,會(huì)生成a的結(jié)構(gòu)體對(duì)象,傳入的是對(duì)象指針地址執(zhí)行block函數(shù)時(shí),是進(jìn)行指針拷貝可讀可寫,通過修改指針指向的內(nèi)容。完成block內(nèi)外通訊。

3. 源碼探索

3.1 Block的三種類型

Block有【3種類型】:

  1. __NSGlobalBlock__無入?yún)?/code>時(shí),是全局Block
  2. __NSMallocBlock__ :有外部變量時(shí),變成堆區(qū)Block
  3. __NSStackBlock__:有外部變量,使用__weak修飾時(shí),變成棧區(qū)Block
  • 測試代碼:
- (void)demo {
    
    // 【3種block類型】
    // 1. __NSGlobalBlock__  (無入?yún)r(shí),是全局Block)
    void(^block1)(void) = ^{
        NSLog(@"HT_Block");
    };
    NSLog(@"%@",block1); // 打?。?<__NSGlobalBlock__: 0x102239040>
    
    // 2. __NSMallocBlock__ (有外部變量時(shí),變成堆區(qū)Block)
    int a = 10;
    void(^block2)(void) = ^{
        NSLog(@"HT_Block %d",a);
    };
    NSLog(@"%@",block2); // 打?。?<__NSMallocBlock__: 0x600000f970c0>
    
    //3. __NSStackBlock__ (有外部變量,使用__weak修飾,變成棧區(qū)Block)
    int b = 20;
    void(^__weak block3)(void) = ^{
        NSLog(@"HT_Block %d",b);
    };
    NSLog(@"%@",block3); // 打印 <__NSStackBlock__: 0x7ffeedae8240>
}
image.png
  • 為弄清楚底層原理,首先得確定block源碼哪個(gè)庫中:

block創(chuàng)建前,加入斷點(diǎn)

image.png
  • 打開匯編模式
image.png
  • 運(yùn)行代碼,加入objc_retainBlock符號(hào)斷點(diǎn):

    image.png

  • 繼續(xù)運(yùn)行,加入_Block_copy符號(hào)斷點(diǎn):

    image.png

  • 發(fā)現(xiàn)Block相關(guān)操作,是在libsystem_blocks.dylib庫中:

image.png

?? 源碼地址 ,搜索libclosure-74,點(diǎn)擊右邊下載按鈕。

  • 查看源碼,可以關(guān)注到block結(jié)構(gòu)類型Block_layout:(后面會(huì)明白為什么是Block_layout
    image.png

Flag標(biāo)識(shí)

image.png

  • 第1 位,釋放標(biāo)記,一般常用BLOCK_NEEDS_FREE& 位與操作,一同傳入Flags,告知該 block 可釋放
  • 第2-16位,存儲(chǔ)引用計(jì)數(shù)的值;是一個(gè)可選用參數(shù) (0xfffe二進(jìn)制為1111 1111 1111 1110
  • 第25位,低16位是否有效標(biāo)志,程序根據(jù)它來決定是否增加或是減少引用計(jì)數(shù)位的值;
  • 第26位,是否擁有拷貝輔助函數(shù)(是否調(diào)用_Block_call_copy_helper函數(shù)); 決定是否有block_description_2
  • 第27位,是否擁有block 析構(gòu)函數(shù);
  • 第28位,標(biāo)志是否有垃圾回收; //OS X
  • 第29位,標(biāo)志是否是全局block;
  • 第30位,與 BLOCK_HAS_SIGNATURE 相對(duì),判斷是否當(dāng)前 block擁有一個(gè)簽名。用于runtime時(shí)動(dòng)態(tài)調(diào)用
  • 第31位,是否有簽名
  • 第32位,標(biāo)志是否有Layout,使用有拓展,決定block_description_3
  • Block基礎(chǔ)結(jié)構(gòu)梳理圖:
Block基礎(chǔ)結(jié)構(gòu)
  • 下面,我們通過案例分析驗(yàn)證上面結(jié)構(gòu)圖

3.2 Block的類型轉(zhuǎn)變

  • 我們從最簡單無入?yún)?/code>、無返參全局block開始分析:
- (void)demo {
    void(^block)(void) = ^{
        NSLog(@"HT_Block");
    };
    block();
    NSLog(@"%@",block);
}
  • 為了便于寄存器讀取操作,我這里使用真機(jī)進(jìn)行演示,在void(^block1)(void) = ^{這一行加入斷點(diǎn),打開匯編模式,運(yùn)行代碼至斷點(diǎn)處

    image.png

  • 當(dāng)前讀取的是全局Block

    image.png

  • 我們給block添加入?yún)?/code>,讓block捕獲外界變量:

- (void)demo {
    NSArray * arr = @[@"1"]; 
    void(^block)(void) = ^{
        NSLog(@"HT_Block %@",arr);  // 捕獲外界變量:arr
    };
    block();
    NSLog(@"%@",block);
}
  • 斷點(diǎn)位置不變,運(yùn)行代碼,進(jìn)入?yún)R編頁面后,斷點(diǎn)在objc_retainBlock這行,打印x0(當(dāng)前對(duì)象):
    image.png

Q: 按照上面打印結(jié)果來說,有外界變量修飾符時(shí),應(yīng)該是堆區(qū)Block,為什么這里是棧區(qū)Block?
A: Block棧區(qū)拷貝到堆區(qū),是 _Block_copy操作的。(我們往下看)

  • 往下驗(yàn)證,加入objc_retainBlock符號(hào)斷點(diǎn),往下執(zhí)行進(jìn)入objc_retainBlock函數(shù)內(nèi)部:

    image.png

  • 再次讀取x0,發(fā)現(xiàn)此時(shí)沒變。我們繼續(xù)control+ 鼠標(biāo)左鍵,點(diǎn)擊進(jìn)入按鈕。不斷使用register read x0p讀取、打印當(dāng)前對(duì)象:

    image.png

  • 發(fā)現(xiàn)進(jìn)入_Block_copy時(shí)是棧區(qū)Block,但是return出來時(shí),卻變成了堆區(qū)Block:

    image.png

  • 所以block真正從棧區(qū)拷貝到堆區(qū),是_Block_copy進(jìn)行的。

回顧上面Block基礎(chǔ)結(jié)構(gòu)梳理圖,我們可以通過Block對(duì)象的首地址進(jìn)行內(nèi)存平移獲取到invoke的。

  • 通過Block對(duì)象的首地址偏移,取到了_block_invoke

    image.png

  • control + 鼠標(biāo)左鍵,點(diǎn)擊進(jìn)入,發(fā)現(xiàn)_block_invoke內(nèi)部就是block的函數(shù)執(zhí)行內(nèi)容

    image.png

【深入探究】

  • 既然我們可以通過內(nèi)存平移,取到invoke函數(shù)。那順便可以驗(yàn)證上面Block基礎(chǔ)結(jié)構(gòu)梳理圖的完整結(jié)構(gòu):
  • objc_retainBlock之后,加斷點(diǎn),此時(shí)完成了_Block_copy 操作,完成棧區(qū)堆區(qū)block拷貝:
    image.png
  • 對(duì)照右邊圖,慢慢看吧 ?? 整個(gè)結(jié)構(gòu)非常清晰。


    image.png
  • block類型@?,簽名中包含參數(shù)個(gè)數(shù),入?yún)?/code>和返參具體內(nèi)容占用內(nèi)存大小。

3.3 _Block_copy

  • 進(jìn)入源碼。搜索_Block_copy:
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    
    // block都是`Block_layout`類型
    struct Block_layout *aBlock;

    // 沒有內(nèi)容,直接返回空
    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 將內(nèi)容轉(zhuǎn)變?yōu)閌Block_layout`結(jié)構(gòu)體格式
    aBlock = (struct Block_layout *)arg;
    // 檢查是否需要釋放
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果是全局Block,直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    //
    else {
        // Its a stack block.  Make a copy.
        // 進(jìn)入的是棧區(qū)block,拷貝一份
        // 開辟一個(gè)大小空間的result對(duì)象
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 開辟失敗,就返回
        if (!result) return NULL;
        // 內(nèi)存拷貝:將aBlock內(nèi)容拷貝到result中
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        //result的invoke指向aBlock的invoke。
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING :前16位都為1
        // ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING):前16位都為0
        // 與操作,結(jié)果為前16位都為0 應(yīng)用計(jì)數(shù)為0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 設(shè)置為需要釋放,引用計(jì)數(shù)為1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // 生成desc,并記錄了result和aBlock
        _Block_call_copy_helper(result, aBlock); //
        // Set isa last so memory analysis tools see a fully-initialized object.
        // 設(shè)置isa為堆區(qū)Block
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  1. 如果block需要釋放(表示已經(jīng)在堆區(qū)),就增加引用計(jì)數(shù)
  2. 如果是全局Block,直接返回Block_layout結(jié)構(gòu)的aBlock
  3. 其他情況,都是從棧區(qū)拷貝到堆區(qū)
    malloc申請(qǐng)空間 -> memove內(nèi)存拷貝 -> invoke指針拷貝 ->
    flag引用計(jì)數(shù)設(shè)為1 -> 生成desc -> 設(shè)置isa堆Block -> 返回堆區(qū)Block

4. block的參數(shù)處理(多次拷貝)

Q: block對(duì)外界捕獲變量怎么管理的?有哪些操作?

  • __block修飾的NSArray為例,測試代碼如下:
- (void)demo {
    __block NSArray * arr = @[@"1"];
    void(^block)(void) = ^{
        NSLog(@"HT_Block %@",arr);  // 捕獲外界變量:arr
    };
    block();
    NSLog(@"%@",block);
}
  • 使用clangViewController.m文件生成ViewController.cpp文件,分析:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
image.png
  • 進(jìn)入源碼,搜索_Block_object_assign:
image.png
  • 不同枚舉類型組合,有不同引用方式,其中最復(fù)雜的,是_Block_copy_Block_byref_copy

  • _Block_copy上面已分析過了,我們搜索_Block_byref_copy

    image.png

重點(diǎn):

此處有2次拷貝+更深層次拷貝

  • 【第一次拷貝】:Block自身 (拷貝一份到)-> Block_byref結(jié)構(gòu)體(src棧區(qū)結(jié)構(gòu)體 )
  • 【第二次拷貝】: src棧區(qū)結(jié)構(gòu)體 (拷貝一份到)-> copy堆區(qū)結(jié)構(gòu)體
  • 【更深次拷貝】:調(diào)用byref_keep方法,內(nèi)部又執(zhí)行_Block_object_assign函數(shù),再判斷是否繼續(xù)往下拷貝嵌套
  • 以上,就是Block創(chuàng)建、調(diào)用Block釋放調(diào)用_Block_object_dispose函數(shù):
    image.png

關(guān)于Block探索,都在這里了。有興趣可以一個(gè)個(gè)類型測試監(jiān)測

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容