一篇文章讓你看懂IOS中的block為何再也不需要WeakSelf弱引用
前言:
最近都在折騰Sagit架框的內(nèi)存釋放的問題,所以對這一塊有些心得。
對于新手,學(xué)到的文章都在教你用:typeof(self) __weak weakSelf = self
。
對于老手,可能早習(xí)慣了到處了WeakSelf了。
這次,就來學(xué)學(xué),如何不用WeakSelf。
1:從引用計數(shù)器開始:
這里先設(shè)計一個TableBlock類:
@interface BlockTable : NSObject typedef void (^AddCellBlock)(); @property (nonatomic,copy)AddCellBlock addCell;@end
先這么簡單,一個BlockTable只有一個block屬性,然后輸出一段釋放的日志。
@interface BlockTable : NSObject typedef void (^AddCellBlock)(); @property (nonatomic,copy)AddCellBlock addCell;@end
接著,隨意找一個地方寫寫代碼:來new了一個BlockTable,并打印一下信息:
這時候它的引用數(shù)是1,并且出了Table relase 。
接著給addCell屬性賦一個值,并運行:
一個空的事件,里面并沒有引用到table,所以引用數(shù)還是1。
2:開始循環(huán)引用
在block引用table,讓它產(chǎn)生循環(huán)引用,并運行:
我們看到:引用數(shù)變成了3,沒有輸出對象釋放信息了,為啥不是2呢?大大的問號??!
一個屬性賦值,為什么增強兩個引用計數(shù)?
3:猜解跳躍的計數(shù)器
接下來,把屬性設(shè)置為nil,運行看看:
設(shè)置為nil,還有2?
也正常釋放了?
為了證實自己對這個看起來就很明顯的猜想:重寫addCell的setter方法,不進行任何保存:
@implementation BlockTable -(void)setAddCell:(AddCellBlock)addCell { }
同時去掉置為nil的代碼:再運行看看:
計數(shù)器仍為2,而且也釋放了。
經(jīng)過思考,出來了以下的結(jié)論:
1:塊的定義本身,就會造成1次引用,不過這次引用,在塊離開所在的函數(shù)時,釋放時,抵消掉引用數(shù)。
2:存檔塊的時候,會造成1次引用,而這個引用,是內(nèi)存無法釋放的原因。
4:根據(jù)上述解釋,得到一個瘋狂的結(jié)論:
只要block的代碼只執(zhí)行1次的,都可以任性的self或其它強引用。
事實上,我們寫的代碼,很多block的確只執(zhí)行一次,不管是傳的時候就執(zhí)行,還是傳完之后過段時間回調(diào)再執(zhí)行。
認定只要執(zhí)行1次的,就不需要WeakSelf,除非第三方框架的設(shè)計者造孽留坑,忘了在存檔block執(zhí)行后補上block=nil這一刀。
5:消滅賦值的引用計數(shù):
繼續(xù)發(fā)揮想象力,既然存的時候,會增加一次引用,辣么,讓它不增加引用不就好了:
@implementation BlockTable -(void)setAddCell:(AddCellBlock)addCell { __weak AddCellBlock addCellWeak=addCell; _addCell=addCellWeak; }
我們先給這個block定義一個弱引用,然后再賦值給_addCell,運行看看:
哇草,成功了!計數(shù)器為2,正常釋放了,看來自己的想象力,還是可以的??!
接下來,我們補充完善一下代碼,增加一個reloadData方法,方法里調(diào)用事件。
完整的代碼如下:
@interface BlockTable : NSObject typedef void (^AddCellBlock)(); @property (nonatomic,copy)AddCellBlock addCell; -(void)reloadData; @end @implementation BlockTable -(void)setAddCell:(AddCellBlock)addCell { __weak AddCellBlock addCellWeak=addCell; _addCell=addCellWeak; } -(void)reloadData { if(self.addCell) { self.addCell(); self.addCell();//沒事來兩次,模擬table多次循環(huán)清加cell } } -(void)dealloc { NSLog(@"Table relase"); } @end
修改一下增加日志輸出,現(xiàn)在再執(zhí)行一下看看:
一切看起來都相當完美,不需要引入第三,需要多次使用的,只是在存的時候,存?zhèn)€弱引用,就搞定了。
6:弱引用降低計數(shù)的缺陷:
塊的定義,和使用的場景,必須在同一個函數(shù)。
說白了就是塊離開函數(shù)體就會消亡,所以要用要趕緊,且用且珍惜。
正常一個Table寫完代碼reloadData后,數(shù)據(jù)出來了。
但如果后面還跟有一個刷新重新加載的功能?
而這個重新調(diào)用reloadData的地方,可能跟block不在同一個函數(shù),比如代碼像這樣:
-(void)start { BlockTable *table=[BlockTable new]; self.table=table;//搞到全局變量中 table.addCell = ^{ __weak typeof(table) this=table; NSLog(@"addCell call"); }; [table reloadData]; NSLog(@"table retain = %ld",CFGetRetainCount((__bridge CFTypeRef)(table))); } -(void)reflesh { [self.table reloadData]; }
給外面的類定義了一個table屬性,然后調(diào)用完start后再調(diào)用reflesh,運行,會怎樣呢?
出現(xiàn)了IOS上最可怕的EXC_BAD_ACCESS 野指針錯誤。
對于block離開函數(shù)后,消亡了容易理解,只是這里:
這什么是直接拋異常?哥不是作了判斷了么?
讓我們換種代碼寫法:
另外從上圖看:_addCell還是有值的。
為什么if(self.addCell)
判斷就直接死,if(_addCell)
卻沒死呢?
正常self.addCell
正常不是也return _addCell么?
這個問題,留給讓你們思考了。
最可怕的,還是下面的這段話:
7:避開野指針,仍是弱引用,功能不變
OK,繼續(xù)發(fā)揮想象力,看看怎么避開野指針,同時還是實現(xiàn)上述的效果:
1:把block屬性從copy改成weak
@property (nonatomic,weak)AddCellBlock addCell;
2:賦值代碼手工copy:
-(void)setAddCell:(AddCellBlock)addCell { addCell=[addCell copy]; _addCell=addCell; //_addCell=[addCell copy];這樣簡寫是不行的,不明白為蝦米呢 // 原來是這樣寫的: // __weak AddCellBlock addCellWeak=addCell; // _addCell=addCellWeak ; }
再次運行,神奇的事情發(fā)生了:
流程還是很順,不會有野批針異常,Table也釋放了。
唯一的遺憾,就是跳出函數(shù)后,block不能再復(fù)用了:
8:block的copy方法:
對于默認傳進來的block(有三種形態(tài):全局、棧、堆)
全局 copy 還是全局
堆 copy 還是堆
棧 copy 變成堆
說白了,copy只對類型是棧是才有效。
這是因為:棧的block,在執(zhí)行完后出括號后,直接是銷毀對象。
如果有弱引用過去,會造成野指針。
而其它兩種類型,銷毀時,會將指針指向一個空指針。
addCell=[addCell copy] 和默認copy的屬性 _addCell=addCell 也是執(zhí)行了copy操作。
執(zhí)行后,addCell的類型就變成堆形態(tài),這樣銷毀的時候,是空指針。
9:空指針和野指針的區(qū)別:
空指針:指向一個:人為創(chuàng)造的一個指針,它的名字叫空,有座空房子,里面什么也沒有。
野指針:就是指向的都不知哪去了,連空房子都木有。
10:擴展想象力,如何消滅引用數(shù),還能長久保留?
弱引用的壞處,就是block出了函數(shù),就不再可用這個block了。
那還能怎么辦呢?沒事,我還有想象力!?。。?!
如果block可以重建呢?
比如:
1:將block轉(zhuǎn)成字符串存檔,適當時機還原回來重新賦值
2:將block序列化保存,適當時機還原回來?
3:runtime讀取block的__FuncPtr,存檔再動態(tài)創(chuàng)建?
偽代碼大體如下:
-(void)setAddCell:(AddCellBlock)addCell { addCell=[addCell copy]; _addCell=addCell; //_addCell=[addCell copy];這樣簡寫是不行的,不明白為蝦米呢 // 原來是這樣寫的: // __weak AddCellBlock addCellWeak=addCell; // _addCell=addCellWeak ; //存檔block的字符串 } -(void)reloadData { if(!_addCell) { //從存檔的block字符串還原block //_addCell=還原block } if(_addCell) { _addCell(); _addCell(); } }
那么就剩下兩個問題?
1:怎么把block存檔?
2:怎么將存檔數(shù)據(jù)還原成block。
對搞C#的來說,這些都家常便飯,oc這塊還不熟,有路過的朋友可順路給支支招??!
11:如果第10的方式解決不了,就只能,只能,引入時機第三者了
不過這個引入第三者,只是一個時機切入點,在這個時機觸發(fā)的時候,將其中的一方的引用設(shè)置為nil。
像Sagit框架的布局方面的時機,就選在導(dǎo)航回退等事件中處理。
不過這里需要一個小技巧:
在存檔block時,不一定要存在當前對象,也可以用一個統(tǒng)一的全局block管理起來。
這樣在業(yè)務(wù)處理時,根據(jù)業(yè)務(wù)情況,從全局block里來移除某些block即可。
具體取決于業(yè)務(wù),所以這個就不展開了。
總結(jié):
相信,一路看下,看懂了,后續(xù)的情況,基本上已經(jīng)用不上WeakSelf這東西了,因為像一個block,其生命周期必須和持有者保持一致的,還是挺少的。
而這種少的情況,如果第10步解決了,基本就全都解決了,解決不了,還有11。
相信讀完此文,如果能完全理解,你就再也看不到block前WeakSelf這種,WeakSelf也沒有存在必要了。
最后,歡迎大伙關(guān)注IT連創(chuàng)業(yè),雖然最近我都在折騰IOS,哈哈。
不過IOS基礎(chǔ)還是要打勞,后續(xù)產(chǎn)品改進起來才有質(zhì)的飛躍。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
詳解iOS如何讓Lottie使用網(wǎng)絡(luò)資源做動畫的實現(xiàn)
這篇文章主要為大家介紹了iOS如何讓Lottie使用網(wǎng)絡(luò)資源做動畫實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02iOS開發(fā)中關(guān)鍵字const/static/extern、UIKIT_EXTERN的區(qū)別和用法
這篇文章主要介紹了iOS 關(guān)鍵字const/static/extern、UIKIT_EXTERN區(qū)別和用法,需要的朋友可以參考下2017-12-12iOS自定義UICollectionViewFlowLayout實現(xiàn)圖片瀏覽效果
這篇文章主要介紹了iOS自定義UICollectionViewFlowLayout實現(xiàn)圖片瀏覽效果的相關(guān)資料,需要的朋友可以參考下2016-03-03