iOS中使用Clang查看Block(Closure)的實(shí)現(xiàn)

什么是Block

Apple文檔說:“A block is an anonymous inline collection of code, and sometimes also called a "closure".

一個(gè)block是一個(gè)匿名內(nèi)聯(lián)代碼的集合,有時(shí)候也叫”Closure“。

block提供了一種新的方式進(jìn)行回調(diào),并且用block進(jìn)行回調(diào)還可以直接訪問局部變量,這是一般的函數(shù)做不到的。

Bblock的幾種適用場(chǎng)合:任務(wù)完成時(shí)回調(diào)處理,消息監(jiān)聽回調(diào)處理,錯(cuò)誤回調(diào)處理,枚舉回調(diào),視圖動(dòng)畫、變換,排序。

寫一個(gè)簡(jiǎn)單的Block

在開發(fā)中使用Block的地方很多,最常見的是UIView的animation(**)動(dòng)畫,以及GCD。使用Block的時(shí)候最需要注意的是內(nèi)存泄漏,在block中使用的局部變量需要用__block修飾;防止循環(huán)引用的時(shí)候,要使用__weak修飾的對(duì)象。

使用C寫一個(gè)Block:打開終端,輸入:

vi test.c

然后鍵入i開始插入代碼:

#include <stdio.h>
int main(){
void(^blk)(void) = ^{
  printf("Block\n");
};
blk();
return 0;
}

鍵入esc,鍵入:,鍵入wq

得到文件test.c,可在??家目錄看到此文件。

查看實(shí)現(xiàn)代碼

然后在終端中鍵入gcc test.c,得到編譯后的輸出文件 a.out。
終端中鍵入clang -rewrite-objc test.c,得到C++文件test.cpp
雙擊使用Xcode打開此文件,就是實(shí)現(xiàn)代碼了。
看到了兩個(gè)熟悉的面孔:

weak

此文件有近600行代碼,我們只看自己和本次研究相關(guān)的代碼。

分析代碼

首先在CPP文件中找到main函數(shù),對(duì)應(yīng)我們寫的代碼:


CPP代碼對(duì)比

兩行代碼相對(duì)應(yīng)。

  1. 第一行:
void(*blk)(void) =
    ((void (*)()) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) );

通過__main_block_impl_0創(chuàng)建了一個(gè)變量blk。
__main_block_impl_0是一個(gè)struct,包含了 __block_impl 的 impl。

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

可以看到,在構(gòu)造函數(shù)里有三個(gè)參數(shù),一個(gè)是函數(shù)指針,一個(gè)是desc,一個(gè)flags。
注意到:isa指針指向了_NSConcreteStackBlock,把傳進(jìn)來的函數(shù)指針fp記錄到了impl里的FuncPtr。
_NSConcreteStackBlock是Block的類型,共有三種:

  1. _NSConcreteGlobalBlock(全局)
  2. _NSConcreteStackBlock(棧)
  3. _NSConcreteMallocBlock(堆)

虛擬內(nèi)存段的分布圖如下:


分布圖

這里不對(duì)類型進(jìn)行深入研究,不過最后一種是私有,基本不會(huì)遇到。第一種類型出現(xiàn)的情況是定義了一個(gè)全局的Block,比如:

void (^globalBlock)() = ^{
};

int main(int argc, const char * argv[]) {
    return 0;
}

書歸正傳,接著討論。傳進(jìn)來的函數(shù)指針fp記錄到了impl里的FuncPtr,這個(gè)impl變量是__block_impl結(jié)構(gòu)體,查看這個(gè)struct:

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

回到CPP里main函數(shù)里的第一行代碼,在調(diào)用這一函數(shù)的時(shí)候,傳入的函數(shù)指針參數(shù)是

__main_block_func_0

找到這個(gè)函數(shù):

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  printf("Block\n");
}

這正是我們寫進(jìn)block里的代碼,也就是說__main_block_impl_0要執(zhí)行的就是這一段代碼。

這一行,只是創(chuàng)建了blk,并未執(zhí)行。

  1. 第二行
    這一行代碼就是執(zhí)行這個(gè)Block里面的內(nèi)容。
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

調(diào)用了blk的FuncPtr(__block_impl *)blk)->FuncPtr,即執(zhí)行了__main_block_func_0函數(shù),輸出了"Block\n"這一字符串.

寫一個(gè)閉包捕獲

我們大多數(shù)情況需要在閉包里訪問閉包外的局部變量、全局變量、靜態(tài)變量。想一下,閉包如果捕獲了局部變量,會(huì)出現(xiàn)什么問題?
先寫一個(gè)捕獲變量的block代碼:
如先前所述方法再創(chuàng)建一個(gè)文件testBlock.c:

#include <stdio.h>
int main(){
    int a = 2;
    void(^blk)(void) = ^{
        a = 3;
        printf("Block\n");
    };
    blk();
    return 0;
}

代碼有問題嗎?
終端鍵入:gcc testBlock.c編譯一下,出錯(cuò)了:

出錯(cuò)了

看到錯(cuò)誤提示:

error: variable is not assignable (missing __block type
specifier)

是的,我們需要添加__block來說明這個(gè)變量是需要在block中使用的。
為什么要添加這個(gè)說明符?是如何作用的?我們大概猜一下,應(yīng)該是更改了這個(gè)變量的作用域,把它從棧挪到了堆中。
寫正確的代碼繼續(xù)實(shí)驗(yàn),更改代碼為:

#include <stdio.h>
int main(){
    __block int a = 2;
    void(^blk)(void) = ^{
        a = 3;
        printf("Block\n");
    };
    blk();
    return 0;
}

繼續(xù)編譯,成功。
使用Clang來獲得實(shí)現(xiàn)代碼,得到的C++的代碼testBlock.cpp。
仍然先找到main入口函數(shù):

int main(){
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 2};
    void(*blk)(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 *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

代碼太長(zhǎng),擠得太密,斷個(gè)句,放個(gè)圖:


main()

emm...多了些。
先找到熟悉的代碼,第一行是定義了一個(gè)__block修飾的變量a。
第二行就是剛才所見的第一行的內(nèi)容,創(chuàng)建一個(gè)Block的blk,第三行就是執(zhí)行。

我們先研究第一行,首先注意到的是__Block_byref_a_0這個(gè)結(jié)構(gòu)體:

__Block_byref_a_0

__isa指針,也是個(gè)對(duì)象。發(fā)現(xiàn)有一個(gè)同樣是__Block_byref_a_0類型的變量__forwarding,發(fā)現(xiàn)了古怪:

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 2}

__forwarding竟然指向了自己,所以以后這個(gè)東西改變會(huì)指向誰呢?
研究第二行,創(chuàng)建block的時(shí)候,多了兩個(gè)參數(shù),一個(gè)是(__Block_byref_a_0 *)&a,即需要捕獲的變量a,還有一個(gè)參數(shù)570425344,什么鬼?
它是一個(gè)標(biāo)記值,570425344的值是1<<29,即BLOCK_HAS_DESCRIPTOR這個(gè)枚舉值。Soga,原來是標(biāo)記了“閉包有描述符”。

原來如此

這時(shí)候注意__main_block_desc_0_DATA,變了嘿,多了兩個(gè)參數(shù)__main_block_copy_0, __main_block_dispose_0

__main_block_desc_0_DATA

看看什么東東:

copy

分別調(diào)用了_Block_object_assign_Block_object_dispose函數(shù),追下去,看看什么:
dllexport

看到了__declspec(dllexport),__declspec(dllexport)用于動(dòng)態(tài)庫(kù)中,聲明導(dǎo)出函數(shù)、類、對(duì)象等供外面調(diào)用。
我找了下資料,這就到了Objc的Block源代碼中,有這么一個(gè)實(shí)現(xiàn),截取有用的代碼:

// _Block_object_assign源碼
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
...
    else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
        // copying a __block reference from the stack Block to the heap
        // flags will indicate if it holds a __weak reference and needs a special isa
        _Block_byref_assign_copy(destAddr, object, flags);
    }
...
}

注意到了_Block_byref_assign_copy(destAddr, object, flags);,注釋為copying a __block reference from the stack Block to the heap.果不其然,把__block標(biāo)注的引用從棧拷貝到了堆中。
查看_Block_byref_assign_copy(destAddr, object, flags);方法如下:

// _Block_byref_assign_copy源碼
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
...
    else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 從main函數(shù)對(duì)__Block_byref_a_0的初始化,可以看到初始化時(shí)將flags賦值為0
        // 這里表示第一次拷貝,會(huì)進(jìn)行復(fù)制操作,并修改原來flags的值
        // static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;
        // 可以看出,復(fù)制后,會(huì)并入BLOCK_NEEDS_FREE,后面的2是block的初始引用計(jì)數(shù)
        ...
        copy->flags = src->flags | _Byref_flag_initial_value;
        ...
    }
    // 已經(jīng)拷貝到堆了,只增加引用計(jì)數(shù)
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // 普通的賦值,里面最底層就*destptr = value;這句表達(dá)式
    _Block_assign(src->forwarding, (void **)destp);
}

由于block的拷貝最終都會(huì)調(diào)用_Block_copy_internal函數(shù),所以觀察這個(gè)函數(shù)就可以知道堆中block是如何被創(chuàng)建的了:

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    ...
    aBlock = (struct Block_layout *)arg;
    ...
    // Its a stack block.  Make a copy.
    if (!isGC) {
        // 申請(qǐng)block的堆內(nèi)存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // 拷貝棧中block到剛申請(qǐng)的堆內(nèi)存中
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
        // 改變isa指向_NSConcreteMallocBlock,即堆block類型
        result->isa = _NSConcreteMallocBlock;
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        ...
    }
}

從以上代碼以及注釋可以很清楚的看出,函數(shù)通過memmove將棧中的block的內(nèi)容拷貝到了堆中,并使isa指向了_NSConcreteMallocBlock。
block主要的一些學(xué)問就出在棧中block向堆中block的轉(zhuǎn)移過程中了。

書歸正傳,回到咱們通過clang得到的C++實(shí)現(xiàn)代碼中來,調(diào)用的函數(shù)指針指向的方法__main_block_func_0為:

__main_block_func_0

注意到(a->__forwarding->a) = 3;,這時(shí)候修改a的值的時(shí)候是修改的__forwarding的值,即已經(jīng)是堆中的值。

這時(shí)候,就不會(huì)出現(xiàn)block回調(diào)的時(shí)候,局部變量已經(jīng)釋放的問題了,因?yàn)橐呀?jīng)“捕獲”到了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容