Objective-C高階程式設計讀書筆記之blocks
iOS與OS X多執行緒和記憶體管理
Blocks
這裡有五道關於block的測試題, 大家可以去做做測試看看自己對block瞭解多少.
目錄
- Block的定義
- Block有哪幾種類型
- Block特性
- __block修飾符
- block呼叫copy方法的內部實現
- block的迴圈引用問題
- 總結
1. block的定義
block是Objective-C對於閉包的實現(閉包是一個函式<或者指向函式的指標>加上函式有關的自由變數).
block的資料結構block也是物件, 以下對block結構體的成員作簡單解釋
- isa : 指向Class物件的指標, 所有物件都有該指標
- flags : 用於按 bit 位表示一些 block 的附加資訊
- reserved : 保留變數
- invoke : 指向函式的指標, 指向block的實現程式碼
- descriptor : 表示該 block 的附加描述資訊,主要是 size 大小,以及 copy 和 dispose 函式的指標。
- 捕捉到的變數 : 捕捉過來的變數,block 之所以能夠訪問它外部的區域性變數,就是因為將這些變數(或物件的地址)拷貝到了結構體中。
- copy : 用於保留捕獲的物件
- dispose : 用於釋放捕獲的物件
2. block的型別
- 全域性塊(_NSConcreteGlobalBlock)
- 棧塊(_NSConcreteStackBlock)
- 堆塊(_NSConcreteMallocBlock)
這三種block各自的儲存域如下表
類 | 設定物件的儲存域 |
---|---|
_NSConcreteStackBlock | 棧 |
_NSConcreteGlobalBlock | 程式的資料區域(.data區) |
_NSConcreteMallocBlock | 堆 |
說明 :
- 全域性塊存在於全域性記憶體中, 相當於單例.
- 棧塊存在於棧記憶體中, 超出其作用域則馬上被銷燬
- 堆塊存在於堆記憶體中, 是一個帶引用計數的物件, 需要自行管理其記憶體
全域性塊(_NSConcreteGlobalBlock)
- block定義在全域性變數的地方
- block沒有截獲任何自動變數
以上兩個情況滿足任意一個則該block為全域性塊, 全域性塊的生命週期貫穿整個程式, 相當於單例.
棧塊(_NSConcreteStackBlock)
只要不是全域性塊, 且block沒有被copy, 就是棧塊.棧塊的生命週期很短, 當前作用域結束, 該block就被廢棄. 要想在當前作用域以外的地方使用該block, 應該把該block從棧copy到堆上
從棧複製到堆上的Block與__block變數堆塊(_NSConcreteMallocBlock)
簡單來說, 棧塊copy之後就變成堆塊, 這簡單吧~
ARC下的block型別
因為ARC下預設變數修飾符為__strong, 所以我們接觸到的block幾乎全是堆block和全域性block.
Objective-C1 | ARC下,blk=block;相當於blk=[block copy]; |
3. block特性
1. 截獲自動變數值
拷貝 Objective-C1> 對於 block 外的變數引用,block 預設是將其複製到其資料結構中來實現訪問的. 也就是說block的自動變數截獲只針對block內部使用的自動變數, 不使用則不截獲, 因為截獲的自動變數會儲存於block的結構體內部, 會導致block體積變大.
123456 | intage=10;myBlock block=^{NSLog(@"age = %d",age);};age=18;block(); |
輸出為
age = 10
引用地址 Objective-C2> 對於用 __block 修飾的外部變數引用,block 是複製其引用地址來實現訪問的.
123456 | __block intage=10;myBlock block=^{NSLog(@"age = %d",age);};age=18;block(); |
輸出為
age = 18
意味著對於第一種情況, 在block外部修改變數的值並不會應該block內部變數的值.而第二種情況則反之.
並且第一種情況block內部不允許修改變數的值, 第二種情況下可以. (有例外, 靜態變數, 靜態全域性變數, 全域性變數即使不使用__block修飾符也可以在block內部修改其值)
2. 截獲物件
物件不同於自動變數, 就算物件不加上__block修飾符, 在block內部能夠修改物件的屬性.
block截獲物件與截獲自動變數有所不同.
堆塊會持有物件, 而不會持有__block修飾的物件, 而棧塊永遠不會持有物件, 為什麼呢?
- 堆塊作用域不同於棧塊, 堆塊可以超出其作用域地方使用, 所以堆塊結構體內部會保留物件的強指標, 保證堆塊在生命週期結束之前都能訪問物件. 而對於__block物件為什麼不會持有呢? 原因很簡單, 因為__block物件會跟隨block被複制到堆中, block再去引用堆中的__物件(後面會講這個過程)..
- 棧塊只能在當前作用域下使用, 所以其內部不會持有物件. 因為不存在在作用域之外訪問物件的可能(棧離開當前作用域立馬被銷燬)
4. __block修飾符
為什麼__block修飾符修飾的變數就能夠在block內部修改呢?? 原因在此
利用
1 | clang-rewrite-objc原始碼檔名 |
便可揭開其神祕的面紗.
Objective-C123456789 | __block intval=10;轉換成__Block_byref_val_0 val={0,&val,0,sizeof(__Block_byref_val_0),10}; |
天哪! 一個區域性變數加上__block修飾符後竟然跟block一樣變成了一個__Block_byref_val_0結構體型別的自動變數例項.
此時我們在block內部訪問val變數則需要通過一個叫__forwarding的成員變數來間接訪問val變數(下面會對__forwarding進行詳解)
5. copy
block的copy操作究竟做了什麼呢?
這裡不得不提及__block變數的儲存域
__block變數的配置儲存域 | block從棧複製到堆時的影響 |
---|---|
棧 | 從棧複製到堆並被Block持有 |
堆 | 被Block持有 |
由上圖可知, 對一個棧塊進行copy操作會連同block與__block變數(不管有沒有使用)在內一同copy到堆上, 並且block會持有__block變數(使用).
ps : 堆上的block及__block變數均為物件, 都有各自的引用計數
當然, 當block被銷燬時, block持有的__block也會被釋放
Block廢棄和__block變數的釋放到這裡我們能知道, 此思考方式與Objective-C的引用計數記憶體管理完全相同.
那麼有人就會問了, 既然__block變數也被複制到堆上去了, 那麼訪問該變數是訪問棧上的還是堆上的呢?? __forwarding 終於要閃亮登場了
複製__block變數通過__forwarding, 無論實在block中, block外訪問__block變數, 也不管該變數在棧上或堆上, 都能順利地訪問同一個__block變數.
什麼時候我們需要手動對block呼叫copy方法
前面我們說到 : 要想在當前作用域以外的地方使用該block, 應該把該block從棧copy到堆上. 實際上, 在ARC下, 以下幾種情況下, 編譯器會幫我們把棧上的block複製到堆中
- block作為函式返回值返回時
- 將block賦值給__strong修飾符id型別或block型別成員變數時
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞block時
理論上我們只有把block作為函式/方法的引數傳入時才需要對block進行copy操作.
我們對不同地方的block呼叫copy會產生什麼效果呢?
Block的類 | 副本源的配置儲存域 | 拷貝效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧拷貝到堆 |
_NSConcreteGlobalBlock | 程式的資料區域 | 什麼也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數增加 |
所以, 不管block是什麼型別, 在什麼地方, 用copy方法都不會引起任何問題.如下表格所示. 就算是反覆多次呼叫copy方法, 如
Objective-C1 | blk=[[[[blk copy] copy] copy] copy]; |
該原始碼可解釋如下 :
Objective-C1234567891011 | {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持有...下面不斷重複該過程 |
我們知道, 這只是一個迴圈的過程, block被tmp持有 -> block被tmp和blk持有 -> block被blk持有 -> block被tmp和blk持有 -> ……
由此可得知, 在ARC下該程式碼也沒有任何問題.
總結 : 如果block需要給作用域外的地方使用, 但是你不知道需不需要copy, 那就copy吧. 反正不會錯
6. block的迴圈引用
這部分相信大家都清楚怎樣做能破環, 所以我在這就只簡單說兩句
- MRC下用__block可以避免迴圈引用(原因見上面block特性之截獲自動變數值)
- ARC下用__weak來避免迴圈引用
這裡需要提醒大家的是, 只有堆塊(_NSConcreteMallocBlock)才可能會造成迴圈引用, 其他兩種block不會
7. Block總結 :
- block相比函式更加方便, 高效, 蘋果強烈推薦使用
- ARC下編譯器會幫助我們更好地管理block的生命週期
- ARC下block屬性宣告為strong或copy其實都一樣, 因為編譯器內部會幫我們實現copy方法
- 善用__weak(ARC)或__block(MRC)來避免迴圈引用
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式