在咱們平常開發中,咱們或多或少的都會遇到循環引用的問題。其實問題的實質就是形成了互相持有的關係,在對象釋放的時候,就好像產生了一個死鎖同樣,系統沒有辦法釋放其中的任何一個對象,就形成了內存泄露的問題。咱們都知道NSTimer是其中的典型。但是爲何繼承自UIControl類的對象一樣調用addtarget的方法就不會形成內存泄露的問題呢?如今就開啓本文的探索。git
這是蘋果作的一種設計模式,在設置target對象以後,該對象能夠執行對應的Selector。咱們能夠看到在咱們的項目中,常常在使用UIButton,UISegmentedControl等繼承自UIControl的類時調用github
- (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;這個方法,可是從代碼可讀性的角度考慮,這樣的並非特別的好,咱們也常常爲這些類寫擴展,完成block的調用。可這種方式爲何會存在,不是設計成block回調。其實這個緣由我的認爲有兩個。設計模式
1.在storyboard下,將selector鏈接出來就是使用的這一模式,這樣的模式我的認爲在這種狀況下仍是很強大的。oop
2.其實這個模式是伴隨整個OC的版本的,而block是在iOS4的時候才推出的。因此在開始的時候Target-Action的模式看起來真的很強大。並且我發如今iOS10中,蘋果已經在NSTimer類中添加了block的方式,其實這時候咱們循環引用的問題能夠用block的方式,但也只能在iOS10的時候使用。性能
其它關於此模式的思考再也不擴展,網上相關的文章不少,Google一下有不少,本文的核心在於去深刻的研究一小下。測試
上面是咱們調用的時候會調用的方法,可是UIButton不會形成循環引用,可是NSTimer爲何會形成循環引用的問題呢?從這個問題出發,我查看了UIControl和NSTimer的官方文檔,對於這裏的解釋真的是聊聊無幾,我沒有找到強有力的證據可以說明其中的緣由,可是咱們思考下猜測應該是UIControl機制下必定是底層將self弱引用了,解開了循環的鏈,因此UIControl下沒有這樣的操做。從這個角度出發,我去Google了一下,看了一些相關的文章,發現能夠在堆棧信息中看出一些貓膩。那麼如今看一下咱們堆棧信息中咱們可以發現什麼.
優化
首先咱們看一下使用LLDB方案咱們獲取到的信息是否是能夠爲咱們所用呢?我分別在兩個addTarget方法出下了斷點。而後在控制檯輸入dis,打印當前堆棧的調用信息,結果以下。設計
在看到這個堆棧信息的時候我發現對於同一塊內存的引用方式居然徹底是同樣的,這就更加增長了個人好奇,這裏的堆棧信息徹底不能解答現有的疑問,還有其餘的方式麼?後來想到調用方法的堆棧,去看方法到底作了什麼也許更清晰,咱們可以清晰地知道方法中用到了什麼,因而在項目中添加了以下兩個symbolic breakpoint斷點踐行進行測試。3d
此時從新跑程序,在每一個斷點執行的時候,咱們能夠看到對應的堆棧信息以下。代理
經過上圖的兩張堆棧信息,咱們能夠看到在UIControl下的target的持有方式確實是weakRetained弱持有的方式解開了引用循環,因此咱們在使用時不會出現引用循環的問題。可是在NSTimer下,我看到的堆棧信息中看到這行代碼的時候,開始明白機制的原理了,在NSTimer機制下對Target持有的方式使用的是autorelease的方式,也就是說target會在runloop下一次執行的時候查看這塊區域是否進行釋放,這也就能解釋爲何咱們若是將repeats屬性設置成NO內存能夠釋放的緣由,以及爲何將self設置成nil後內存依然不釋放的緣由。接下來我對invalidate方法打印堆棧信息,可是我發現沒有對應方法的堆棧信息,反而會再次調用addtarget方法,這是我聯想到NSTimer的官方文檔中有說明,一旦調用了invalidate方法以後,這個timer就不能再使用,我認爲底層這個時候就是個當前的timer進行了一個target的重定向,正好執行一次runloop的timerobserver監聽,將以前的內存釋放掉了,而後解開了引用的循環,如今咱們已經明白了原理,那麼咱們就從原理出發,看看現有的解決方案是否合理。
我百度了一下NSTimer循環引用的問題,概括總結一下,大概的解決方案是
1)及時的調用invalidate方法
2)給NSTimer寫一個擴展類,而後使用block回調的方式
3)在給self增長代理的時候建立中間層代理。
那麼咱們如今看到三個方法的時候,首先知道方法一重定向的方式在上邊已經知曉了可以解決問題的緣由,那麼咱們看下方法2和方法3是否是可以解決問題。
首先方法二實現的核心代碼大體以下
看完上邊的代碼,咱們發現此時的target爲NSTimer類對象,其實自己就是一個單例,因此會伴隨程序的整個生命週期,因此程序是否是保留對他的循環引用都已經無所謂,因此不會形成內存泄露的問題,可是咱們須要思考的一件事,咱們的程序仍是依然會在咱們看不到的地方不停地去執行repeats事件,若是咱們程序中有不少的NSTimer這樣的事件用這樣的方法,由於不太瞭解底層的具體實現,可是我認爲這樣的方案對於程序的性能上會有必定的影響。可是對於內存釋放上的考量我認爲問題已經獲得瞭解決。因此個人建議是即使用這樣的方案也要及時的調用invalidate方法,不然程序的性能會受到影響,固然咱們的項目也用到了不少這樣的方法,由於我認爲在代碼可讀性的角度出發,因此這樣使用時不要以爲內存問題解決了就完事了。
看完了方法2中的問題,咱們如今再來看方法3是如何解開循環引用的。我在github上下載了一個相關demo,核心源碼大體以下。
咱們看到做者從新寫了一個類,使用這個類老做爲target,解開了循環引用,這個時候測試delloc方法就不會出現循環引用,看似建立timer類的解決了循環引用的問題。可是我測試驗證了個人想法,做者建立的weakTimer對象就會常駐內存一直都沒法釋放掉的。其實若是做者在中間層將target指向一個類對象,我認爲這樣的方法仍是可以解決不少問題的,可是關鍵仍是在於上邊所說,仍是可能會引起性能問題,並且還須要在寫對應的invalidate方法等,我以爲這個時候其實這樣的方法自己意義就已經不大了。因此對於中間代理的方式,我的認爲真的可用性不大,增長了程序的複雜度,還不能本質上的解決問題。
因此最後對NSTimer的使用我的建議就是建立擴展,我認爲這樣的方式代碼的可讀性是最強的。可是注意和平時使用時同樣及時的調用invalidate方法,畢竟不是能看到的問題解決了,咱們的程序就沒有問題了。
但願本文能給你們在開發中帶來幫助,最近一直都在作一些項目優化上的事,最近有時間會分享關於如何讓程序變得更省電上的思考和一些優化上的小經驗。若是文章中的觀點有任何問題,煩請留言區指出,我會當即進行更正,謝謝。