iOS-OC底層-block分析

前言
block的類型

從一段代碼開始

// 全局變量 n,name
static int n = 100;
static NSString *name = @"zezefamily";
- (void)demo10
{
    //0.沒有引用外部變量的 testBlock0
    void (^testBlock0)(BOOL ok);
    testBlock0 = ^(BOOL ok){
        
    };
    NSLog(@"testBlock0 == %@",testBlock0);
    //1.引用全局靜態變量的 testBlock1
    void (^testBlock1)(BOOL ok);
    testBlock1 = ^(BOOL ok){
        n;
        name;
    };
    NSLog(@"testBlock1 == %@",testBlock1);
    //2.引用了局部變量的 testBlock2
    int a = 10;
    void (^testBlock2)(BOOL ok);
    testBlock2 = ^(BOOL ok){
        a;
    };
    NSLog(@"testBlock2 == %@",testBlock2);
    //3.引用了局部靜態變量的 testBlock3
    static int c = 15;
    void (^testBlock3)(BOOL ok);
    testBlock3 = ^(BOOL ok){
        c;
    };
    NSLog(@"testBlock2 == %@",testBlock3);
    //4.沒有引用外部變量且弱引用修飾的 testBlock4
    void (^__weak testBlock4)(void) = ^{

    };
    NSLog(@"testBlock4 == %@",testBlock4);
    
    //5.引用了局部變量的且弱引用修飾的 testBlock5
    NSString *b = @"string";
    void (^__weak testBlock5)(void) = ^{
        b;
    };
    NSLog(@"testBlock5 == %@",testBlock5);
    //6.引用了全局靜態變量且弱引用修飾的 testBlock6
    void (^__weak testBlock6)(void) = ^{
        n;
        name;
    };
    NSLog(@"testBlock6 == %@",testBlock6);
}

看下打印信息:

TestApp[69115:8949095] testBlock0 == <__NSGlobalBlock__: 0x10ef24230>
TestApp[69115:8949095] testBlock1 == <__NSGlobalBlock__: 0x10ef24250>
TestApp[69115:8949095] testBlock2 == <__NSMallocBlock__: 0x6000004d4930>
TestApp[69115:8949095] testBlock2 == <__NSGlobalBlock__: 0x10ef24290>
TestApp[69115:8949095] testBlock4 == <__NSGlobalBlock__: 0x10ef242d0>
TestApp[69115:8949095] testBlock5 == <__NSStackBlock__: 0x7ffee0cdd100>
TestApp[69115:8949095] testBlock6 == <__NSGlobalBlock__: 0x10ef242f0>

首先我們可以看到,block有3中類型,分別為:NSGlobalBlock,NSMallocBlock,NSStackBlock
1.當沒有引用任何外部變量的,或引用了全局靜態變量的,或引用了局部靜態變量的blockNSGlobalBlock(全局block);
2.當引用了局部變量且沒有__weak修飾的block,為NSMallocBlock堆block);
3.當引用了局部變量且__weak修飾的block,為NSStackBlock(棧block);

block的本質

我們在main.m中注入幾行簡單的block代碼片段,然后通過Clang看一下編譯后的情況,下面是簡單的block應用:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        // block 測試代碼
        void (^testBlock)(void) = ^{
            NSLog(@"hello block");
        };
        testBlock();
    }
    
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

通過Clang來看下編譯后的樣子:

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

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));

        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    }

    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

上面代碼看著比較凌亂,我們稍微的整理一下:

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

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        
        void (*testBlock)(void) = _main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
        
        testBlock->FuncPtr(testBlock);
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

簡化之后我們可以看到,我們的block代碼部分被轉化成了一個__main_block_impl_0 ()的函數,通過傳入了__main_block_func_0,__main_block_desc_0_DATA 2個參數。
testBlock();調用部分被轉化為了testBlock->FuncPrt(testBlock),調用FuncPrt函數,并將testBlock自己傳入。
首先我們先看下__main_block_func_0:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};

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

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

首先__main_block_impl_0是一個結構體,包含impl,Desc,和一個同名的構造函數,而我們的block最終訪問的就是這個__main_block_impl_0()構造函數,內部對implDesc進行了賦值操作,這里重點看下impl.FuncPtr = fp, 這里的fp就是外界出入的__main_block_func_0
接下來我們看下構造函數里的參數:
參數1:__main_block_func_0 也就是這個fp,同樣是impl.FuncPtr

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_main_9dacc5_mi_0);
}

這里就是block代碼塊。
__main_block_func_0是什么時候被調用的吶?這里看一下main函數

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

這里將testBlock強轉為__block_impl*類型,并調用了FuncPtr(),也就是__main_block_func_0 ()函數,之后就執行了整個block;
到這里,我們也就知道了,我們的block,在編譯時被包裝成了一個xx_block_impl_0的結構體,并執行了同名的構造函數,將block塊轉換成__xx_block_func_0函數作為參數傳入,同時還傳入了一些描述信息。執行block時,被轉為調用FuncPtr()函數,即__xx_block_func_0函數。

block變量捕獲

我們在來看下block的變量捕獲,首先給我們的代碼添加一個變量:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        // block 測試代碼
        int a = 1024;
        void (^testBlock)(void) = ^{
            NSLog(@"a = %d",a);
        };
        testBlock();
    }
    
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

Clang看下編譯后有什么不同,直接將main函數中簡化的代碼片段看一下:

int main(int argc, char * argv[]) {
  {
    int a = 1024;
    void (*testBlock)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
    testBlock->FuncPtr(testBlock);
  }
}

這里可以看到我們定義的變量a,同時可以看到__main_block_impl_0函數末尾也多了一個參數a; 再看下__main_block_impl_0結構體:

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

這個結構體中也明顯的看到了多一個成員int a;,同時構造函數也多了一個參數,并將傳入的_a賦值給a;
這就意味著,在程序編譯時,自動將外部變量捕獲并添加到了__main_block_impl_0結構體內;
再看下__main_block_func_0() 代碼塊:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
  int a = __cself->a; // bound by copy
    
  NSLog((NSString *)&"a = %d",a);
}

這里可以注意到,NSLog輸出的這個變量a,并不是block外界的那個int a,而是來自于__cself->a,即__main_block_impl_0結構體中的int a變量,這里我的理解是發生了一次深拷貝的a, 這樣也就解釋了為什么非__block修飾的變量無法在block內部修改,因為他們完全不是同一樣變量(值和指針都不是同一個)。
接下來使用__block修飾一下int a,看編譯結果:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        // block 測試代碼
        __block int a = 1024;
        void (^testBlock)(void) = ^{
            a++;
            NSLog(@"a = %d",a);
        };
        testBlock();
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

編譯后:

int main() {
  __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
            (void*)0,
            (__Block_byref_a_0 *)&a,
            0,
            sizeof(__Block_byref_a_0),
            1024
        };
        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
}

這里很明顯的看到使用__block修飾的變量a被包裝成了一個__Block_byref_a_0結構體對象

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

并保存了變量a的指針地址和值,然后將__Block_byref_a_0對象傳入__main_block_impl_0()函數:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

這個可以看到a = _a->__forwarding,即外界變量a的指針地址,再看下__main_block_func_0代碼塊:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
  (a->__forwarding->a)++;
  NSLog((NSString *)&"a = %d",(a->__forwarding->a));
}

這里的a++操作,是對__forwarding->a的操作,即外界的那個變量a。所以通過__block修飾后的變量為指針拷貝(淺拷貝),都指向同一片地址空間。
我們可以用更直接的方式驗證一下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        // block 測試代碼
        __block int a = 1024;
        int b = 100;
        NSString *c = @"我是c";
        __block NSString *d = @"我是d";
        NSLog(@"a = %d",a);
        NSLog(@"b = %d",b);
        NSLog(@"c = %@, c = %p, &c = %p",c,c,&c);
        NSLog(@"d = %@, d = %p, &d = %p",d,d,&d);
        void (^testBlock)(void) = ^{
            NSLog(@"block內,c = %@, c = %p, &c = %p",c,c,&c);
            NSLog(@"block內,d = %@, d = %p, &d = %p",d,d,&d);
            NSLog(@"block內,a = %d, a = %d, &a = %p",a,a,&a);
            NSLog(@"block內,b = %d, b = %d, &b = %p",b,b,&b);
        };
        a++;
        b++;
        c = @"我修改了c";
        d = @"我修改了d";
        NSLog(@"block外,c = %@, c = %p, &c = %p",c,c,&c);
        NSLog(@"block外,d = %@, d = %p, &d = %p",d,d,&d);
        NSLog(@"block外,a = %d",a);
        NSLog(@"block外,b = %d",b);
        //執行block
        testBlock();
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

打印結果:

TestApp[6068:9618080] c = 我是c, c = 0x10f1cb640, &c = 0x7ffee0a38d08
TestApp[6068:9618080] d = 我是d, d = 0x10f1cb660, &d = 0x7ffee0a38d00
TestApp[6068:9618080] block外,c = 我修改了c, c = 0x10f1cb740, &c = 0x7ffee0a38d08
TestApp[6068:9618080] block外,d = 我修改了d, d = 0x10f1cb760, &d = 0x6000030b3268
TestApp[6068:9618080] block外,a = 1025
TestApp[6068:9618080] block外,b = 101
TestApp[6068:9618080] block內,c = 我是c, c = 0x10f1cb640, &c = 0x600002ba9be0
TestApp[6068:9618080] block內,d = 我修改了d, d = 0x10f1cb760, &d = 0x6000030b3268
TestApp[6068:9618080] block內,a = 1025, a = 1025, &a = 0x600003e925f8
TestApp[6068:9618080] block內,b = 100, b = 100, &b = 0x600002ba9bf8

再結合剛才的源碼是不是瞬間清晰了很多。而且在block內部訪問的參數其實是有一個__cself的隱藏參數間接訪問的,盡管說直觀看到的block內外變量的形式并無差別。

底層原理

上面我們都是編譯時的探索,接下我們通過匯編來看下block運行時的情況。
先來一個簡單的代碼段:

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 10;
    void (^block)(void) = ^{
        a;
        NSLog(@"hello block");
    };
    block();
}

文章一開始就介紹,當一個沒有引用任何外部變量時,是一個__NSGlobalBlock__類型的block,引用局部變量時,是一個__NSMallocBlock__,但在Clang編譯后發現無論怎么引用外部變量,其中的isa都指向_NSConcreteStackBlock類型,這也就說明其他的類型是由運行時動態決定的,但它們是在什么時候發生的改變吶?接下來詳細看下:

截屏2020-11-12 下午4.07.48.png

下一個符號斷點objc_retainBlock,看到其內部跳轉了_Block_copy指令:
截屏2020-11-12 下午4.09.23.png

看下_Block_copy內部實現:
截屏2020-11-12 下午4.12.48.png

可以看到這個指令來自于libsystem_blocks.dylib庫。
libobjc.A.dylibobjc_retainBlock指令,進入了libsystem_blocks.dylib_Block_copy指令;
首先看下我們的代碼片段,根據之前我們知道的當block引用了局部變量為__NSMallocBlock__,這里我們觀察一下objc_retainBlockblock變化情況:
截屏2020-11-12 下午6.03.09.png

(lldb) register read x0
      x0 = 0x000000016b11b850
(lldb) po 0x000000016b11b850
<__NSStackBlock__: 0x16b11b850>
 signature: "v8@?0"
 invoke   : 0x104cedbc0 (/private/var/containers/Bundle/Application/92CCF1EA-8000-48E0-A52B-4B8ECDDFC227/TestApp.app/TestApp`__main_block_invoke)

(lldb) 

通過寄存器輸出,我們看到當前block__NSStackBlock__類型

向下執行一步,然后再次查看寄存器的情況:

(lldb) register read x0
      x0 = 0x00000002833a02d0
(lldb) po 0x00000002833a02d0
<__NSMallocBlock__: 0x2833a02d0>
 signature: "v8@?0"
 invoke   : 0x104cedbc0 (/private/var/containers/Bundle/Application/92CCF1EA-8000-48E0-A52B-4B8ECDDFC227/TestApp.app/TestApp`__main_block_invoke)

(lldb)

此時其實觀察x0,地址已經發生了變化,同時block類型由原來的__NSStackBlock__變為__NSMallocBlock__,也就是我們NSLog輸出的類型。
下一步我們看下objc_retainBlock指令內部操作:

截屏2020-11-12 下午6.07.38.png

此時打印寄存器輸出:

(lldb) register read x0
      x0 = 0x000000016ae13850
(lldb) po 0x000000016ae13850
<__NSStackBlock__: 0x16ae13850>
 signature: "v8@?0"
 invoke   : 0x104ff5bc0 (/private/var/containers/Bundle/Application/BC10EC5A-B715-4785-843E-50181B210D15/TestApp.app/TestApp`__main_block_invoke)

(lldb)

這里說明真正發生改變的還在_Block_copy指令:

截屏2020-11-12 下午6.09.54.png

截屏2020-11-12 下午6.10.04.png

這里我們忽略掉_Block_copy中間的執行部分,直接在retab指令時下斷點,并查看寄存器信息:

(lldb) register read x0
      x0 = 0x0000000281d68000
(lldb) po 0x0000000281d68000
<__NSMallocBlock__: 0x281d68000>
 signature: "v8@?0"
 invoke   : 0x104ff5bc0 (/private/var/containers/Bundle/Application/BC10EC5A-B715-4785-843E-50181B210D15/TestApp.app/TestApp`__main_block_invoke)

(lldb) 

此時的block類型已經變為了__NSMallocBlock__類型,也就是說是在_Block_copy時發生了改變。即:__NSStackBlock__ -> _Block_copy -> __NSMallocBlock__
那我們就看一下_Block_copy的源碼具體做了哪些處理:

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) { 
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {  //是否為Global Block
        return aBlock;
    }
    else { // ARC 下,StackBlock ,copy to MallocBlock , 
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        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;
#endif
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

比較醒目的注釋

// Its a stack block. Make a copy.

如果是stack block 執行一次拷貝

  1. 通過malloc() 開辟一個Block_layout類型的內存空間result
  2. aBlock的內容拷貝給result
  3. result賦值,將result->isa指向_NSConcreteMallocBlock
  4. 返回result;

從源碼可以看到block是一個Block_layout的結構體:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
typedef void(*BlockInvokeFunction)(void *, ...);

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

結構體中的isa指向了block類型; flags存放了很多的標識符信息,類似于isa_t; invokeblock代碼塊,編譯時看到的那個fp->FuncPtr->_block_func_0,descriptor指附加屬性,這里可以看到他是一個Block_descriptor_1的結構體,往下可以看到Block_descriptor_2,Block_descriptor_3, 這些都是由flags內部標識動態決定的。
下面是flags的標識定義:

// 注釋: flag 標識
// 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_HAS_COPY_DISPOSE 決定了是否包含Block_descriptor_2;
BLOCK_HAS_SIGNATURE 決定了是否包含Block_descriptor_3;
對應的實現:

#if 0
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;
}
#endif
// 注釋:Block 的描述 : copy 和 dispose 函數
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

// 注釋: Block 的描述 : 簽名相關
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

通過flags+指針平移向下取值;
我們從編譯時的源碼看到,使用__block修飾的變量,會被包裝成一個Block_byref的結構體對象:

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};
// 注釋: __Block 修飾的結構體 byref_keep 和 byref_destroy 函數 - 來處理里面持有對象的保持和銷毀
struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};

這里也會根據flags標識,動態的插入Block_byref_2Block_byref_3的屬性

// Values for Block_byref->flags to describe __block variables
enum {
    // Byref refcount must use the same bits as Block_layout's refcount.
    // BLOCK_DEALLOCATING =      (0x0001),  // runtime
    // BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime

    BLOCK_BYREF_LAYOUT_MASK =       (0xf << 28), // compiler
    BLOCK_BYREF_LAYOUT_EXTENDED =   (  1 << 28), // compiler
    BLOCK_BYREF_LAYOUT_NON_OBJECT = (  2 << 28), // compiler
    BLOCK_BYREF_LAYOUT_STRONG =     (  3 << 28), // compiler
    BLOCK_BYREF_LAYOUT_WEAK =       (  4 << 28), // compiler
    BLOCK_BYREF_LAYOUT_UNRETAINED = (  5 << 28), // compiler

    BLOCK_BYREF_IS_GC =             (  1 << 27), // runtime

    BLOCK_BYREF_HAS_COPY_DISPOSE =  (  1 << 25), // compiler
    BLOCK_BYREF_NEEDS_FREE =        (  1 << 24), // runtime
};

block捕獲外部變量的核心操作->_Block_object_assign

// 注釋: Block 捕獲外界變量的操作
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

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.
};

該方法會通過傳入的不同類型(id,NSObject,__block,__weak)的數據做響應的處理;
++++++++++++++++++++++start++++++++++++++++++++++++++
以下內容是我對Block的總結:

block 在編譯時會 被轉換為一個 Block_layout 結構體
 同時會將變量捕獲到該結構體內部 指針捕獲,值捕獲
    __block 修飾的變量 會被包裝為 Block_byref 的結構體
        Block_byref 內部的flags 也會根據類型 來決定是否包含 Block_byref1 2
 同時動態生成的 flags 值 來確定是否包含 Block_descriptor_1 2 3
 
 運行時,runtime 會調用 Block_copy()
 根據類型來判斷生成什么類型的Block_layout
 判斷是否為BLOCK_NEEDS_FREE類型 如果是 直接返回
 判斷是否為BLOCK_IS_GLOBAL類型 如果是 直接返回
 否則 從 棧空間 copy 到 堆空間
    1. 通過malloc()開辟一個Block_layout類型的result
    2. memmove 將編譯時的Block_layout 映射給 result
    3. 對 result 做一些初始化操作
    4. _Block_call_copy_helper(result, aBlock);
        4.1 判斷是否包含Block_descriptor_2,如果存在,執行desc2->copy(result, aBlock)->
         _Block_object_assign(void *destArg, const void *object, const int flags)
           4.1.1 判斷當前捕獲的參數類型
                BLOCK_FIELD_IS_OBJECT 普通對象類型
                    _Block_retain_object(object);
                    *dest = object;
                BLOCK_FIELD_IS_BLOCK  block對象類型
                    *dest = _Block_copy(object);
                BLOCK_FIELD_IS_BYREF  __block修飾類型
                    *dest = _Block_byref_copy(object); //對Block_byref類型進行處理
                        1.拿到編譯期生成的Block_byref類型的src
                        2.通過malloc創建Block_byref類型的copy
                        3.對copy進行初始化賦值操作
                        4.判斷src 是否包含Block_byref1 Block_byref2
                            4.1如果包含
                                4.1.1 對copy進行Block_byref1賦值
                                4.1.2 調用Block_byref1->byref_keep->_Block_object_assign()
                            4.2 否則 memmove()
                BLOCK_FIELD_IS_WEAK   __weak修飾類型
                    *dest = _Block_byref_copy(object);
                BLOCK_BYREF_CALLER    called from __block (byref)
                    *dest = object;
                
    5. result.isa = _NSConcreteMallocBlock
    6. 返回 result
 響應block:通過reuslt->invoke()->_block_func_0()
 
 Block即將銷毀時,runtime調用_Block_release()
    1.獲取當前Block_layout aBlock;
    2.判斷是否為空,是否為GlobalBlock 是否不需要Free 直接return
    3._Block_call_dispose_helper(aBlock);
        3.1 獲取Block_descriptor_2 數據 desc
        3.2 desc->dispose()->__main_block_dispose_0->_Block_object_dispose()
            3.2.1 是否為BLOCK_FIELD_IS_BYREF __block 修飾
                _Block_byref_release(object)
                    1.判斷是否存在 Block_byref2 存在 調用byref2->byref_destroy()->__Block_byref_id_object_dispose()->
                            _Block_object_dispose()
                    2.free(byref)
            3.2.2 是否為BLOCK_FIELD_IS_BLOCK block對象
                _Block_release(object);
            3.2.3 其他類型不處理
    4._Block_destructInstance(aBlock); //這個里面其實什么都沒做
    5.free(aBlock)
 
==============>以下編譯時Block生成的數據結構<==================== 

 struct Block_byref {
     void *isa;
     struct Block_byref *forwarding;
     volatile int32_t flags; // contains ref count
     uint32_t size;
 };
 
 Block_layout {
    //基本結構
    &_NSConcreteStackBlock,                 // isa
    570425344,                              // flags
    0,                                      // reserved
    __main_block_func_0,                    // invoke
    //根據flags 動態添加 [Block_descriptor_1]
    0,                                      // Block_descriptor_1 -> reserved
    sizeof(struct __main_block_impl_0),     // Block_descriptor_1 -> size
    //根據flags 動態添加 [Block_descriptor_2]
    __main_block_copy_0,                    // Block_descriptor_2 -> copy
    __main_block_dispose_0                  // Block_descriptor_2 -> dispose
    //變量捕獲
    NSString *age;                          // 對象類型 age
    int *b;                                 // 基本數據類型 b
    __Block_byref *a;                       // __block修飾類型
    __Block_byref *string;                  // __block修飾類型
 }
 
 Block_byref a = {
     //基本結構
     (void*)0,                      //isa
     (Block_byref *)&a,             //forwarding
     0,                             //flags
     sizeof(__Block_byref_a_0),     //size
     //變量捕獲
     10                             //變量a的值
 };
 
 Block_byref string = {
     (void*)0,                          //isa
     (__Block_byref *)&string,,         //forwarding
     33554432,                          //flags
     sizeof(__Block_byref),             //size
     //根據flag動態添加
     __Block_byref_id_object_copy,      //Block_byref_2 -> byref_keep
     __Block_byref_id_object_dispose    //Block_byref_2 -> byref_destroy
 };
 
*/

Block源碼中重要就是有以下幾個重要的結構體類型 和 函數:

//創建
_Block_copy()
_Block_call_copy_helper()
_Block_object_assign()
//銷毀
_Block_release()
_Block_call_dispose_helper()
_Block_object_dispose()
//結構體
Block_layout
Block_descriptor_1
Block_descriptor_2
Block_descriptor_3
Block_byref
Block_byref_2
Block_byref_3

++++++++++++++++++++++end+++++++++++++++++++++++++++

block循環引用問題

循環引用的產生
先上一段代碼:

typedef void(^TestBock2)(void);
@interface SecondViewController ()
@property (nonatomic,copy) TestBock2 block;
@property (nonatomic,copy) NSString *name;
@end
@implementation SecondViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        NSLog(@"self.name == %@",self.name);
    };
    self.block();
}
- (void)dealloc
{
    NSLog(@"%s",__func__);
}

當我們這樣寫完之后其實編譯器已經提示警告了:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

這個寫法發生了一個循環引用,即當前self持有blockblock內部又持有了self(self -> block -> self),從而導致該頁面無法正常釋放而導致內存泄露問題。

解決循環引用
解決這個問題,我們需要做的就是掐斷這個循環引用鏈,要嘛處理掉selfblock的強引用,要嘛處理掉blockself的強引用。

方式一: 使用__weak來處理掉blockself的強引用

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"self.name == %@",weakSelf.name);
    };
    self.block();
}

這里利用了__weak的特性,當__weak修飾的變量,當指向的對象釋放時,該變量自動置nil
此時循環引用問題得到了解決,但是這里還是存在一個問題就是,當block內部如果執行的了一個耗時異步任務,比如下面這樣:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"zezefamily";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        //模擬延時任務
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"self.name == %@",weakSelf.name);
        });
    };
    self.block();
}

當我們進入頁面,退出時,耗時任務還沒執行完,但此時我們已經頁面已經dealloc了,這就導致了這個耗時任務是去了意義。

TestApp[154:9509667] -[SecondViewController dealloc]
TestApp[154:9509667] self.name == (null)

這里就需要我們來延長一下weakSelf的生命周期到延時任務的block執行完。通過__strong來延長生命周期:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"zezefamily";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        //模擬延時任務
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"self.name == %@",strongSelf.name);
        });
    };
    self.block();
}

此時delloc會在延時任務執行完之后才觸發

TestApp[281:9511903] self.name == zezefamily
TestApp[281:9511903] -[SecondViewController dealloc]

方式二: 上面我們利用了__weak自動置nil的特性,我們這里是不是也可以使用手動的方式去置空吶,比如下面這樣:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"zezefamily";
    __block SecondViewController *vc = self;
    self.block = ^{
        //模擬延時任務
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"self.name == %@",vc.name);
            vc = nil;
        });
    };
    self.block();
}

看打印結果:

TestApp[453:9514212] self.name == zezefamily
TestApp[453:9514212] -[SecondViewController dealloc]

其實這個方式的本質跟__weak是一樣的,只不過這里需要我們手動置nil

方式三: 以參數的方式傳遞給block,作為block的局部變量,在block執行完之后釋放

typedef void(^TestBock2)(SecondViewController *);
- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"zezefamily";
    self.block = ^(SecondViewController *vc){
        //模擬延時任務
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"self.name == %@",vc.name);
        });
    };
    self.block(self);
}

再看輸出:

TestApp[619:9516826] self.name == zezefamily
TestApp[619:9516826] -[SecondViewController dealloc]

同樣完美執行了delloc

方式四:NSProxy (待完善)

總結
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容