iOS中block介紹(三)揭開神祕面紗(上)
2013-07-19 14:00 佚名 dreamingwish 字號:T | T
上一篇我們總結了各個情況下,block及其引用到的內存位置情況。接下來幾篇,我們將剖析編譯器轉碼以及運行時庫源碼來一探block的究竟。
block到底是什麼
我們使用clang的rewrite-objc命令來獲取轉碼後的代碼。
1、block的底層實現
我們來看看最簡單的一個block:
這個block僅僅打印棧變量i和j的值,其被clang轉碼爲:
首先是一個結構體__main_block_impl_0(從圖二中的最後一行可以看到,block是一個指向__main_block_impl_0的指針,初始化後被類型強轉爲函數指針),其中包含的__block_impl是一個公共實現(學過c語言的同學都知道,__main_block_impl_0的這種寫法表示其可以被類型強轉爲__block_impl類型):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa指針說明block可以成爲一個objc對象。
__main_block_impl_0的意思是main函數中的第0個block的implementation,這就是這個block的主體了。
這個結構體的構造函數的參數:
block實際執行代碼所在的函數的指針,當block真正被執行時,實際上是調用了這個函數,其命名也是類似的方式。
block的描述結構體,注意這個結構體聲明結束時就創建了一個唯一的desc,這個desc包含了block的大小,以及複製和析構block時需要額外調用的函數。
接下來是block所引用到的變量們
最後是一個標記值,內部實現需要用到的。(我用計算器看了一下,570425344這個值等於1<<29,即BLOCK_HAS_DESCRIPTOR這個枚舉值)
所以,我們可以看到:
爲什麼上一篇我們說j已經不是原來的j了,因爲j是作爲參數傳入了block的構造函數,進行了值複製。
帶有__block標記的變量會被取地址來傳入構造函數,爲修改其值奠定了基礎
接下來是block執行函數__main_block_func_0:
其唯一的參數是__main_block_impl_0的指針,我們看到printf語句的數據來源都取自__cself這個指針,比較有意思的是i的取值方式(帶有__block標記的變量i被轉碼爲一個結構體),先取__forward指針,再取i,這爲將i複製到堆中奠定了基礎。
再下來是預定義好的兩個複製/釋放輔助函數,其作用後面會講到。
最後是block的描述信息結構體 __main_block_desc_0,其包含block的內存佔用長度,已經複製/釋放輔助函數的指針,其聲明結束時,就創建了一個名爲__main_block_desc_0_DATA的結構體,我們看它構造時傳入的值,這個DATA結構體的作用就一目瞭然了:
長度用sizeof計算,輔助函數的指針分別爲上面預定義的兩個輔助函數。
注意,如果這個block沒有使用到需要在block複製時進行copy/retian的變量,那麼desc中不會有輔助函數
至此,一個block所有的部件我們都看齊全了,一個主體,一個真正的執行代碼函數,一個描述信息(可能包含兩個輔助函數)。
2、構造一個block
我們進入main函數:
圖一中的第三行(block的聲明),在圖二中,轉化爲一個函數指針的聲明,並且都沒有被賦予初始值。
而圖一中的最後一行(創建一個block),在圖二中,成爲了對__main_block_impl_0的構造函數的調用,傳入的參數的意義上面我們已經講過了。
所以構造一個block就是創建了__main_block_impl_0 這個c++類的實例。
3、調用一個block
調用一個block的寫法很簡單,與調用c語言函數的語法一樣:
blk();
其轉碼後的語句:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
將blk這個函數指針類型強轉爲__block_impl類型,然後取其執行函數指針,然後將此指針類型強轉爲返回void*並接收一個__block_impl*的函數指針,最後調用這個函數,傳入強轉爲__block_impl*類型的blk,
即調用了前述的函數__main_block_func_0
4、objective-c類成員函數中的block
源碼如下:
- (void)of1 { OBJ1* oj = self; void (^oblk)(void) = ^{ printf("%d\n", oj.oi);}; Block_copy(oblk); }
這裏我故意將self賦值給oj這個變量,是爲了驗證前一章提出的一個結論:無法通過簡單的間接引用self來防止retain循環,要避免循環,我們需要__block標記(多謝樓下網友的提醒)
轉碼如下:
struct __OBJ1__of1_block_impl_0 { struct __block_impl impl; struct __OBJ1__of1_block_desc_0* Desc; OBJ1 *oj; __OBJ1__of1_block_impl_0(void *fp, struct __OBJ1__of1_block_desc_0 *desc, OBJ1 *_oj, int flags=0) : oj(_oj) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __OBJ1__of1_block_func_0(struct __OBJ1__of1_block_impl_0 *__cself) { OBJ1 *oj = __cself->oj; // bound by copy printf("%d\n", ((int (*)(id, SEL))(void *)objc_msgSend)((id)oj, sel_registerName("oi")));}
objc方法中的block與c中的block並無太多差別,只是一些標記值可能不同,爲了標記其是objc方法中的blcok。
注意其構造函數的參數:OBJ1 *_oj
這個_oj在block複製到heap時,會被retain,而_oj與self根本就是相等的,所以,最終retain的就是self,所以如果當前實例持有了這個block,retain循環就形成了。
而一旦爲其增加了__block標記:
- (void)of1 { __block OBJ1 *bSelf = self; ^{ printf("%d", bSelf.oi); }; }其轉碼則變爲: //增加了如下行 struct __Block_byref_bSelf_0 { void *__isa; __Block_byref_bSelf_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); OBJ1 *bSelf; }; static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); } //聲明處變爲 __block __Block_byref_bSelf_0 bSelf = {(void*)0,(__Block_byref_bSelf_0 *)&bSelf, 33554432, sizeof(__Block_byref_bSelf_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, self};
clang爲我們的bSelf結構體創建了自己的copy/dispose輔助函數,33554432(即1<<25 BLOCK_HAS_COPY_DISPOSE)這個值告訴系統,我們的bSelf結構體具有copy/dispose輔助函數。
而131這個參數(二進制1000 0011,即BLOCK_FIELD_IS_OBJECT (3) |BLOCK_BYREF_CALLER(128))
中的BLOCK_BYREF_CALLER在內部實現中告訴系統不要進行retain或者copy,
也就是說,在 __block bSelf 被複制至heap上時,系統會發現有輔助函數,而輔助函數調用後,並不retain或者copy 其結構體內的bSelf。
這樣就避免了循環retain。
小結:
當我們創建一個block,並調用之,編譯器爲我們做的事情如下:
1.創建block所有的部件代碼:一個主體,一個真正的執行代碼函數,一個描述信息(可能包含兩個輔助函數)。
2.將我們的創建代碼轉碼爲block_impl的構造語句。
3.將我們的執行語句轉碼爲對block的執行函數的調用。
下一篇我們將剖析runtime.c的源碼,並理解block的堆棧轉換。