Objective-C block 實現機制

chenjiang3發表於2018-03-22

前言

在Objective-C中,block是一個很常見的東西,說白了就是個匿名函式,網上有很多關於block如何使用的文章,講的都非常精彩,這裡主要探討下block的實現原理。關於如何使用block,請參考網上的教程。

例項

先來新建一個控制檯工程,main.m裡的程式碼如下,並思考下最後的輸出結果是什麼:

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

void blockFunc2() {
    __block int num = 100;
    void (^block)(void) = ^() {
        NSLog(@"num = %d
", num);
    };
    num = 200;
    block();
}

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

void blockFunc4()
{
    static int num = 100;
    void (^block)(void) = ^{
        NSLog(@"num = %d", num);
    };
    num = 200;
    block();
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        ^{ printf("Hello, World!
"); } ();
        
        blockFunc1();
        blockFunc2();
        
        blockFunc3();
        blockFunc4();
    }
    return 0;
}

列印結果是:

Hello, World!
2018-03-21 15:50:01.996591+0800 BlockDemo[34825:4848536] num = 100
2018-03-21 15:50:01.997009+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997025+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997037+0800 BlockDemo[34825:4848536] num = 200

聰明的讀者應該早就知道這個結果,?,先簡單解釋一下:
1、^{ printf("Hello, World!
"); } ();

這個沒啥好說的,肯定就列印“Hello, World!”了。

2、blockFunc1裡面,num是以值傳遞的方式被block獲取,所以儘管後面更改了num的值,但是在block裡面還是保持保持原來的值。

2、blockFunc2裡面,num由__block修飾,num在block變成了外部的一個引用(後面會通過原始碼解釋),所以在block外部改變num的值時,block裡面的num也隨著改變。

3、blockFunc3裡面,block引用的是一個全域性的num,所以,num改變的時候也會改變block內部num的值。

4、blockFunc3裡面,block引用的是一個static的num,所以,num改變也會改變block內部的num的值。

原始碼分析

也許大家看到上面的解釋還是不知道為啥會這樣,所以接下,我通過原始碼來分析下其中的緣由,我們先把這段先轉換成c++檔案,cd到main.m所在的目錄,並執行這條命令clang -rewrite-objc main.m,通過這條命令可以把main.m檔案轉換成cpp檔案,裡面可以看到block的結構。我們開啟這份檔案,這個檔案比較長,直接拉到最後。可以看到在檔案的最後是main函式的入口,程式碼如下:

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

        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();

        blockFunc1();
        blockFunc2();

        blockFunc3();
        blockFunc4();
    }
    return 0;
}

先看第一行程式碼,構造了一個__main_block_impl_0物件,__main_block_impl_0是一個結構體。相關程式碼如下:

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

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

從程式碼可知,最後block會轉化成一個__block_impl物件,而block執行的程式碼會轉化成一個靜態函式,__block_impl裡面的FuncPtr會指向這個靜態函式。在這裡printf("Hello, World!
");
這個block轉換後的靜態函式如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("Hello, World!
"); 
}

所以整個過程是這樣的:
1、先構造一個__main_block_impl_0物件,構造的時候把__main_block_func_0傳進去,當然還有別的引數,這裡先不考慮。

2、在__main_block_impl_0的構造方法中,再把__main_block_func_0賦給__block_impl的FuncPtr。

3、呼叫FuncPtr。

所以,從上面可以看出,block實際上是轉化為了一個__block_impl物件,這個物件有isa指標,用來表示block的型別,上面的block的isa指向&_NSConcreteStackBlock。同時block物件還有一個FuncPtr指標,用來指向block執行的方法(轉換後的靜態函式)。

再來看看blockFunc1相關的內容

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

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

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_0, num);
}
    
void blockFunc1() {
    int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

在這個函式裡面,構造block的時候把num傳了進去,而且是普通值傳遞,這樣的話其實是拷貝了一份num。然後在執行block方法的時候,使用的是拷貝的那份num,從int num = __cself->num; // bound by copy可以看出。這個block也是_NSConcreteStackBlock型別的。

再來看看__block修飾過的num在block裡面是怎麼傳遞的,我們看看blockFunc2相關的程式碼:

// 封裝num的結構
struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __blockFunc2_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc2_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_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 __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
    __Block_byref_num_0 *num = __cself->num; // bound by ref
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_1, (num->__forwarding->num));
}
    
void blockFunc2() {
    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
    void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    (num.__forwarding->num) = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

從程式碼中可以看到,__block修飾的num在內部被包裝成一個__Block_byref_num_0的物件,假設叫a,原來num的值100儲存在物件a的num欄位中,同時這個物件a有一個__forwarding欄位,指向a本身。當改變num的值的時候(原始碼是num = 200;),這段程式碼變為(num.__forwarding->num) = 200;,也就是說把物件a裡面的num欄位的值變為了200。同時,在block的執行函式__blockFunc2_block_func_0中,列印出來的取值是從__Block_byref_num_0 *num = __cself->num;取出,也就是取得是改變後的值,所以列印結果是200。這就是為什麼用__block修飾的變數可以在block內部被修改。

那當num為全域性變數的時候,block又是怎樣的呢?請看程式碼:

static void __blockFunc3_block_func_0(struct __blockFunc3_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_2, num);
    }
    
void blockFunc3() {
    void (*block)(void) = ((void (*)())&__blockFunc3_block_impl_0((void *)__blockFunc3_block_func_0, &__blockFunc3_block_desc_0_DATA));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

從程式碼裡可以看出,這種情況很簡單,block物件根本沒有num欄位,也就是列印的時候直接取得全域性的num。

最後一種情況也很簡單,當num時static的時候,構造block物件的時候直接用引用傳值的方式把num放到block物件中。所以,當外部改變num的值的時候,也能反映到block內部。程式碼如下:

void blockFunc4()
{
    static int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

總結

這篇文章主要從原始碼的角度講述了block的實現機制,並針對四種情況分析了block是如何引用外部變數的,分別是:
1、當引用區域性變數的時候,如果沒有__block修飾,那麼在block內部獲取的是外部變數的一份拷貝,改變外部變數不影響block內部的那份拷貝。

2、當引用區域性變數的時候,同時區域性變數用__block修飾,那麼在block內部使用的實際上是外部變數的一個引用,所以改變外部變數會影響block內部變數的值。

3、當引用全域性變數的時候,block並不持有這個變數。

4、當引用static變數的時候,block會以引用的方式持有這個變數。當在外部修改這個變數的時候,會影響block內部持有的這個變數的值。

相關文章