iOS 揭露Block的內部實現原理

LinXunFeng發表於2017-12-14

想必大家對block都很熟悉了,雖然都會用,但是你真的知道它的原理嗎?比如為什麼要加上__block,這個修飾符到底有什麼用?不加會有什麼後果?block又是如何實現的等等。。。該篇文章就為大家揭曉關於Block的實現原理~

拋磚引玉

先給出問題,大家思考下結果吧,如果分別呼叫以下兩個方法,結果如何?

void blockFunc1()
{
    int num = 100;
    void (^block)() = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}
複製程式碼
void blockFunc2()
{
    __block int num = 100;
    void (^block)() = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}
複製程式碼

答案是

blockFunc1 : num equal 100
blockFunc2 : num equal 200
複製程式碼

是不是有人答錯了?再來兩個函式。這兩個的結果與blockFunc2一樣,列印出來的 num 為 200

// 全域性變數
int num = 100;
void blockFunc3()
{
    void (^block)() = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}
複製程式碼
void blockFunc4()
{
    static int num = 100;
    void (^block)() = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}
複製程式碼

疑問: 我們發現num做為區域性變數時加上 _ _block 修飾符、num做為全域性變數以及num為靜態區域性變數時在block中輸出結果是一樣的,皆為被修改之後的值,而做為區域性變數並且未加上__block的num在block中輸出的值卻還是未賦值之前的值。這是為什麼呢?探索這個問題我們就需要看看底層結構是如何實現的了

探索內部原理

Objective-C是一個全動態語言,它的一切都是基於runtime實現的!在執行時會將OC轉換成C,我們可以利用這個來檢視關於block在內部是如何實現的 新建一個Command Line Tool專案,將以上程式碼放入main.m中,如圖

main.m

這裡我們開啟終端,cd到專案目錄下,然後將用下面的命令將OC重寫為C

clang -rewrite-objc main.m
複製程式碼

rewrite-objc
這時我們可以發現當前目錄下多了一個main.cpp檔案,開啟它並滾到最下面
開啟main.cpp

main.cpp
這裡我們可以看到blockFunc1的C語言實現方法

void blockFunc1()
{
    int num = 100;
    void (*block)() = ((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);
}
複製程式碼

去掉型別轉換

void blockFunc1()
{
    int num = 100;
    // *************************重點句***********************
    void (*block)() = &__blockFunc1_block_impl_0(__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
    // *****************************************************
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
複製程式碼

這裡我們可以看到

block實際上是指向結構體的指標

該結構體為

__blockFunc1_block_impl_0

我們來看下帶__block的blockFunc2

blockFunc2
在 blockFunc1 中,block指向了一個名為__blockFunc1_block_impl_0的結構體,並且在初始化時輸入了三個引數(__blockFunc1_block_impl_0最後的flags有預設引數,所以可以不用傳參),第三個引數就是我們寫的num,與blockFunc2相比較,這裡的num並沒有帶*號,所以說在這裡它只是傳值而非傳址,而下面的【num = 200;】也就沒什麼卵用了。這就是blockFunc2、blockFunc3與blockFunc4為什麼能列印出num改變後的值,而blockFunc1不行的原因。

iOS   揭露Block的內部實現原理

在這裡我們也可以看出:

編譯器會將block的內部程式碼生成對應的函式

** SO **

我們總結下,block在內部會作為一個指向結構體的指標,當呼叫block的時候其實就是根據block對應的指標找到相應的函式,進而進行呼叫,並傳入自身

__block的實現

我們再來看看 _ block, _block也被轉換成了結構體,並含有5個變數

struct __Block_byref_num_0 {
  void *__isa;  // isa指標
__Block_byref_num_0 *__forwarding;  // 例項本身
 int __flags; 
 int __size;
 int num;  // 我們的num值
};
複製程式碼

iOS   揭露Block的內部實現原理
圖片對應著blockFunc2中的

__block int num = 100;
複製程式碼

當建立num並用__block修飾的時候,會初始化這五個變數 當我們執行

num = 200;
複製程式碼

對應著

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

上面剛剛提到過 _ _forwarding是例項本身,即型別結構體__Block_byref_num_0的&num,再找到對應的num變數,將其原來的100修改為200~~

到此,關於Block內部實現的揭曉也就到此結束了,希望本文能讓你對block有更深的理解,感謝你耐心的閱讀!

相關文章