姓名:雷瀟 16030110083????
轉(zhuǎn)載自:http://www.lai18.com/content/24631425.html
【嵌牛導讀】iOS10已經(jīng)發(fā)布了一段時間泽疆,iOS10的各種適配相信大家已經(jīng)完成竞思。本文將講述的是關(guān)于iOS10內(nèi)核的一個小改動,慣例,本文屬于進階性技術(shù)文艳吠,不會講解API的使用主卫,要求讀者對RunLoop有一定的認知,感謝網(wǎng)友@送你的獨白么 提供的SDK虾宇。
【嵌牛鼻子】ios系統(tǒng)搓彻, runloop
【嵌牛提問】ios10怎么進行定是消息的改動?
【嵌牛正文】當我們的程序需要定時處理一些事件時嘱朽,我們就會用到定時器旭贬,常用的定時器有NSTimer,CADisplayLink搪泳,GCD Timer稀轨,本文主要針對NSTimer和CADisplayLink進行講述,因為這兩者跟你的Application更為密切岸军。
NSTimer和CADisplayLink都是建立在CFRunLoopTimer之上的抽象物奋刽,但有趣的是,蘋果只提供了NSTimer和CFRunLoopTimer互轉(zhuǎn)的Toll-Free Bridge艰赞,并沒有提供CADisplayLink和CFRunLoopTimer互轉(zhuǎn)的接口佣谐,因此一些開發(fā)者對此產(chǎn)生了一些猜想,有的人認為方妖,CADisplayLink是用GCD Dispatch Source來實現(xiàn)的狭魂,有的人認為,CADisplayLink是用RunLoopSource來實現(xiàn)的,但這些猜想的依據(jù)都太容易被推翻了雌澄。如果CADisplayLink是用GCD Dispatch Source來實現(xiàn)的斋泄,那么CADisplayLink是怎么在你所創(chuàng)建的子線程中工作的呢?如果CADisplayLink是用RunLoopSource來實現(xiàn)的掷伙,會不會多此一舉是己?
CFRunLoopTimer是RunLoop的定時源,與Source1(Port)一樣任柜,都屬于端口事件源卒废,但不同的是,每一個Source1都有與之對應的端口宙地,而一個RunLoopMode中的所有CFRunLoopTimer共用一個端口(Mode Timer Port)摔认,CFRunLoopTimer在RunLoop中的工作原理如下圖。
定時源工作 從定時源在RunLoop中的工作原理我們得知宅粥,只要符合條件的定時器都會被觸發(fā)参袱,也就是說,在同一次Loop中秽梅,可能會執(zhí)行幾個定時器的回調(diào)抹蚀。
很多講述定時器的技術(shù)文中都有這么一個觀點,如果一個定時器錯過了本次可以觸發(fā)的時間點企垦,那么定時器將跳過這個時間點环壤,等待下一個時間點的到來,這個觀點似乎是從官方文檔中得來的钞诡,但這個觀點跟定時器在RunLoop中的工作原理并不符郑现。定時消息從內(nèi)核發(fā)出,消息在消息中心等待被處理荧降,RunLoop每次Loop都會去消息中心查找相應的端口消息接箫,若找到相應的端口消息就會進行處理,所以朵诫,即使當前RunLoop正在執(zhí)行一個耗時很長的任務(wù)辛友,當任務(wù)執(zhí)行完進入下一次Loop時,那些未被處理的消息仍然會被處理剪返。經(jīng)過大量測試表明废累,定時消息并不會因延遲而掉失。
關(guān)于RunLoop随夸,官方文檔在這一部份的勘誤比較多九默,經(jīng)常會出現(xiàn)文檔的介紹跟源碼不同的情況,所以想學習RunLoop的同學宾毒,建議看源碼和自己做測試驼修,特別是自己做測試殿遂。
NSTimer和CADisplayLink最大的區(qū)別在于信號的發(fā)射頻率不同,CADisplayLink的發(fā)射頻率固定在16.67ms一次乙各,而NSTimer則可以自由定義墨礁。我在頁面間跳轉(zhuǎn)的性能優(yōu)化(一)中曾經(jīng)提到過,不是必要的情況下耳峦,都不要選擇使用CADisplayLink作為定時器恩静,因為它會使目標RunLoop一直處理活躍狀態(tài)。下面通過一個例子來看看實際的效果蹲坷,創(chuàng)建一個CADisplayLink定時器驶乾,設(shè)置為100秒后觸發(fā),然后觀察目標RunLoop的狀態(tài)循签。
CADisplayLink 從實際效果我們可以看到级乐,目標RunLoop一直處于活躍狀態(tài),不斷地處理內(nèi)核發(fā)出的信號县匠,直到RunLoop Stop或CADisplayLink定時器被移除风科。同樣的條件,我們把定時器換成NSTimer來觀察實際情況乞旦。
NSTimer 與CADisplayLink的固定信號不同贼穆,NSTimer的信號間隔完全是由使用者來定義。所以兰粉,除非你需要實現(xiàn)定時動畫故痊,不然都不要選擇使用CADisplayLink作為定時器,它不僅會損耗大量的CPU資源亲桦,還會響應目標RunLoop處理其它事件源崖蜜。
改動
前面介紹了定時器的工作原理浊仆,現(xiàn)在來看看實際的改動客峭,從一個例子入手進行講述。現(xiàn)在有頁面A抡柿,B舔琅,頁面A,B各有一個按鈕洲劣,頁面A的按鈕用來進入頁面B备蚓,進入頁面B后創(chuàng)建一個子線程,然后向子線程添加一個定時器并啟動RunLoop囱稽,頁面B的按鈕用于停止定時器郊尝,并返回頁面A,頁面B被釋放時會在dealloc方法里輸出dealloc战惊,編譯環(huán)境是ARC流昏,下圖為頁面B的代碼,Gif圖分別是iOS10與iOS9的實際運行效果。
頁面B代碼
iOS10
iOS9 一般情況下况凉,從頁面B返回到頁面A后谚鄙,頁面B會被釋放,頁面B的dealloc方法會輸出dealloc刁绒,但從實際的運行效果可以看到闷营,在iOS10環(huán)境下頁面B并沒有被釋放,WTF知市,為什么iOS10環(huán)境下會這樣傻盟?要回答這個問題,我們需要先知道iOS10的改動是什么嫂丙。
若目標RunLoop當前沒有定時源需要處理(像上面的例子那樣莫杈,子線程RunLoop只有一個定時器,該定時器移除后奢入,則子線程RunLoop沒有定時源需要處理)筝闹,則通知內(nèi)核不需要再向當前Timer Port發(fā)送定時消息并移除該Timer Port。在iOS10環(huán)境下腥光,當移除Timer Port后关顷,內(nèi)核會把消息列表中與該Timer Port相應的定時消息移除,而iOS10以前的環(huán)境下武福,當移除Timer Port后议双,內(nèi)核不會把消息列表中與該Timer Port相應的定時消息移除。iOS10的處理是更為合理的捉片,iOS10以前的處理可能是歷史遺留問題吧平痰。
看回上面的例子,例子中遇到的問題是頁面B返回后并沒有被釋放伍纫,即頁面B的內(nèi)存被強制保留了宗雇,所以我們現(xiàn)在需要知道的是頁面B為什么被強制保留了。在頁面B中我們創(chuàng)建了一個子線程莹规,子線程的主函數(shù)是頁面B的對象函數(shù)赔蒲,這可能是導致頁面B被強制保留的原因,所以良漱,我們需要知道子線程開啟前后舞虱,頁面B對象的引用計數(shù)是否有增加。
創(chuàng)建并開啟子線程
頁面B的引用計數(shù) 從輸出的信息我們得知母市,創(chuàng)建子線程后矾兜,Target會被強制保留,直到子線程的主函數(shù)返回患久。引用計數(shù)在很多時候可以幫助我們了解內(nèi)存的使用情況椅寺,但在ARC編譯環(huán)境下舶沿,我們無法直接使用retainCount方法來獲取一個對象的引用計數(shù),所以配并,我們需要做額外的處理括荡。
獲取對象的引用計數(shù) 回到例子中,我們知道了頁面B被強制保留的原因后溉旋,就知道了怎么解決畸冲,只需要退出子線程即可,子線程之所以可以一直存活观腊,是因為啟動了RunLoop邑闲,所以,我們只需要退出RunLoop梧油,子線程的主函數(shù)就會返回苫耸。例子中涉及到線程異步的問題,定時器是在子線程RunLoop中注冊的儡陨,但定時器的移除操作卻是在主線程褪子,由于子線程RunLoop處理完一次定時信號后,就會進入休眠狀態(tài)骗村。在iOS10以前的環(huán)境下嫌褪,定時器被移除后,內(nèi)核仍然會向?qū)腡imer Port發(fā)送一次信號胚股,所以子線程RunLoop接收到信號后會被喚醒笼痛,由于沒有定時源需要處理,所以RunLoop會直接跳轉(zhuǎn)到判斷階段琅拌,判斷階段會檢測當前RunLoopMode是否有事件源需要處理缨伊,若沒有事件源需要處理,則會退出RunLoop进宝。由于例子中子線程RunLoop的當前RunLoopMode只有一個定時器刻坊,而定時器被移除后,RunLoopMode就沒有了需要處理的事件源即彪,所以會退出RunLoop紧唱,子線程的主函數(shù)也因此返回活尊,頁面B對象被釋放隶校。
但在iOS10環(huán)境下,當定時器被移除后蛹锰,內(nèi)核不再向?qū)腡imer Port發(fā)送任何信號深胳,所以子線程RunLoop一直處于休眠狀態(tài)并沒有退出,而我們只需要手動喚醒RunLoop即可铜犬。
更改頁面B代碼
iOS10 例子中所遇到的問題已經(jīng)解決轻庆,但看完這個例子敛劝,可能你會有疑問,這個例子講述的情況有實戰(zhàn)意義夸盟?這個例子是從一個國外成熟產(chǎn)品所提供的配套SDK中簡化而來,配套的SDK用于與產(chǎn)品進行對接上陕。額......實話說桩砰,當我看到這個處理方式的時候释簿,我被震驚了,沒想到一個成熟產(chǎn)品所提供的配套SDK會出現(xiàn)這樣的問題庶溶,讓我更震驚的是煮纵,隨后在其它SDK中也發(fā)現(xiàn)了這個問題,這......
我們回頭來看看例子中的處理方式偏螺,例子中醉途,子線程RunLoop的退出依賴于RunLoopMode的事件源為空,這種RunLoop的退出方式是極不穩(wěn)定的砖茸,因為系統(tǒng)有很多API會向目標RunLoopMode添加額外的事件源來處理系統(tǒng)事件的隘擎,所以這種方式是不能確保一定可以退出RunLoop的。正確的方式應該是配對調(diào)用CFRunLoopRun( )凉夯,CFRunLoopStop( )來啟動和退出RunLoop货葬,需要注意的是,除非你要創(chuàng)建一個單例線程劲够,不然不要使用[runloop run]方法來啟動RunLoop震桶,因為使用run方法啟動RunLoop后,唯一退出RunLoop的方式是當前RunLoopMode的事件源為空征绎,而我們知道這種方式本身是極不穩(wěn)定的蹲姐。