iOS Target-Action模式下內(nèi)存泄露問(wèn)題深入探究

在我們?nèi)粘i_(kāi)發(fā)中,我們或多或少的都會(huì)遇到循環(huán)引用的問(wèn)題。其實(shí)問(wèn)題的實(shí)質(zhì)就是造成了互相持有的關(guān)系,在對(duì)象釋放的時(shí)候,就好像產(chǎn)生了一個(gè)死鎖一樣,系統(tǒng)沒(méi)有辦法釋放其中的任何一個(gè)對(duì)象,就造成了內(nèi)存泄露的問(wèn)題。我們都知道NSTimer是其中的典型??墒菫槭裁蠢^承自UIControl類(lèi)的對(duì)象同樣調(diào)用addtarget的方法就不會(huì)造成內(nèi)存泄露的問(wèn)題呢?現(xiàn)在就開(kāi)啟本文的探索。

1.Target-Action模式

這是蘋(píng)果做的一種設(shè)計(jì)模式,在設(shè)置target對(duì)象之后,該對(duì)象可以執(zhí)行對(duì)應(yīng)的Selector。我們可以看到在我們的項(xiàng)目中,經(jīng)常在使用UIButton,UISegmentedControl等繼承自UIControl的類(lèi)時(shí)調(diào)用

- (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;這個(gè)方法,但是從代碼可讀性的角度考慮,這樣的并不是特別的好,我們也經(jīng)常為這些類(lèi)寫(xiě)擴(kuò)展,完成block的調(diào)用??蛇@種方式為什么會(huì)存在,不是設(shè)計(jì)成block回調(diào)。其實(shí)這個(gè)原因個(gè)人認(rèn)為有兩個(gè)。

1.在storyboard下,將selector連接出來(lái)就是使用的這一模式,這樣的模式個(gè)人認(rèn)為在這種情況下還是很強(qiáng)大的。

2.其實(shí)這個(gè)模式是伴隨整個(gè)OC的版本的,而block是在iOS4的時(shí)候才推出的。所以在開(kāi)始的時(shí)候Target-Action的模式看起來(lái)真的很強(qiáng)大。而且我發(fā)現(xiàn)在iOS10中,蘋(píng)果已經(jīng)在NSTimer類(lèi)中添加了block的方式,其實(shí)這時(shí)候我們循環(huán)引用的問(wèn)題可以用block的方式,但也只能在iOS10的時(shí)候使用。

其它關(guān)于此模式的思考不再擴(kuò)展,網(wǎng)上相關(guān)的文章很多,Google一下有很多,本文的核心在于去深入的研究一小下。

2.UIControl和NSTimer下調(diào)用addTarget方法到底為什么不同

Target-Action模式.

上面是我們調(diào)用的時(shí)候會(huì)調(diào)用的方法,但是UIButton不會(huì)造成循環(huán)引用,但是NSTimer為什么會(huì)造成循環(huán)引用的問(wèn)題呢?從這個(gè)問(wèn)題出發(fā),我查看了UIControl和NSTimer的官方文檔,對(duì)于這里的解釋真的是聊聊無(wú)幾,我沒(méi)有找到強(qiáng)有力的證據(jù)能夠說(shuō)明其中的原因,但是我們思考下猜想應(yīng)該是UIControl機(jī)制下一定是底層將self弱引用了,解開(kāi)了循環(huán)的鏈,所以UIControl下沒(méi)有這樣的操作。從這個(gè)角度出發(fā),我去Google了一下,看了一些相關(guān)的文章,發(fā)現(xiàn)可以在堆棧信息中看出一些貓膩。那么現(xiàn)在看一下我們堆棧信息中我們能夠發(fā)現(xiàn)什么.

首先我們看一下使用LLDB方案我們獲取到的信息是不是可以為我們所用呢?我分別在兩個(gè)addTarget方法出下了斷點(diǎn)。然后在控制臺(tái)輸入dis,打印當(dāng)前堆棧的調(diào)用信息,結(jié)果如下。

在看到這個(gè)堆棧信息的時(shí)候我發(fā)現(xiàn)對(duì)于同一塊內(nèi)存的引用方式竟然完全是一樣的,這就更加增加了我的好奇,這里的堆棧信息完全不能解答現(xiàn)有的疑問(wèn),還有其他的方式么?后來(lái)想到調(diào)用方法的堆棧,去看方法到底做了什么也許更清晰,我們能夠清晰地知道方法中用到了什么,于是在項(xiàng)目中添加了如下兩個(gè)symbolic breakpoint斷點(diǎn)踐行進(jìn)行測(cè)試。

symbolic breakpoints

此時(shí)重新跑程序,在每個(gè)斷點(diǎn)執(zhí)行的時(shí)候,我們可以看到對(duì)應(yīng)的堆棧信息如下。

UIControl 下的target
NSTimer下的target

通過(guò)上圖的兩張堆棧信息,我們可以看到在UIControl下的target的持有方式確實(shí)是weakRetained弱持有的方式解開(kāi)了引用循環(huán),所以我們?cè)谑褂脮r(shí)不會(huì)出現(xiàn)引用循環(huán)的問(wèn)題。但是在NSTimer下,我看到的堆棧信息中看到這行代碼的時(shí)候,開(kāi)始明白機(jī)制的原理了,在NSTimer機(jī)制下對(duì)Target持有的方式使用的是autorelease的方式,也就是說(shuō)target會(huì)在runloop下一次執(zhí)行的時(shí)候查看這塊區(qū)域是否進(jìn)行釋放,這也就能解釋為什么我們?nèi)绻麑epeats屬性設(shè)置成NO內(nèi)存可以釋放的原因,以及為什么將self設(shè)置成nil后內(nèi)存依然不釋放的原因。接下來(lái)我對(duì)invalidate方法打印堆棧信息,但是我發(fā)現(xiàn)沒(méi)有對(duì)應(yīng)方法的堆棧信息,反而會(huì)再次調(diào)用addtarget方法,這是我聯(lián)想到NSTimer的官方文檔中有說(shuō)明,一旦調(diào)用了invalidate方法之后,這個(gè)timer就不能再使用,我認(rèn)為底層這個(gè)時(shí)候就是個(gè)當(dāng)前的timer進(jìn)行了一個(gè)target的重定向,正好執(zhí)行一次runloop的timerobserver監(jiān)聽(tīng),將之前的內(nèi)存釋放掉了,然后解開(kāi)了引用的循環(huán),現(xiàn)在我們已經(jīng)明白了原理,那么我們就從原理出發(fā),看看現(xiàn)有的解決方案是否合理。

3.從根源出發(fā),看看現(xiàn)有解決方案

我百度了一下NSTimer循環(huán)引用的問(wèn)題,歸納總結(jié)一下,大概的解決方案是

1)及時(shí)的調(diào)用invalidate方法?

2)給NSTimer寫(xiě)一個(gè)擴(kuò)展類(lèi),然后使用block回調(diào)的方式

3)在給self增加代理的時(shí)候創(chuàng)建中間層代理。

那么我們現(xiàn)在看到三個(gè)方法的時(shí)候,首先知道方法一重定向的方式在上邊已經(jīng)知曉了能夠解決問(wèn)題的原因,那么我們看下方法2和方法3是不是能夠解決問(wèn)題。

首先方法二實(shí)現(xiàn)的核心代碼大致如下

看完上邊的代碼,我們發(fā)現(xiàn)此時(shí)的target為NSTimer類(lèi)對(duì)象,其實(shí)本身就是一個(gè)單例,所以會(huì)伴隨程序的整個(gè)生命周期,所以程序是不是保留對(duì)他的循環(huán)引用都已經(jīng)無(wú)所謂,所以不會(huì)造成內(nèi)存泄露的問(wèn)題,但是我們需要思考的一件事,我們的程序還是依然會(huì)在我們看不到的地方不停地去執(zhí)行repeats事件,如果我們程序中有很多的NSTimer這樣的事件用這樣的方法,因?yàn)椴惶私獾讓拥木唧w實(shí)現(xiàn),但是我認(rèn)為這樣的方案對(duì)于程序的性能上會(huì)有一定的影響。但是對(duì)于內(nèi)存釋放上的考量我認(rèn)為問(wèn)題已經(jīng)得到了解決。所以我的建議是即便用這樣的方案也要及時(shí)的調(diào)用invalidate方法,否則程序的性能會(huì)受到影響,當(dāng)然我們的項(xiàng)目也用到了很多這樣的方法,因?yàn)槲艺J(rèn)為在代碼可讀性的角度出發(fā),所以這樣使用時(shí)不要覺(jué)得內(nèi)存問(wèn)題解決了就完事了。

看完了方法2中的問(wèn)題,我們現(xiàn)在再來(lái)看方法3是如何解開(kāi)循環(huán)引用的。我在github上下載了一個(gè)相關(guān)demo,核心源碼大致如下。

我們看到作者重新寫(xiě)了一個(gè)類(lèi),使用這個(gè)類(lèi)老作為target,解開(kāi)了循環(huán)引用,這個(gè)時(shí)候測(cè)試delloc方法就不會(huì)出現(xiàn)循環(huán)引用,看似創(chuàng)建timer類(lèi)的解決了循環(huán)引用的問(wèn)題。但是我測(cè)試驗(yàn)證了我的想法,作者創(chuàng)建的weakTimer對(duì)象就會(huì)常駐內(nèi)存一直都無(wú)法釋放掉的。其實(shí)如果作者在中間層將target指向一個(gè)類(lèi)對(duì)象,我認(rèn)為這樣的方法還是能夠解決很多問(wèn)題的,但是關(guān)鍵還是在于上邊所說(shuō),還是可能會(huì)引發(fā)性能問(wèn)題,而且還需要在寫(xiě)對(duì)應(yīng)的invalidate方法等,我覺(jué)得這個(gè)時(shí)候其實(shí)這樣的方法本身意義就已經(jīng)不大了。所以對(duì)于中間代理的方式,個(gè)人認(rèn)為真的可用性不大,增加了程序的復(fù)雜度,還不能本質(zhì)上的解決問(wèn)題。

所以最后對(duì)NSTimer的使用個(gè)人建議就是創(chuàng)建擴(kuò)展,我認(rèn)為這樣的方式代碼的可讀性是最強(qiáng)的。但是注意和平時(shí)使用時(shí)一樣及時(shí)的調(diào)用invalidate方法,畢竟不是能看到的問(wèn)題解決了,我們的程序就沒(méi)有問(wèn)題了。

希望本文能給大家在開(kāi)發(fā)中帶來(lái)幫助,最近一直都在做一些項(xiàng)目?jī)?yōu)化上的事,最近有時(shí)間會(huì)分享關(guān)于如何讓程序變得更省電上的思考和一些優(yōu)化上的小經(jīng)驗(yàn)。如果文章中的觀點(diǎn)有任何問(wèn)題,煩請(qǐng)留言區(qū)指出,我會(huì)立即進(jìn)行更正,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • __block和__weak修飾符的區(qū)別其實(shí)是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,606評(píng)論 0 6
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,656評(píng)論 30 472
  • 多線程、特別是NSOperation 和 GCD 的內(nèi)部原理。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景。SDWebImage的原...
    LZM輪回閱讀 2,131評(píng)論 0 12
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛(ài)閱讀 2,255評(píng)論 0 7
  • 史上最全的iOS面試題及答案 iOS面試小貼士———————————————回答好下面的足夠了----------...
    Style_偉閱讀 2,580評(píng)論 0 35

友情鏈接更多精彩內(nèi)容