iOS中Block實現原理的全面分析

大神Q發表於2019-05-04

本人簡書地址iOS中Block實現原理的全面分析

Block的底層基本結構


void blockTest()
{
    void (^block)(void) = ^{
        NSLog(@"Hello World!");
    };
    block();
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        blockTest();
    }
}
複製程式碼

通過clang命令檢視編譯器是如何實現Block的,在終端輸入clang -rewrite-objc main.m,然後會在當前目錄生成main.cpp的C++檔案,程式碼如下:


struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_0048d2_mi_0);
    }
    

static struct __blockTest_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};

void blockTest()
{
    void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        blockTest();
    }
}

static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

複製程式碼

下面我們一個一個來看

__blockTest_block_impl_0

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

__blockTest_block_impl_0Block的C++實現,是一個結構體,從命名可以看出表示blockTest中的第一個(0Block。通常包含兩個成員變數__block_impl impl__blockTest_block_desc_0* Desc和一個建構函式。

__block_impl

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

複製程式碼

__block_impl也是一個結構體

  • *isa:isa指標,指向一個類物件,有三種型別:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,本例中是_NSConcreteStackBlock型別。
  • Flags:block 的負載資訊(引用計數和型別資訊),按位儲存。
  • Reserved:保留變數。
  • *FuncPtr:一個指標,指向Block執行時呼叫的函式,也就是Block需要執行的程式碼塊。在本例中是__blockTest_block_func_0函式。

__blockTest_block_desc_0

static struct __blockTest_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};

複製程式碼

__blockTest_block_desc_0是一個結構體,包含兩個成員變數:

  • reserved:Block版本升級所需的預留區空間,在這裡為0。
  • Block_size:Block大小(sizeof(struct __blockTest_block_impl_0))

__blockTest_block_desc_0_DATA是一個__blockTest_block_desc_0的一個例項。

__blockTest_block_func_0

__blockTest_block_func_0就是Block的執行時呼叫的函式,引數是一個__blockTest_block_impl_0型別的指標。

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_0048d2_mi_0);
    }

複製程式碼

blockTest

void blockTest()
{
    void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

複製程式碼

第一部分,定義Block

void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));

複製程式碼

我們看到block變成了一個指標,指向一個通過__blockTest_block_impl_0建構函式例項化的結構體__blockTest_block_impl_0例項,__blockTest_block_impl_0在初始化的時候需要兩個個引數:

  • __blockTest_block_func_0Block塊的函式指標。
  • __blockTest_block_desc_0_DATA:作為靜態全域性變數初始化__main_block_desc_0的結構體例項指標。

第二部分,呼叫Block

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

複製程式碼

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)通過block->FuncPtr指標找到__blockTest_block_func_0函式並且轉成(void (*)(__block_impl *))型別。
((__block_impl *)block)然後將block作為引數傳給這個函式呼叫。

Flags

__block_impl中我們看到Flags,現在來詳細講一講。

在這裡Block_private.h可以看到Flags的具體資訊:

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

複製程式碼

引用淺談 block(1) - clang 改寫後的 block 結構的解釋:

也就是說,一般情況下,一個 block 的 flags 成員預設設定為 0。如果當 block 需要 Block_copy()Block_release 這類拷貝輔助函式,則會設定成 1 << 25 ,也就是 BLOCK_HAS_COPY_DISPOSE 型別。可以搜尋到大量講述 Block_copy 方法的博文,其中涉及到了 BLOCK_HAS_COPY_DISPOSE

總結一下列舉類的用法,前 16 位即起到標記作用,又可記錄引用計數:

  • BLOCK_DEALLOCATING:釋放標記。一般常用 BLOCK_NEEDS_FREE 做 位與 操作,一同傳入 Flags ,告知該 block 可釋放。
  • BLOCK_REFCOUNT_MASK:一般參與判斷引用計數,是一個可選用引數。
  • BLOCK_NEEDS_FREE:通過設定該列舉位,來告知該 block 可釋放。意在說明 block 是 heap block ,即我們常說的 _NSConcreteMallocBlock 。
  • BLOCK_HAS_COPY_DISPOSE:是否擁有拷貝輔助函式(a copy helper function)。
  • BLOCK_HAS_CTOR:是否擁有 block 解構函式(dispose function)。
  • BLOCK_IS_GC:是否啟用 GC 機制(Garbage Collection)。
  • BLOCK_HAS_SIGNATURE:與 BLOCK_USE_STRET 相對,判斷是否當前 block 擁有一個簽名。用於 runtime 時動態呼叫。

block截獲變數

截獲auto變數值

Screen Shot 2019-05-03 at 3.47.08 PM.png

我們看到直接在block修改變數會提示錯誤,為什麼呢?

void blockTest()
{
    int num = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",num);
    };
    num = 20;
    block();
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        blockTest();
    }
}

複製程式碼

列印結果是10,clang改寫後的程式碼如下:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int num;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
  int num = __cself->num; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_3c2714_mi_0,num);
    }
    
    void blockTest()
{
    int num = 10;
    void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, num));
    num = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

複製程式碼

__blockTest_block_impl_0多了一個成員變數int num;,再看看建構函式__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0),可以看到第三個引數只是變數的值,這也就解釋了為什麼列印的是10,因為**block截獲的是值**。

使用static修飾變數

void blockTest()
{
    static int num = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",num);
        num = 30;
    };
    num = 20;
    block();
    NSLog(@"%d",num);
}

複製程式碼

可以在block內部修改變數了,同時列印結果是20,30。clang改寫後的程式碼如下:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int *num;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
  int *num = __cself->num; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_5a95f6_mi_0,(*num));
        (*num) = 30;
    }
    
    void blockTest()
{
    static int num = 10;
    void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, &num));
    num = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_5a95f6_mi_1,num);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

複製程式碼

__blockTest_block_impl_0多了一個成員變數int *num;,和上面不同的是,這次**block截獲的是指標**,所以可以在內部通過指標修改變數的值,同時在外部修改變數的值,block也能"感知到"。那麼為什麼之前傳遞指標呢?因為變數是棧上,作用域是函式blockTest內,那麼有可能變數比block先銷燬,這時候block再通過指標去訪問變數就會有問題。而static修飾的變數不會被銷燬,也就不用擔心。

全域性變數

int num = 10;

void blockTest()
{
    void (^block)(void) = ^{
        NSLog(@"%d",num);
        num = 30;
    };
    num = 20;
    block();
    NSLog(@"%d",num);
}

複製程式碼

列印結果是20,30。clang改寫後的程式碼如下:

int num = 10;


struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_1875c6_mi_0,num);
        num = 30;
    }

複製程式碼

非常簡單,在初始化__blockTest_block_impl_0並沒有把num作為引數,__blockTest_block_func_0中也是直接訪問全域性變數。

總結:

變數型別 是否捕獲到block內部 訪問方式
區域性auto變數 值傳遞
區域性static變數 指標傳遞
全域性變數 直接訪問

使用__block修飾變數

void blockTest()
{
    __block int num = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",num);
        num = 30;
    };
    num = 20;
    block();
    NSLog(@"%d",num);
}

複製程式碼

效果和使用static修飾變數一樣,clang改寫後的程式碼如下:

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
  __Block_byref_num_0 *num = __cself->num; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_018b76_mi_0,(num->__forwarding->num));
        (num->__forwarding->num) = 30;
    }
    
static void __blockTest_block_copy_0(struct __blockTest_block_impl_0*dst, struct __blockTest_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __blockTest_block_dispose_0(struct __blockTest_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __blockTest_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __blockTest_block_impl_0*, struct __blockTest_block_impl_0*);
  void (*dispose)(struct __blockTest_block_impl_0*);
} __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0), __blockTest_block_copy_0, __blockTest_block_dispose_0};

void blockTest()
{
    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
    void (*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    (num.__forwarding->num) = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_04_xwbq8q6n0p1dmhhd6y51_vbc0000gp_T_main_018b76_mi_1,(num.__forwarding->num));
}

複製程式碼

哇,難受啊兄dei,怎麼多出來這麼多東西,沒關係,慢慢分析。

__blockTest_block_impl_0多出來一個成員變數__Block_byref_num_0 *num;,我們看到經過__block修飾的變數型別變成了結構體__Block_byref_num_0__blockTest_block_impl_0多出來一個成員變數__Block_byref_num_0 *num;block捕獲的是__Block_byref_num_0型別指標

__Block_byref_num_0
我們看到__Block_byref_num_0是一個結構體,並且有一個isa,因此我們可以知道它其實就是一個物件。同時還有一個__Block_byref_a_0 *型別的__forwardingnumnum我們能猜到就是用來儲存變數的值,__forwarding就有一點複雜了,後面慢慢講。

__blockTest_block_copy_0和**__blockTest_block_dispose_0**

__blockTest_block_copy_0中呼叫的是_Block_object_assign__blockTest_block_dispose_0中呼叫的是_Block_object_dispose

函式 呼叫時機
__blockTest_block_copy_0 __block變數結構體例項從棧拷貝到堆時
__blockTest_block_dispose_0 __block變數結構體例項引用計數為0時

關於_Block_object_assign_Block_object_dispose更詳細程式碼可以在runtime.c 中檢視。

BLOCK_FIELD_IS_BYREF
我們看到_Block_object_assign_Block_object_dispose中都有個引數值為8,BLOCK_FIELD_IS_BYREF型別,什麼意思呢?在Block_private.h 中可以檢視到:

// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
複製程式碼
  • BLOCK_FIELD_IS_OBJECT:OC物件型別
  • BLOCK_FIELD_IS_BLOCK:是一個block
  • BLOCK_FIELD_IS_BYREF:在棧上被__block修飾的變數
  • BLOCK_FIELD_IS_WEAK:被__weak修飾的變數,只在Block_byref管理內部物件記憶體時使用
  • BLOCK_BYREF_CALLER:處理Block_byref內部物件記憶體的時候會加的一個額外標記(告訴內部實現不要進行retain或者copy)

__blockTest_block_desc_0 我們可以看到它多了兩個回撥函式指標*copy*dispose,這兩個指標會被賦值為__main_block_copy_0__main_block_dispose_0

最後我們看到訪問num是這樣的:

__Block_byref_num_0 *num = __cself->num; // bound by ref   

(num->__forwarding->num) = 30;
複製程式碼

下面就講一講為什麼要這樣。

Block的記憶體管理

在前面我們講到__block_impl指向的_NSConcreteStackBlock型別的類物件,其實總共有三種型別:

型別 儲存區域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 資料區
_NSConcreteMallocBlock

前面也講到copydispose,在ARC環境下,有哪些情況編譯器會自動將棧上的把Block從棧上覆制到堆上呢?

Block從棧中複製到堆
呼叫Block的copy例項方法時
Block作為函式返回值返回時
在帶有usingBlock的Cocoa方法或者GCD的API中傳遞Block時候
將block賦給帶有__strong修飾符的id型別或者Block型別時

Bock從棧中複製到堆,__block也跟著變化:

Screen Shot 2019-05-04 at 1.03.23 AM.png

  1. Block在棧上時,__block的儲存域是棧,__block變數被棧上的Block持有。
  2. Block被複制到堆上時,會通過呼叫Block內部的copy函式,copy函式內部會呼叫_Block_object_assign函式。此時__block變數的儲存域是堆,__block變數被堆上的Block持有。
  3. 當堆上的Block被釋放,會呼叫Block內部的disposedispose函式內部會呼叫_Block_object_dispose,堆上的__block被釋放。

Screen Shot 2019-05-04 at 1.09.18 AM.png

  1. 當多個棧上的Block使用棧上的__block變數,__block變數被棧上的多個Block持有。
  2. Block0被複制到堆上時,__block也會被複制到堆上,被堆上Block0持有。Block1仍然持有棧上的__block,原棧上__block變數的__forwarding指向拷貝到堆上之後的__block變數。
  3. Block1也被複制到堆上時,堆上的__block被堆上的Block0Block1只有,並且__block的引用計數+1。
  4. 當堆上的Block都被釋放,__block變數結構體例項引用計數為0,呼叫_Block_object_dispose,堆上的__block被釋放。

下圖是描述__forwarding變化。這也就能解釋__forwarding存在的意義:

__forwarding 保證在棧上或者堆上都能正確訪問對應變數

Screen Shot 2019-05-04 at 2.52.00 PM.png

int main(int argc, char * argv[]) {

    int num = 10;

    NSLog(@"%@",[^{
        NSLog(@"%d",num);
    } class]);

    void (^block)(void) = ^{
        NSLog(@"%d",num);
    };

    NSLog(@"%@",[block class]);
}

複製程式碼

列印結果:

2019-05-04 18:40:48.470228+0800 BlockTest[35824:16939613] __NSStackBlock__
2019-05-04 18:40:48.470912+0800 BlockTest[35824:16939613] __NSMallocBlock__

複製程式碼

我們可以看到第一個Block沒有賦值給__strong指標,而第二個Block沒有賦值給__strong指標,所以第一個在棧上,而第二個在堆上。

Block截獲物件

int main(int argc, char * argv[]) {
    {
        Person *person = [[Person alloc] init];
        person.name = @"roy";

        NSLog(@"%@",[^{
            NSLog(@"%@",person.name);
        } class]);
        NSLog(@"%@",@"+++++++++++++");
    }
    NSLog(@"%@",@"------------");
}

複製程式碼

列印結果:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person

- (void)dealloc {
    NSLog(@"-------dealloc-------");
}

@end

typedef void(^Block)(void);

int main(int argc, char * argv[]) {
    {
        Person *person = [[Person alloc] init];
        person.name = @"roy";

        NSLog(@"%@",[^{
            NSLog(@"%@",person.name);
        } class]);
        NSLog(@"%@",@"+++++++++++++");
    }
    NSLog(@"%@",@"------------");
}
複製程式碼

我們看到當Block內部訪問了物件型別的auto物件時,如果Block是在棧上,將不會對auto物件產生強引用。

auto Strong 物件


typedef void(^Block)(void);

int main(int argc, char * argv[]) {
    Block block;
    {
        Person *person = [[Person alloc] init];
        person.name = @"roy";

        block = ^{
            NSLog(@"%@",person.name);
        };
        person.name = @"david";
        NSLog(@"%@",@"+++++++++++++");
    }
    NSLog(@"%@",@"------------");
    block ();
}

複製程式碼

列印結果是

2019-05-04 17:46:27.083280+0800 BlockTest[33745:16864251] +++++++++++++
2019-05-04 17:46:27.083934+0800 BlockTest[33745:16864251] ------------
2019-05-04 17:46:27.084018+0800 BlockTest[33745:16864251] david
2019-05-04 17:46:27.084158+0800 BlockTest[33745:16864251] -------dealloc-------

複製程式碼

我們看到是先列印的david再呼叫Person的析構方法dealloc,在終端輸入clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.13 main.m -fobjc-arc,clang在ARC環境下改寫後的程式碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

複製程式碼

我們看到__main_block_impl_0中的Person *__strong person;成員變數。

Block截獲了auto物件,當Block被拷貝到堆上,Block強引用auto物件,這就能解釋了為什麼超出了person的作用域,person沒有立即釋放,當Block釋放之後,會自動去掉對該物件的強引用,該物件就會被釋放了。

auto Weak 物件


typedef void(^Block)(void);

int main(int argc, char * argv[]) {
    Block block;
    {
        Person *person = [[Person alloc] init];
        person.name = @"roy";
        __weak Person *weakPerson = person;

        block = ^{
            NSLog(@"%@",weakPerson.name);
        };
        weakPerson.name = @"david";
        NSLog(@"%@",@"+++++++++++++");
    }
    NSLog(@"%@",@"------------");
    block ();
}

複製程式碼

列印結果是

2019-05-04 17:49:38.858554+0800 BlockTest[33856:16869229] +++++++++++++
2019-05-04 17:49:38.859218+0800 BlockTest[33856:16869229] -------dealloc-------
2019-05-04 17:49:38.859321+0800 BlockTest[33856:16869229] ------------
2019-05-04 17:49:38.859403+0800 BlockTest[33856:16869229] (null)

複製程式碼

直接在終端輸入clang -rewrite-objc main.m會報cannot create __weak reference because the current deployment target does not support weak ref錯誤。需要用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.13 main.m-fobjc-arc代表當前是ARC環境 -fobjc-runtime=macosx-10.13:代表當前執行時環境,缺一不可,clang之後的程式碼:


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

我們看到__main_block_impl_0中的Person *__weak weakPerson;成員變數。

總結:

  1. Block內部訪問了物件型別的auto物件時,如果Block是在棧上,將不會對auto物件產生強引用。
  2. 如果block被拷貝到堆上,會呼叫Block內部的copy函式,copy函式內部會呼叫_Block_object_assign函式,_Block_object_assign會根據auto物件的修飾符(__strong__weak__unsafe_unretained)做出相應的操作,當使用的是__strong時,將會對person物件的引用計數加1,當為__weak時,引用計數不變。
  3. 如果Block從對上移除,會呼叫block內部的dispose函式,內部會呼叫_Block_object_dispose函式,這個函式會自動釋放引用的auto物件。

Block迴圈引用


@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void (^block)(void);

- (void)testReferenceSelf;

@end

@implementation Person

- (void)testReferenceSelf {
    self.block = ^ {
        NSLog(@"self.name = %s", self.name.UTF8String);
    };
    self.block();
}

- (void)dealloc {
    NSLog(@"-------dealloc-------");
}

@end


int main(int argc, char * argv[]) {
    Person *person = [[Person alloc] init];
    person.name = @"roy";
    [person testReferenceSelf];
}

複製程式碼

列印結果是self.name = royPerson的析構方法dealloc並沒有執行,這是典型的迴圈引用,下面我們研究研究為啥會迴圈引用。clang改寫後的程式碼如下:


struct __Person__testReferenceSelf_block_impl_0 {
  struct __block_impl impl;
  struct __Person__testReferenceSelf_block_desc_0* Desc;
  Person *const __strong self;
  __Person__testReferenceSelf_block_impl_0(void *fp, struct __Person__testReferenceSelf_block_desc_0 *desc, Person *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void _I_Person_testReferenceSelf(Person * self, SEL _cmd) {
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__Person__testReferenceSelf_block_impl_0((void *)__Person__testReferenceSelf_block_func_0, &__Person__testReferenceSelf_block_desc_0_DATA, self, 570425344)));
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}

複製程式碼

我們看到本來Person中testReferenceSelf方法是沒有引數的,但是轉成C++之後多出來兩個引數:* self_cmd,再看看__Person__testReferenceSelf_block_impl_0中多出來一個成員變數Person *const __strong self;,因此我們知道Person中block捕獲了selfblock強引用self,同時self也強引用block,因此形成迴圈引用。

Weak解除迴圈引用

@implementation Person

- (void)testReferenceSelf {
    __weak typeof(self) weakself = self;
    self.block = ^ {
        __strong typeof(self) strongself = weakself;
        NSLog(@"self.name = %s", strongself.name.UTF8String);
    };
    self.block();
}

- (void)dealloc {
    NSLog(@"-------dealloc-------");
}

@end

複製程式碼

列印結果:

2019-05-04 19:27:48.274358+0800 BlockTest[37426:17007507] self.name = roy
2019-05-04 19:27:48.275016+0800 BlockTest[37426:17007507] -------dealloc-------

複製程式碼

我們看到Person物件被正常釋放了,說明不存在迴圈引用,為什麼呢?clang改寫後的程式碼如下:

struct __Person__testReferenceSelf_block_impl_0 {
  struct __block_impl impl;
  struct __Person__testReferenceSelf_block_desc_0* Desc;
  Person *const __weak weakself;
  __Person__testReferenceSelf_block_impl_0(void *fp, struct __Person__testReferenceSelf_block_desc_0 *desc, Person *const __weak _weakself, int flags=0) : weakself(_weakself) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static void _I_Person_testReferenceSelf(Person * self, SEL _cmd) {
    __attribute__((objc_ownership(weak))) typeof(self) weakself = self;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__Person__testReferenceSelf_block_impl_0((void *)__Person__testReferenceSelf_block_func_0, &__Person__testReferenceSelf_block_desc_0_DATA, weakself, 570425344)));
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}
複製程式碼

可以看到__Person__testReferenceSelf_block_impl_0結構體中weakself成員是一個__weak修飾的Person型別物件,也就是說__Person__testReferenceSelf_block_impl_0對Person的依賴是弱依賴。weak修飾變數是在runtime中進行處理的,在Person物件的Dealloc方法中會呼叫weak引用的處理方法,從weak_table中尋找弱引用的依賴物件,進行清除處理。

最後

好了,關於Block就寫到這裡了,花了五一的三天時間解決了一個基礎知識點,如釋重負,寫的真心累。

參考文章
淺談 block(1) - clang 改寫後的 block 結構
Objc Block實現分析
(四)Block之 __block修飾符及其儲存域
(三)Block之截獲變數和物件 關於Block再囉嗦幾句
__block變數儲存域
Block學習⑤--block對物件變數的捕獲
淺談Block實現原理及記憶體特性之三: copy過程分析
iOS底層原理總結 - 探尋block的本質(一)

相關文章