1. 程式人生 > >Objective-C高階程式設計讀書筆記之blocks

Objective-C高階程式設計讀書筆記之blocks

Objective-C高階程式設計
iOS與OS X多執行緒和記憶體管理

Blocks

這裡有五道關於block的測試題, 大家可以去做做測試看看自己對block瞭解多少.

目錄

  1. Block的定義
  2. Block有哪幾種類型
  3. Block特性
  4. __block修飾符
  5. block呼叫copy方法的內部實現
  6. block的迴圈引用問題
  7. 總結

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-C
1 ARC,blk=block;相當於blk=[block copy];

3. block特性

1. 截獲自動變數值

1> 對於 block 外的變數引用,block 預設是將其複製到其資料結構中來實現訪問的. 也就是說block的自動變數截獲只針對block內部使用的自動變數, 不使用則不截獲, 因為截獲的自動變數會儲存於block的結構體內部, 會導致block體積變大.

拷貝 Objective-C
123456 intage=10;myBlock block=^{NSLog(@"age = %d",age);};age=18;block();

輸出為
age = 10

2> 對於用 __block 修飾的外部變數引用,block 是複製其引用地址來實現訪問的.

引用地址 Objective-C
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修飾的物件, 而棧塊永遠不會持有物件, 為什麼呢?

  1. 堆塊作用域不同於棧塊, 堆塊可以超出其作用域地方使用, 所以堆塊結構體內部會保留物件的強指標, 保證堆塊在生命週期結束之前都能訪問物件. 而對於__block物件為什麼不會持有呢? 原因很簡單, 因為__block物件會跟隨block被複制到堆中, block再去引用堆中的__物件(後面會講這個過程)..
  2. 棧塊只能在當前作用域下使用, 所以其內部不會持有物件. 因為不存在在作用域之外訪問物件的可能(棧離開當前作用域立馬被銷燬)

4. __block修飾符

為什麼__block修飾符修飾的變數就能夠在block內部修改呢?? 原因在此
利用

Objective-C
1 clang-rewrite-objc原始碼檔名

便可揭開其神祕的面紗.

Objective-C
123456789 __block intval=10;轉換成__Block_byref_val_0 val={0,&amp;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持有

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-C
1 blk=[[[[blk copy] copy] copy] copy];

該原始碼可解釋如下 :

Objective-C
1234567891011 {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)來避免迴圈引用

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式