iOS底層分析-block(一)

語言: CN / TW / HK

這是我參與8月更文挑戰的第20天,活動詳情檢視: 8月更文挑戰

block的分類

iOSblock分為三類: * NSGlobalBlock:全域性block * 位於全域性區; * 在block內部不使用外部變數,或者只是用靜態變數和全域性變數; * NSMallocBlock:堆block * 位於堆區; * 在block內部使用區域性變數或者OC屬性,並且賦值給強引用; * NSStackBlock:棧block * 位於棧區; * 與NSMallocBlock一樣,可以在內部使用區域性變數或者OC屬性。但是不能賦值給強引用或者Copy修飾的變數;

block的執行

  • NSGlobalBlock:全域性block,內部不使用外部變數
  • NSMallocBlock:堆block,內部使用變數a
  • NSStackBlock:棧block,使用__weak進行弱引用(預設為強引用)

需要注意的是:block持有的是^{}的記憶體空間;

block操作引用計數

我們在使用block的過程中,經常會出現迴圈引用的問題,繼而導致出現記憶體洩漏,檢視無法釋放dealloc不執行的問題;所以我們要熟練掌握不同的block對變數的持有情況;

解析:

  • 1、[NSObject new]之後,其引用計數為1;相信這一步列印大家應該是沒有異議的;
  • 2、strongBlock列印3應該是出乎大家意料的,這是為什麼呢?
    • 首先:strongBlock會捕獲objc,從而導致objc的引用計數+1
    • 但是因為strongBlock內部使用了objc,並且是預設的強引用,所以strongBlock是個堆區的block;由於strongBlock是一個非全域性的block,那麼進行操作時會進行block記憶體的開闢一個result,然後會將原始的block也就是ablocksizeinvokeflagsisa等成員變數都會賦值給result一份,相當於將aBlock也就是原始的block拷貝了一份給result這個新開闢了記憶體空間的block;那麼此時a因為被strongBlock強持有,會把的記憶體拷貝了一份到上去,那麼其引用計數也要再+1,所以最終是3;這部分分析,可在block的底層原始碼中看到:
  • 3、weakBlock因為使用了__weak進行修飾,那麼他就是個棧block棧block不進行記憶體拷貝的操作;其引用技術只能+14
  • 4、weakBlock拷貝了一份放在名為mallocBlock堆block上,所以引用計數再次+15;

此處,其實第二步操作可以看作是第三步和第四步操作合體;第二步分解之後就是第三步和第四步操作;

block的記憶體拷貝

為了便於操作block的成員變數,我們按照系統block來重寫了一個block的結構;那麼我們就可以對系統的block進行強轉,然後操作成員變數;

我們來看一段程式碼:

  • _LGBlock是我們重寫的block的結構
  • struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;將系統的block強制轉換為_LGBlock;
  • blc->invoke = nil;這裡我們想要論證一個點:我們強轉之後的blcweakBlock是不是同一片的記憶體空間?如果是,那麼對blcinvoke進行=nil操作,那麼weakBlock也會被修改,將無法呼叫;
  • void(^strongBlock1)(void) = strongBlock;因為我們把strongBlock 宣告為了id,此處這麼寫是為了使strongBlock具有block的特性,可以使用strongBlock()進行呼叫;其實可以直接寫成void(^strongBlock1)(void) __strong strongBlock = weakBlock

我們來看一下程式碼的執行結果:

程式碼崩潰,表明invoke置空之後導致block找不到了;

我們對程式碼進行調整之後,再次執行:

進行copy操作之後,strongBlock的記憶體將會從上拷貝到上,那麼strongBlockweakBlock已經不是同一片記憶體了;

分析到這裡,我們應該對棧block堆block有一定的認知了;

block堆疊的釋放

我們接著看一段程式碼的執行:

  • 因為strongBlock使用了局部變數,但是使用了__weak進行弱引用,所以strongBlock是個棧block,然後將strongBlock賦值給了weakBlock,所以此時weakBlock指向了棧block的記憶體空間,而strongBlock在所在程式碼塊執行完畢之後就被釋放;但是weakBlock的生命週期是在blockDemo3的整個程式碼區;所以雖然strongBlock最已經釋放,但是由於weakBlock指向了其記憶體空間,最後依然能夠正常打印出1 0;

程式碼執行如下:

我們將程式碼稍作修改,去掉strongBlock__weak,檢視執行結果:

直接崩潰!!!Why???

解析:

  • 去掉了__weakstrongBlock成為了堆block,其生命週期為所在的程式碼塊區域;
  • strongBlock賦值給了weakBlock,而weakBlock是一個堆block,所以會將strongBlock的指標拷貝給了weakBlock,他們兩個的指標地址是一樣的;當出了strongBlock的作用域之後,strongBlock就會釋放,那麼weakBlock所指向的記憶體空間將會不存在,所以會崩潰;

如圖所示:

block的迴圈引用

  • 可以正常釋放

  • 迴圈引用無法釋放

接下來我們來看段程式碼:

UIView持有了blockblock捕獲的self,所以不會造成迴圈引用;

self持有了blockblock又捕獲了self,然後導致了retain cycle,造成迴圈引用;

那麼我們怎麼解決這個迴圈引用呢?

我們可以通過__weak typeof(self) weakSelf = self;來解決迴圈引用;

那麼是不是通過__weak typeof(self) weakSelf = self;就能一勞永逸呢?我們再來看一段程式碼:

block中有一個延遲2秒執行的任務,我們在當前介面停留超過2秒在進行pop返回上級介面時,一切正常;

那麼如果我們在2秒內就返回呢?

可以看到,如果我們快速的返回上級介面,dealloc執行之後,name值就獲取不到了;因為weakSelf已經釋放了,nil傳送訊息還是nil

那麼如何解決呢?我們繼續修改程式碼如下:

此時,即使我們快速的返回上級介面,dealloc執行,name也能夠正常列印;__weak typeof(self) weakSelf = self;__strong typeof(weakSelf) strongSelf = weakSelf;結合使用可以避免資料的丟失;

strongSelf是一個臨時變數,在作用域執行完畢之後就會釋放;weakSelf可以自動置為nil

那麼除此之外,還有沒有其他方式可以避免迴圈引用呢?

我們修改程式碼如下:

此時,即使我們快速的返回上級介面,dealloc執行,name也能夠正常列印;

解析: * vc是一個臨時變數,對self進行持有;其生命週期只在NSLog之前有意義,然後進行=nil; * 如果不進行=nil操作;那麼self持有block,block持有vc,vc又持有self,最終導致迴圈引用;

除此之外,我們還可以通過引數的方式來解決迴圈引用;

我們修改block的定義,把typedef void(^KCBlock)(void);修改為typedef void(^KCBlock)(ViewController *);,程式碼修改如下:

正常執行,可以釋放;

  • 因為vc是以引數的形式傳遞的,所以並不會被block捕獲;

block迴圈引用的面試題

面試題一

```Objective-C++ static ViewController *staticSelf_;

  • (void)blockWeak_static { __weak typeof(self) weakSelf = self; staticSelf_ = weakSelf; } ```

上面方法是否會發生迴圈引用?答案是會發生迴圈引用!!!

weakSelf是個弱引用,那麼為什麼還會發生記憶體洩漏導致無法釋放呢?

解析:

weakSelfself是同一片記憶體空間,是對映關係;可以通過列印進行驗證:

那麼staticSelf持有的實際上是self,而staticSelf是一個全域性的靜態變數,所以self無法釋放;

面試題二

```Objective-C++ @property (nonatomic, copy) KCBlock doWork; @property (nonatomic, copy) KCBlock doStudent;

  • (void)block_weak_strong { __weak typeof(self) weakSelf = self; self.doWork = ^{ __strong typeof(self) strongSelf = weakSelf; weakSelf.doStudent = ^{ NSLog(@"%@", strongSelf); }; weakSelf.doStudent(); }; self.doWork(); } ```

這個方法依然會造成記憶體洩漏

解析:

這個方法的問題出在 Objective-C++ weakSelf.doStudent = ^{ NSLog(@"doStudent:%@", strongSelf); }; weakSelf.doStudent();

strongSelfdoStudent進行捕獲持有,那麼strongSelf的引用計數就會+1;所以在當前程式碼塊執行完畢,strongSelf引用計數-1之後,其引用計數依然不為0;所以無法釋放;