Objective-C block 實現機制

前言

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

實例

先來新建一個控制檯工程,main.m裏的代碼以下,並思考下最後的輸出結果是什麼:函數

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

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

int num = 100;
void blockFunc3() {
    void (^block)(void) = ^() {
        NSLog(@"num = %d\n", 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!\n"); } ();
        
        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

聰明的讀者應該早就知道這個結果,?,先簡單解釋一下:
一、^{ printf("Hello, World!\n"); } ();
這個沒啥好說的,確定就打印「Hello, World!」了。指針

二、blockFunc1裏面,num是以值傳遞的方式被block獲取,因此儘管後面更改了num的值,可是在block裏面仍是保持保持原來的值。code

二、blockFunc2裏面,num由__block修飾,num在block變成了外部的一個引用(後面會經過源碼解釋),因此在block外部改變num的值時,block裏面的num也隨着改變。對象

三、blockFunc3裏面,block引用的是一個全局的num,因此,num改變的時候也會改變block內部num的值。教程

四、blockFunc3裏面,block引用的是一個static的num,因此,num改變也會改變block內部的num的值。get

源碼分析

也許你們看到上面的解釋仍是不知道爲啥會這樣,因此接下,我經過源碼來分析下其中的原因,咱們先把這段先轉換成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是一個結構體。相關代碼以下:it

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!\n");這個block轉換後的靜態函數以下:

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

因此整個過程是這樣的:
一、先構造一個__main_block_impl_0對象,構造的時候把__main_block_func_0傳進去,固然還有別的參數,這裏先不考慮。

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

三、調用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是如何引用外部變量的,分別是:
一、當引用局部變量的時候,若是沒有__block修飾,那麼在block內部獲取的是外部變量的一份拷貝,改變外部變量不影響block內部的那份拷貝。

二、當引用局部變量的時候,同時局部變量用__block修飾,那麼在block內部使用的其實是外部變量的一個引用,因此改變外部變量會影響block內部變量的值。

三、當引用全局變量的時候,block並不持有這個變量。

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

原文連接:http://it9012.com/2019/02/27/...

相關文章
相關標籤/搜索