iOS進(jìn)階—— Block
花幾分鐘時間看下面三個小題目,寫下你的答案。
這個三個小題目,我在整理此片博文之前給了三位朋友去解答,***的結(jié)果,除了一位朋友 3 題全部正確,其他兩個朋友均只答中 1 題。
說明還是有很多 iOS 的朋友對于 Block 并沒有透徹理解。本篇博文會對 Block 進(jìn)行詳細(xì)的解說。
1 Block 使用的簡單規(guī)則
先了解簡單規(guī)則,再去分析原理和實(shí)現(xiàn):
Block 中,Block 表達(dá)式截獲所使用的自動變量的值,即保存該自動變量的瞬間值。
修飾為 __block 的變量,在捕獲時,獲取的不再是瞬間值。
至于 Why,后面將會繼續(xù)說。
2 Block 的實(shí)現(xiàn)
Block 是帶有自動變量(局部變量)的匿名函數(shù)。
Block 表達(dá)式很簡單,總體可以描述為:『^ 返回值類型 參數(shù)列表 表達(dá)式』。
但是 Block 并不是 Objective-C 中才有的語法,這是怎么一回事?
clang 編譯器提供給程序員了解 Objective-C 背后機(jī)制的方法,通過 clang 的轉(zhuǎn)換可以看到 Block 的實(shí)現(xiàn)原理。
通過 clang -rewrite-objc yourfile.m clang 將會把 Objective-C 的代碼轉(zhuǎn)換成 C 語言的代碼。
2.1 Block 基本實(shí)現(xiàn)剖析
用 Xcode 創(chuàng)建 Command Line 項(xiàng)目,寫如下代碼:
- int main(int argc, const char * argv[]) {
- void (^blk)(void) = ^{NSLog(@"Block")};
- blk();
- return 0;
- }
用 clang 轉(zhuǎn)換:
以上是轉(zhuǎn)換后的代碼,不要方,一段一段看。
可以看到,Block 內(nèi)部的內(nèi)容,被轉(zhuǎn)換成了一個普通的靜態(tài)函數(shù) __main_func_0。
再看其他部分:
main.cpp __block_impl:
- struct __block_impl {
- void *isa;
- int Flags;
- int Reserved;
- void *FuncPtr;
- };
__block_impl 結(jié)構(gòu)體包括了一些標(biāo)志、今后版本升級預(yù)留的變量、函數(shù)指針。
main.cpp __main_block_desc_0:
- 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)};
__main_block_desc_0 結(jié)構(gòu)體包括了今后版本升級預(yù)留的變量、block 大小。
main.cpp __main_block_impl_0:
__main_block_impl_0 結(jié)構(gòu)體含有兩個成員變量,分別是 __block_impl 和 __main_block_desc_0實(shí)例變量。
此外,還含有一個構(gòu)造方法。該構(gòu)造方法在 main 函數(shù)中被如下調(diào)用:
main.cpp __main_block_impl_0 構(gòu)造函數(shù)的調(diào)用:
- void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
- &__main_block_desc_0_DATA));
去掉各種強(qiáng)制轉(zhuǎn)換,做簡化:
main.cpp __main_block_impl_0 構(gòu)造函數(shù)的調(diào)用 簡化:
- struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
- struct __main_block_impl_0 *blk = &tmp;
以上代碼即:將 __main_block_impl_0 結(jié)構(gòu)體實(shí)例的指針,賦值給 __main_block_impl_0 結(jié)構(gòu)體指針類型的變量 blk。也就是我們最初的結(jié)構(gòu)體定義:
- void (^blk)(void) = ^{NSLog(@"Block");};
另外,main 函數(shù)中還有另外一段:
- ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉各種轉(zhuǎn)換:
- (*blk->impl.FuncPtr)(blk);
實(shí)際就是最初的:
- blk();
本節(jié)所有代碼在 block_implementation (https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory/ObjcMemory-Test-Code/block_implementation)中
2.2 Block 截獲外部變量瞬間值的實(shí)現(xiàn)剖析
2.1 中對最簡單的 無參數(shù) Block 聲明、調(diào)用 進(jìn)行了 clang 轉(zhuǎn)換。接下來再看一段『截獲自動變量』的代碼(可以使用命令 clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 main.m):
- int main(int argc, const char * argv[]) {
- int val = 10;
- const char *fmt = "val = %d\n";
- void (^blk)(void) = ^{printf(fmt, val);};
- val = 2;
- fmt = "These values were changed, val = %d\n";
- blk();
- return 0;
- }
clang 轉(zhuǎn)換之后:
和 2.1 節(jié)中的轉(zhuǎn)換代碼對比,可以發(fā)現(xiàn)多了一些代碼。
首先,__main_block_impl_0 多了一個變量 val,并在構(gòu)造函數(shù)的參數(shù)中加入了 val 的賦值:
main.cpp __main_block_impl_0:
- 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;
- }
- };
而在 main 函數(shù)中,對 Block 的聲明變?yōu)榇司洌?/p>
main.cpp __main_block_impl_0 構(gòu)造函數(shù)的調(diào)用:
- void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
去掉轉(zhuǎn)換:
main.cpp __main_block_impl_0 構(gòu)造函數(shù)的調(diào)用 簡化:
- struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, val);
- struct __main_block_impl_0 *blk = &tmp;
_所以,在 Block 被聲明時,Block 已經(jīng)將 val 作為 __main_block_impl_0 的內(nèi)部變量保存下來了。無論在在聲明之后怎樣更改 val 的值,都不會影響,Block 調(diào)用時訪問的內(nèi)部 val 值。這就是 Block 捕獲變量瞬間值的原理。_
本節(jié)所有代碼在 EX05 中
2.3 __block 變量的訪問實(shí)現(xiàn)剖析
我們知道,Block 中能夠讀取,但是不能更改一個局部變量,如果去更改,Xcode 會提示你無法在 Block 內(nèi)部更改變量。
Block 內(nèi)部只是對局部變量只讀,但是 Block 能讀寫以下幾種變量:
- 靜態(tài)變量
- 靜態(tài)全局變量
- 全局變量
也就是說以下代碼是沒有問題的:
- int global_val = 1;
- static int static_global_val = 2;
- int main(int argc, const char * argv[]) {
- static int static_val = 3;
- void (^blk)(void) = ^ {
- global_val = 1 * 2;
- static_global_val = 2 * 2;
- static_val = 3 * 2;
- }
- return 0;
- }
如果想在 Block 內(nèi)部寫局部變量,需要對訪問的局部變量增加 __block 修飾。
__block 修飾符其實(shí)類似于 C 語言中 static、auto、register 修飾符。用于指定將變量值設(shè)置到哪個存儲域中。
具體 __block 之后究竟做了哪些變化我們可以寫代碼測試:
EX07:
- int main(int argc, const char * argv[]) {
- __block int val = 10;
- void (^blk)(void) = ^{val = 1;};
- return 0;
- }
clang 轉(zhuǎn)換之后:
跟 2.2 對比,似乎又加了非常代碼。發(fā)現(xiàn)多了兩個結(jié)構(gòu)體。
main.cpp __Block_byref_val_0:
- struct __Block_byref_val_0 {
- void *__isa;
- __Block_byref_val_0 *__forwarding;
- int __flags;
- int __size;
- int val;
- };
很驚奇的發(fā)現(xiàn),block 類型的 val 變成了結(jié)構(gòu)體 Block_byref_val_0的實(shí)例。這個實(shí)例內(nèi),包含了isa指針、一個標(biāo)志位flags、一個記錄大小的size。最最重要的,多了一個forwarding指針和val 變量。這是怎么回事?
在 main 函數(shù)部分,實(shí)例化了該結(jié)構(gòu)體:
main.cpp main.m 部分:
- __Block_byref_val_0 val = {(void*)0,
- (__Block_byref_val_0 *)&val,
- 0,
- sizeof(__Block_byref_val_0),
- 10};
我們可以看出該結(jié)構(gòu)體對象初始化時:
- __forwarding 指向了結(jié)構(gòu)體實(shí)例本身在內(nèi)存中的地址
- val = 10
而在 main 函數(shù)中,val = 1 這句賦值語句變成了:
main.cpp val = 1; 對應(yīng)的函數(shù)
- (val->__forwarding->val) = 1;
這里就可以看出其精髓,val = 1,實(shí)際上更改的是 __Block_byref_val_0 結(jié)構(gòu)體實(shí)例 val 中的 __forwarding 指針(也就是本身)指向的 val 變量。
而對 val 訪問也是如此。你可以理解為通過取地址改變變量的值,這和 C 語言中取地址改變變量類似。
所以,聲明 block 的變量可以被改變。至于 forwarding 的其他巨大作用,會繼續(xù)分析。
本節(jié)代碼在 EX05 中
3 Block 的存儲域
Block 有三種類型,分別是:
- __NSConcreteStackBlock ————————棧中
- __NSConcreteGlobalBlock ————————數(shù)據(jù)區(qū)域中
- __NSConcreteMallocBlock ————————堆中
__NSConcreteGlobalBlock 出現(xiàn)的地方有:
- 設(shè)置全局變量的地方有 Block 語法時
- Block 語法的表達(dá)式中不使用任何外部變量時
設(shè)置在棧上的 Block,如果所屬的變量作用域結(jié)束,Block 就會被廢棄。如果其中用到了 block,block 所屬的變量作用域結(jié)束也會被廢棄。
為了解決這個問題,Block 在必要的時候就需要從棧中移到堆中。ARC 有效時,很多情況下,編譯器會幫助完成 Block 的 copy,但很多情況下,我們需要手動 copy Block。
對不同存儲域的 Block copy 時,影響如下:
copy 時,對訪問到的 __block 類型對象影響如下:
此時可以看出 __forwarding 的巨大作用——無論 Block 此時在堆中還是在棧中,由于 __forwarding 指向局部變量轉(zhuǎn)換成的結(jié)構(gòu)體實(shí)例的真是地址,所以都能確保正確的訪問。
具體的來說:
- 當(dāng) block 變量被一個 Block 使用時,Block 從棧復(fù)制到堆,block 變量也會被復(fù)制到,并被該 Block 持有。
- 在 block 變量被多個 Block 使用時,在任何一個 Block 從棧復(fù)制到堆時, block 變量也會被復(fù)制到堆,并被該 Block 持有。但由于 __forwarding 指針的存在,無論 block 變量和 Block 在不在同一個存儲域,都可以正確的訪問 block 變量。
- 如果堆上的 Block 被廢棄,那么它所使用的 __block 變量也會被釋放。
前面說到編譯器會幫助完成一些 Block 的 copy,也有手動 copy Block。那么 Block 被復(fù)制到堆上的情況有(此段摘自于『Objective-C高級編程 iOS與OS X多線程和內(nèi)存管理』):
- 調(diào)用 Block 的 copy 方法時
- Block 作為返回值時
- 將 Block 賦值給附有 __strong 修飾符的成員變量時(id類型或 Block 類型)時
- 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中傳遞 Block 時
4 Block 循環(huán)引用
Block 循環(huán)引用,是在編程中非常常見的問題,甚至很多時候,我們并不知道發(fā)生了循環(huán)引用,直到我們突然某一天發(fā)現(xiàn)『怎么這個對象沒有調(diào)用 delloc』,才意識到有問題存在。
在『Block 存儲域』中也說明了 Block 在 copy 后對 __block 對象會 retain 一次。
那么對于如下情況就會發(fā)生循環(huán)引用:
- block_retain_cycle:
- @interface MyObject : NSObject
- @property (nonatomic, copy) blk_t blk;
- @property (nonatomic, strong) NSObject *obj;
- @end
- @implementation MyObject
- - (instancetype)init {
- self = [super init];
- _blk = ^{NSLog(@"self = %@", self);};
- return self;
- }
- - (void)dealloc {
- NSLog(@"%@ dealloc", self.class);
- }
- @end
- int main(int argc, const char * argv[]) {
- id myobj = [[MyObject alloc] init];
- NSLog(@"%@", myobj);
- return 0;
- }
由于 self -> blk,blk -> self,雙方都無法釋放。
但要注意的是,對于以下情況,同樣會發(fā)生循環(huán)引用:
- block_retain_cycle
- @interface MyObject : NSObject
- @property (nonatomic, copy) blk_t blk;
- // 下面是多加的一句
- @property (nonatomic, strong) NSObject *obj;
- @end
- @implementation MyObject
- - (instancetype)init {
- self = [super init];
- // 下面是多加的一句
- _blk = ^{NSLog(@"self = %@", _obj);};
- return self;
- }
- - (void)dealloc {
- NSLog(@"%@ dealloc", self.class);
- }
- @end
- int main(int argc, const char * argv[]) {
- id myobj = [[MyObject alloc] init];
- NSLog(@"%@", myobj);
- return 0;
- }
這是由于 self -> obj,self -> blk,blk -> obj。這種情況是非常容易被忽視的。
5 重審問題
我們再來看看最初的幾個小題目:
***題:
由于 Block 捕獲瞬間值,所以輸出為 in block val = 0
第二題:
由于 val 為 __block,外部更改會影響到內(nèi)部訪問,所以輸出為 in block val = 1
第三題:
和第二題類似,val = 1 能影響到 Block 內(nèi)部訪問,所以先輸出 in block val = 1,之后在 Block 內(nèi)部更改 val 值,再次訪問時輸出 after block val = 2。
Other
我寫這篇文章是在我閱讀了『Objective-C高級編程 iOS與OS X多線程和內(nèi)存管理』一書之后,博文中也有很內(nèi)容源于『Objective-C高級編程 iOS與OS X多線程和內(nèi)存管理』。
非常向大家推薦此書。這本書里記錄了關(guān)于 iOS 內(nèi)存管理的深入內(nèi)容。但要注意的是,此書中的多處知識點(diǎn)并不是很詳細(xì),需要你以拓展的心態(tài)去學(xué)習(xí)。在有解釋不詳細(xì)的地方,自己主動去探索,去拓展,找更多的資料,***,你會發(fā)現(xiàn)你對 iOS 內(nèi)存管理有了更多的深入的理解。
對于文章中的測試代碼,全部在(https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory)。