? ? ?本文只介紹了ARC時的情況,有些細節不適用于MRC。比如MRC下__block不會增加引用計數,但ARC會,ARC下必須用__weak指明不增加引用計數;ARC下block內存分配機制也與MRC不一樣(ARC下會將棧區的Block在賦值的時候copy到堆區,從而導致截取的堆區變量引用計數增加),所以文中的一些例子在MRC下測試結果可能與文中描述的不一樣
簡介:這是一篇講解如何使用Block,以及在使用過程中如何避免Cycle Retain的文章。如果想要知道Block的深層次的實現,可以去看<Objective-C 高級編程 iOS與OS X多線程和內存管理>的Block篇,書中詳解了Block的底層實現。
一、Blcok的?優點和種類
?1、Block的優點
? ? ? Block雖然會由于使用不當,而導致Cycle Retain,但還是有很多優點的。語法簡潔,回調方便,思路清晰,還有就是Block作為C語言的擴展執行效率較高。這樣用文字說明可能不?直觀,直接上代碼做對比。通知的設計模式是開發過程中常用的,以使用Block回調和不使用Block的方式來作對比。
? ? 通過對比,使用Block的接收通知處理和通知接收的方法緊密的黏在一起,直觀明了,不過這里有個大坑,待會會提到。是否感受到Block的好處了呢,如果是,那么以后就多用吧,它會讓你的代碼思路更連貫!
2、Block的種類
? ? Block不就是匿名函數么,還有種類?這個種類不是說形式上的種類,而是根據Block在內存中存儲區域的不同而分的種類,有三種:Stack(棧區),Malloc(堆區),Global(全局)。之所以要在這里提到這三種Block,是因為后面的Cycle Retain就是由于Malloc(堆區)的Block導致的。在OC中堆區的內存管理都是用引用計數來管理的,而Stack和Global都是沒有引用計數的,當它們超出作用域后,就會失去作用。那么Stack(棧區),Malloc(堆區),Global(全局)的block怎么判斷,它們分別有哪些呢。
(1)判斷方式
? ? 在代碼中,我們定義了一個全局靜態區的變量,通過它和block地址的對比,可以發現它們差不多,也就是說這個Block是Global(全局)的。同樣的方式,Stack(棧區),Malloc(堆區),都可以判斷出來。如果你覺得這種判斷方式太low的話,Clang可以查看中間代碼(C++),打開終端用Clang -rewrite-objc 編譯你的文件,就可以看到中間代碼了。說了不說原理的,不然太長了。如果想用這種方式判斷的話,可以去看看這篇博客:iOS中block實現的探究。
(2)Stack(棧區),Malloc(堆區),Global(全局)的Block有哪些
? ?以下所說的都是在ARC模式下
二、Block的使用
? ? 之所以寫這一部分,是因為一些初學者,連基本的Block都不會使用,也不知道用在什么情形下,下面就是說Block用在什么情況下,又怎么用,如果你已經會用了,可以跳過這一部分。
1、用于兩個類之間的通信
? ?這是開發中最常用的,也就是ViewController和View,ViewController和ViewController之間的通信,這個通信就包括傳值或者讓另一個對象執行一些處理。這個思路和delegate(代理)很像,不過Block更簡潔。這里就不上代碼了,因為代碼實在是不好上??!如果真的需要的可以私聊我。
2、用于?方法的回調
? ? 這種使用情況,也是常用的,系統和很多第三方都用了這樣的方法。還是以前面接收通知的Block為例子
? ?我們來分析一下這個方法的最后一個參數usingBlock,跟前面一樣,在:后面都是跟的參數類型,那么usingBlock后面也是跟的參數類型,那么這個參數類型就是沒有返回值、參數為note(NSNotification類的對象)的Block類型(后面的block為參數名)。那么接下來,我們就自己定義一個類似的方法,讓它有回調Block
? ?這樣,我們就定義了一個沒有返回值,沒有參數的Block類型,這個類型的變量為block,并且在函數內部實現回調,這樣,我們就實現了和前面系統通知所寫的一樣的Block回調。當然在寫Block類型的時候,是不會這樣寫的,而是用typedef。
這就是Block的兩種常用用法,當然這是最基本的。下面就進入本文的重點,如何避免在使用Block的過程中造成的Cycle Retain。
三、避免Cycle Retain
1、Cycle Retain
? ? ? retain cycle問題的根源在于Block和obj可能會互相強引用,Malloc(堆區)Block的內存管理方式也是引用計數,它的內部實現和類一樣,都是通過isa指針指向堆區的該類型對象,可以說Malloc(堆區)Block就是一個類的對象,而被block截取的變量,就作為它的"屬性",會被retain一次或者copy到堆區(如果它是在棧區的話)),互相retain對方。比如A和B兩個對象,A持有B,B同時也持有A,按照上面的規則,A只有B釋放之后才有可能釋放,同樣B只有A釋放后才可能釋放,當雙方都在等待對方釋放的時候, retain cycle就形成了,結果是,兩個對象都永遠不會被釋放,最終內存泄露。
根據這個原理,那么會造成Cycle Retain的情況就只有三種。
一種是:block作為某個類的屬性,可是它又截取了這個類的對象,從而導致Block retain了一次這個對象,這個對象又retain了一次這個Block(作為屬性的時候會用copy,引用計數加一)。以ViewController這個類為例
我們發現這種情況,xcode會給我們警告,所以這種情況是很容易發現并解決的,用__weak typeof(self) weakself = self;來代替block里面的self,就可以了。
第二種:這種情況很難發現,但是很好解決(解決方法一樣)。那是什么呢,其實本質還是一樣,就是一個類的對象retain或者copy了這個Block,而這個Block又同時持有了這個類的對象,導致互相不能釋放,因為block不能釋放,導致其它被這個block截取的對象也無法釋放。還是以通知為例(請原諒我,我真的超級喜歡用通知~)
? ? 這段代碼的思路是,當我接收到通知的時候,我就改變ViewController的顏色,然后在當ViewController釋放的時候移除通知??墒沁@會導致Cycle Retain,導致ViewController不能釋放。解決辦法你可能也知道,跟上面一樣,block里面放weakself??墒菫槭裁茨??這個Block我們沒有作為屬性,ViewController并沒有retain它,只是Block retain了ViewController而已,沒有造成Cycle Retain。我們先看一段官方文檔:
翻譯一下:這個block會再接收到通知的時候執行,這個block被通知中心copy并且直到觀察者被移除的時候才會移除。也就是說這個block會一直被通知中心持有,直到觀察者被移除,它才會被釋放。很好,問題解決了。block一直被通知中心持有,而block又retain了一次ViewController,導致ViewController不能釋放(引用計數不能為0),這樣ViewController就不會走dealloc這個方法。解決辦法也是一樣:
第三種:這種情況和第二種情況原理一樣,但是是最常遇到的,所以單獨拿出來講。這種情況是在項目中,用MJRefresh這個第三方的時候發現的。其實,只要懂了Cycle Retain的問題根源,這種情況也是很好理解的。
tableView.mj_footer = [MJRefreshFooter footerWithRefreshingBlock:^(void)refreshingBlock]
當tableView進行上拉加載的時候,會觸發這個這個回調refreshingBlock,執行相應的加載操作(跟新數據),如果在refreshingBlock里面用了self,也會導致Cycle Retain,那這又是為什么呢。把這個方法點進去之后可以看到它的實現:
可以看到,方法的實現中,把block作為屬性?賦值給MJRefreshFooter對象并且返回作為tableView的屬性。我們知道所有的View都被ViewController retain了一次(view的生存周期),如果block作為view的屬性,那就相當于self.view.tableView.mj_footer.refreshingBlock;所以refreshingBlock前面所有的對象:self、tableView、mj_footer都不能被refreshingBlock retain,如果有一個被retain了,那就是Cycle Retain!?這里我們仍然用__weak指針打破Cycle Retain。解決方法一樣,這里就不詳解了。
2、??不能濫用__weak指針
? ? __weak指針可以解決Cycle Retain問題,但是不能亂用比如gcd和UIView的Animation等等,因為Block沒有retain那個對象,雖然不會像MRC下那樣造成Crash,但是還是可能會導致沒法實現你要的功能。例子如下:
? ?這里我們讓dispatch_async中的隊列延遲5秒執行,?在執行隊列前按下button,讓self釋放掉(dissmiss),這樣self會為nil,可是我想要在5秒后讓它輸出"test",由于self已經被釋放變為nil,雖然不會crash或者內存泄露,但是我想要實現的功能卻不能實現了。
? ? ? 將Block作為參數傳給dispatch_async時,系統會將Block拷貝到堆上,如果Block中使用了實例變量,還將retain self,因為dispatch_async并不知道self會在什么時候被釋放,為了確保系統調度執行Block中的任務時self沒有被意外釋放掉,dispatch_async必須自己retain一次self,任務完成后再release self。但這里使用__weak,使dispatch_async沒有增加self的引用計數,這使得在系統在調度執行Block之前,self可能已被銷毀,但系統并不知道這個情況,可能導致有些功能不能實現。
? ? 總結:要想用好Block就得多寫、多用,當Block作為屬性的時候,就值得你去關注Retain Cycel的問題了。
? ? ?最后也是最重要的,如果有用到Block,?盡量在那個類里寫下-(void)dealloc這個方法,看看這個類本該釋放?是否沒有釋放,?如果沒有釋放,再去研究并解決!這樣積累的經驗越多,相信看理論知識也能看得更深。