Objective-C高級編程讀書筆記三部曲已經(jīng)寫完, 另外兩篇如下 :
Objective-C高級編程讀書筆記之內(nèi)存管理
Objective-C高級編程讀書筆記之GCD
Blocks
這里有五道關(guān)于block的測試題, 大家可以去做做測試看看自己對block了解多少.
目錄
- Block的定義
- Block有哪幾種類型
- Block特性
- __block修飾符
- block調(diào)用copy方法的內(nèi)部實現(xiàn)
- block的循環(huán)引用問題
- 總結(jié)
1. block的定義
block是Objective-C對于閉包的實現(xiàn)(閉包是一個函數(shù)<或者指向函數(shù)的指針>加上函數(shù)有關(guān)的自由變量).
block也是對象, 以下對block結(jié)構(gòu)體的成員作簡單解釋
- isa : 指向Class對象的指針, 所有對象都有該指針
- flags : 用于按 bit 位表示一些 block 的附加信息
- reserved : 保留變量
- invoke : 指向函數(shù)的指針, 指向block的實現(xiàn)代碼
- descriptor : 表示該 block 的附加描述信息宏悦,主要是 size 大小焰坪,以及 copy 和 dispose 函數(shù)的指針。
- 捕捉到的變量 : 捕捉過來的變量拷恨,block 之所以能夠訪問它外部的局部變量舔亭,就是因為將這些變量(或?qū)ο蟮牡刂罚┛截惖搅私Y(jié)構(gòu)體中些膨。
- copy : 用于保留捕獲的對象
- dispose : 用于釋放捕獲的對象
2. block的類型
- 全局塊(_NSConcreteGlobalBlock)
- 棧塊(_NSConcreteStackBlock)
- 堆塊(_NSConcreteMallocBlock)
這三種block各自的存儲域如下表
類 | 設(shè)置對象的存儲域 |
---|---|
_NSConcreteStackBlock | 棧 |
_NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域(.data區(qū)) |
_NSConcreteMallocBlock | 堆 |
說明 :
- 全局塊存在于全局內(nèi)存中, 相當(dāng)于單例.
- 棧塊存在于棧內(nèi)存中, 超出其作用域則馬上被銷毀
- 堆塊存在于堆內(nèi)存中, 是一個帶引用計數(shù)的對象, 需要自行管理其內(nèi)存
全局塊(_NSConcreteGlobalBlock)
- block定義在全局變量的地方
- block沒有截獲任何自動變量
以上兩個情況滿足任意一個則該block為全局塊, 全局塊的生命周期貫穿整個程序, 相當(dāng)于單例.
棧塊(_NSConcreteStackBlock)
只要不是全局塊, 且block沒有被copy, 就是棧塊.棧塊的生命周期很短, 當(dāng)前作用域結(jié)束, 該block就被廢棄. 要想在當(dāng)前作用域以外的地方使用該block, 應(yīng)該把該block從棧copy到堆上
堆塊(_NSConcreteMallocBlock)
簡單來說, 棧塊copy之后就變成堆塊, 這簡單吧~
ARC下的block類型
因為ARC下默認變量修飾符為__strong, 所以我們接觸到的block幾乎全是堆block和全局block.
ARC下, blk = block; 相當(dāng)于 blk = [block copy];
3. block特性
1. 截獲自動變量值
1> 對于 block 外的變量引用,block 默認是將其復(fù)制到其數(shù)據(jù)結(jié)構(gòu)中來實現(xiàn)訪問的. 也就是說block的自動變量截獲只針對block內(nèi)部使用的自動變量, 不使用則不截獲, 因為截獲的自動變量會存儲于block的結(jié)構(gòu)體內(nèi)部, 會導(dǎo)致block體積變大.
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
輸出為
age = 10
2> 對于用 __block 修飾的外部變量引用分歇,block 是復(fù)制其引用地址來實現(xiàn)訪問的.
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
輸出為
age = 18
意味著對于第一種情況, 在block外部修改變量的值并不會應(yīng)該block內(nèi)部變量的值.而第二種情況則反之.
并且第一種情況block內(nèi)部不允許修改變量的值, 第二種情況下可以. (有例外, 靜態(tài)變量, 靜態(tài)全局變量, 全局變量即使不使用__block修飾符也可以在block內(nèi)部修改其值)
2. 截獲對象
對象不同于自動變量, 就算對象不加上__block修飾符, 在block內(nèi)部能夠修改對象的屬性.
block截獲對象與截獲自動變量有所不同.
堆塊會持有對象, 而不會持有__block修飾的對象, 而棧塊永遠不會持有對象, 為什么呢?
- 堆塊作用域不同于棧塊, 堆塊可以超出其作用域地方使用, 所以堆塊結(jié)構(gòu)體內(nèi)部會保留對象的強指針, 保證堆塊在生命周期結(jié)束之前都能訪問對象. 而對于__block對象為什么不會持有呢? 原因很簡單, 因為block對象會跟隨block被復(fù)制到堆中, block再去引用堆中的對象(后面會講這個過程)..
- 棧塊只能在當(dāng)前作用域下使用, 所以其內(nèi)部不會持有對象. 因為不存在在作用域之外訪問對象的可能(棧離開當(dāng)前作用域立馬被銷毀)
4. __block修飾符
為什么__block修飾符修飾的變量就能夠在block內(nèi)部修改呢?? 原因在此
利用clang -rewrite-objc 源代碼文件名
便可揭開其神秘的面紗.
__block int val = 10;
轉(zhuǎn)換成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
天哪! 一個局部變量加上__block修飾符后竟然跟block一樣變成了一個__Block_byref_val_0結(jié)構(gòu)體類型的自動變量實例.
此時我們在block內(nèi)部訪問val變量則需要通過一個叫__forwarding的成員變量來間接訪問val變量(下面會對__forwarding進行詳解)
5. copy
block的copy操作究竟做了什么呢?
這里不得不提及__block變量的存儲域
__block變量的配置存儲域 | block從棧復(fù)制到堆時的影響 |
---|---|
棧 | 從棧復(fù)制到堆并被Block持有 |
堆 | 被Block持有 |
由上圖可知, 對一個棧塊進行copy操作會連同block與__block變量(不管有沒有使用)在內(nèi)一同copy到堆上, 并且block會持有__block變量(使用).
ps : 堆上的block及__block變量均為對象, 都有各自的引用計數(shù)
當(dāng)然, 當(dāng)block被銷毀時, block持有的__block也會被釋放
到這里我們能知道, 此思考方式與Objective-C的引用計數(shù)內(nèi)存管理完全相同.
那么有人就會問了, 既然__block變量也被復(fù)制到堆上去了, 那么訪問該變量是訪問棧上的還是堆上的呢?? __forwarding 終于要閃亮登場了
通過__forwarding, 無論實在block中, block外訪問__block變量, 也不管該變量在棧上或堆上, 都能順利地訪問同一個__block變量.
什么時候我們需要手動對block調(diào)用copy方法
前面我們說到 : 要想在當(dāng)前作用域以外的地方使用該block, 應(yīng)該把該block從棧copy到堆上. 實際上, 在ARC下, 以下幾種情況下, 編譯器會幫我們把棧上的block復(fù)制到堆中
- block作為函數(shù)返回值返回時
- 將block賦值給__strong修飾符id類型或block類型成員變量時
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞block時
理論上我們只有把block作為函數(shù)/方法的參數(shù)傳入時才需要對block進行copy操作.
我們對不同地方的block調(diào)用copy會產(chǎn)生什么效果呢?
Block的類 | 副本源的配置存儲域 | 拷貝效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從椏兀拷貝到堆 |
_NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
所以, 不管block是什么類型, 在什么地方, 用copy方法都不會引起任何問題.如下表格所示. 就算是反復(fù)多次調(diào)用copy方法, 如
blk = [[[[blk copy] copy] copy] copy];
該源碼可解釋如下 :
{
block tmp = [blk copy]; // block被tmp持有
blk = tmp; // block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被釋放, block被blk持有
{
block tmp = [blk copy]; // block被tmp和blk持有
blk = tmp; // blk指向的舊block釋放, 并強引用新block, 最終block被tmp和blk持有
}
// tmp超出作用域, 其指向的block也被釋放, block被blk持有
...下面不斷重復(fù)該過程
我們知道, 這只是一個循環(huán)的過程, block被tmp持有 -> block被tmp和blk持有 -> block被blk持有 -> block被tmp和blk持有 -> ......
由此可得知, 在ARC下該代碼也沒有任何問題.
總結(jié) : 如果block需要給作用域外的地方使用, 但是你不知道需不需要copy, 那就copy吧. 反正不會錯
6. block的循環(huán)引用
這部分相信大家都清楚怎樣做能破環(huán), 所以我在這就只簡單說兩句
- MRC下用__block可以避免循環(huán)引用(原因見上面block特性之截獲自動變量值)
- ARC下用__weak來避免循環(huán)引用
這里需要提醒大家的是, 只有堆塊(_NSConcreteMallocBlock)才可能會造成循環(huán)引用, 其他兩種block不會
7. Block總結(jié) :
- block相比函數(shù)更加方便, 高效, 蘋果強烈推薦使用
- ARC下編譯器會幫助我們更好地管理block的生命周期
- ARC下block屬性聲明為strong或copy其實都一樣, 因為編譯器內(nèi)部會幫我們實現(xiàn)copy方法
- 善用__weak(ARC)或__block(MRC)來避免循環(huán)引用
推薦幾篇有關(guān)block的文章
談Objective-C block的實現(xiàn)
讓我們來深入簡出block吧
歡迎大家關(guān)注@Jerry4me, 關(guān)注菜鳥成長_. 我會不定時更新一些學(xué)習(xí)心得與文章.