Object-C高階程式設計讀書筆記(3)——Block的變數擷取
阿新 • • 發佈:2019-02-13
之前我們對於Block的定義為 “帶有自動變數值的匿名函式”。通過前面的介紹,知道了Block能夠保持傳入其中的變數的值,即使在Block外部這些傳入的值已經結束了其作用域,但是在Block被呼叫時,仍能夠在Block內部獲取到這些外部變數的值。
如下面的程式碼
int main(){
int val = 10;
const char *fmt = "val = %d\n";
void(^blk)(void) = ^{printf(fmt, val);}
blk();
return 0;
}
在這裡Block的外部變數fmt,與val被傳入到Block的執行函式體中,雖然在這裡這些變數並未失效,但設想若是在complete型別的Block中,當Block被回撥來時,fmt和val可能已經過期,但是在Block中仍保持其值,可以打印出 val = 10;
Block對於變數的擷取
這是怎麼實現的呢?不廢話,同樣利用clang將OC程式碼轉換為C++實現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; const char *fmt; int val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; // bound by copy int val = __cself->val; // bound by copy printf(fmt, val);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int val = 10; const char *fmt = "val = %d\n"; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
同上一篇中所述類似,Block物件被轉換為了__main_block_impl_0結構體型別,其成員也與上一篇中介紹的大致相同,但是卻多了兩個成員變數const char* fmt, 與int val。再看其建構函式及mian函式中呼叫建構函式的地方,可知,這兩個成員變數正好對應儲存了(按值儲存)Block外部傳進來的變數。 如此一來,只要Block物件不釋放,則這兩個值會一直跟隨著Block物件。再細看Block的函式體
__main_block_func_0的引數是__cself,也就是Block物件自身的指標,類似於在呼叫C++類方法時,其函式第一個引數會隱式的預設為this一樣。 在函式體內,則用了其自身的成員變數fmt,val作為函式執行時用到的變數。static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; // bound by copy int val = __cself->val; // bound by copy printf(fmt, val);}
結論
結合上一篇中揭露的Block物件本質是一個OC類物件,那麼Block物件外部傳入變數的處理其實是: Block物件會將外部傳入的值按照值傳遞的方式儲存在自身成員變數中 這就難怪當外部變數過期釋放後,在Block中仍能夠訪問到變數的值,因為在Block中始終保留著對外部變數的一個值拷貝。一個有趣的問題
這裡說到,對於外部傳進的變數,Block內部都有一份值拷貝。同樣的,對於指標型別也是值拷貝。也就是說,對於傳入的指標變數,Block內部也會相應的指向同一塊記憶體,而非另闢一塊記憶體來儲存相同的資料。那麼當外部指標所指向的記憶體失效後,Block內部的指標,是否還能夠在這塊記憶體上獲得正確的值呢? 我們這裡來做個實驗,如下程式碼typedef void (^myBlock)(void);
void initBlock(myBlock *blk)
{
int a = 12;
int *v = &a;
printf("In function v=%d &v = %d *v=%d\n",v, &v, *v);
*blk = ^{
printf("In block v = %d &v = %d *v = %d\n",v, &v, *v);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^blk)(void) = nil;
initBlock(&blk);
blk();
}
return 0;
}
在函式initBlock中接收了一個Block物件指標,同時用指向函式區域性變數的指標V來初始化Block中的指標V(這裡按值拷貝,Block中的V與Block外的V指向同一塊記憶體)。當initBlock函式執行完畢,其指向的&a記憶體失效。我們看下輸出結果
In function v=1606416420 &v = 1606416408 *v=12
In block v = 1606416420 &v = 1049040 *v = 1
果然,雖然Block中的v仍在指向1606416420這塊地址,但是在函式外面,該地址資料已經失效,輸出一個1。同時我們可以注意到,Block中的v與Block外的v在記憶體中的地址是不一樣的,這正也說明了傳入Block中的變數,其實是在Block中的一個值拷貝的事實。