1畜伐、什么是Run Loop辣吃?
(1)、Run Loop是線程的一項基礎配備晌涕,它的主要作用是來讓某一條線程在有任務的時候工作狐赡、沒有任務的時候休眠撞鹉。
(2)、線程和 Run Loop 之間的關(guān)系是一一對應的颖侄,但是并不是說新開一條線程就會自動生成這條線程對應的Run Loop鸟雏,每一條線程里的Run Loop都是需要主動去獲取,并且啟動它览祖,它才會開始運作的孝鹊。主線程的Run Loop之所以不用我們手動去獲取啟動它,是因為在App啟動的時候它已經(jīng)默認啟動了展蒂。
(3)又活、Run Loop的核心是__CFRunLoopRun()函數(shù),它的主要結(jié)構(gòu)是一個do-while循環(huán)锰悼,當一個Run Loop啟動后柳骄,它在這個do-while循環(huán)里的基本運作如下:
如果有事件源(見后文詳解),它就會一直在這個do-while循環(huán)中運作:當事件源有發(fā)出消息箕般,它就處理消息并繼續(xù)循環(huán)耐薯;當事件源沒有發(fā)出消息時,它就停在休眠的代碼處丝里,等待事件源發(fā)出消息來喚醒它處理消息并繼續(xù)循環(huán)可柿;
如果沒有事件源,它就會直接退出循環(huán)丙者。
(4)、線程里的Run Loop是在第一次獲取的時候才會去創(chuàng)建营密,而它的銷毀發(fā)生在以下3種情況下:
線程結(jié)束械媒;
因為沒有任何事件源而退出循環(huán);
手動結(jié)束了這個Run Loop。
2纷捞、Run Loop的結(jié)構(gòu)痢虹?
(1)、Run Loop的結(jié)構(gòu)需要涉及到以下4個概念:Run Loop Mode主儡、Input Source奖唯、Timer Source和Run Loop Observer。
(2)糜值、Run Loop Mode是Run Loop的模式丰捷。一個Run Loop可以有很多種mode,但是每次啟動一個Run Loop的時候只能選擇一種mode寂汇,如果要切換mode病往,需要先退出當前Run Loop再重新使用新的mode進入Run Loop。關(guān)于這個的演示詳見后文5(8)骄瓣。
最常見的Run Loop Mode有兩個:NSDefaultRunLoopMode和UITrackingRunLoopMode停巷,主線程的RunLoop 里預置了這兩個 Mode。其中NSDefaultRunLoopMode在App平常狀態(tài)下主線程Run Loop的mode榕栏,UITrackingRunLoopMode是在拖動UIScrollView的時候主線程Run Loop的mode畔勤。即是說,在App平常狀態(tài)下扒磁,主線程的NSDefaultRunLoopMode的Run Loop在運作庆揪;在拖動UIScrollView的時候,切換成了主線程的UITrackingRunLoopMode的Run Loop在運作渗磅。
每個Run Loop Mode里面都可以有3種mode item嚷硫,即是上文所說的Input Source、Timer Source和Run Loop Observer始鱼。當一個Run Loop在某個mode下有Input Source或Timer Source的時候仔掸,這個Run Loop就不會退出,它會一直循環(huán)并處理或等待Input Source或Timer Source發(fā)出來的消息医清,也即是1(3)所描述的運作起暮。
Run Loop、Run Loop Mode和mode items的關(guān)系如下圖:
(2)会烙、Input Source是用來傳送event給線程的负懦,它是線程中的Run Loop最主要的事件源。在蘋果官方文檔中將Input Source分為3種類型:Port-Based Sources柏腻、Custom Input Sources和Cocoa Perform Selector Sources纸厉。而在實際中,根據(jù)函數(shù)調(diào)用棧五嫂,Input Source實際只被區(qū)分為兩種:Source0和Source1颗品。
Source1是基于Port的肯尺,即是蘋果官方分類的Port-Based Sources,這一類事件源通過內(nèi)核和其他線程進行通信躯枢,它處理的消息是系統(tǒng)事件则吟。它接收或分發(fā)系統(tǒng)事件,比如屏幕觸摸之類的硬件操作產(chǎn)生的事件锄蹂,就會先由Source1接收再進行分發(fā)等處理氓仲;
Source0是非基于port的,比如performSelector:onThread: withObject: waitUntilDone:之類方法產(chǎn)生的事件得糜,便歸結(jié)到Source0里敬扛。
有一個需要注意的地方:按鈕點擊事件,它從函數(shù)調(diào)用棧來看是Source0事件掀亩。但是實際上這個事件是從觸摸屏幕開始產(chǎn)生的舔哪,觸摸屏幕產(chǎn)生了event,Source1接收了這個event槽棍, 再由Source1將事件分發(fā)給了Source0捉蚤,所以最終在函數(shù)調(diào)用棧里可以看到Source0的調(diào)用。
(3)炼七、Timer Source會在預設的時間點向Run Loop發(fā)送消息缆巧,并且可以重復,其實就是NSTimer產(chǎn)生的事件豌拙。所以同時可以發(fā)現(xiàn)陕悬,NSTimer所在的線程必須有正在運行的Run Loop才能生效。由于主線程有默認啟動的Run Loop按傅,所以主線程上的NSTimer有時可以不考慮Run Loop相關(guān)的操作捉超;但是如果NSTimer運作在新開的線程上,那就必須要在當前線程上啟動Run Loop唯绍,不然NSTimer不會生效拼岳。
(4)、Run Loop Observer并不是事件源况芒,它是用在Run Loop本身運作的時候往外發(fā)送消息的惜纸,Run Loop通過Observer隨時告知外界它現(xiàn)在所處的狀態(tài)。Run Loop會在以下幾個事件點觸發(fā)Observer:
(5)绝骚、由2(2)我們可以知道耐版,Run Loop每次只能運行在一個mode 下,要切換mode必須先退出再換一個mode 進入压汪。假設我們在主線程有一個NSTimer對象粪牲,它默認被添加到NSDefaultRunLoopMode這個mode中,那么當頁面中有UIScrollView被拖動的時候止剖,主線程中NSDefaultRunLoopMode的Run Loop退出腺阳,換成UITrackingRunLoopMode的Run Loop進入湿滓,這時候這個NSTimer對象就不起作用了,因為它所添加到的Run Loop已經(jīng)退出了舌狗。
處理這種情況,就需要用到commonModes扔水,一個mode可以被標記為commonModes痛侍,Run Loop會把所有標記為commonModes的mode涉及到的所有Input Source、Timer Source和Run Loop Observer同步到這些mode里魔市,于是這些mode就會共享Input Source主届、Timer Source和Run Loop Observer,當Run Loop切換了一種mode重新啟動的時候待德,Source和Observer就仍然有效君丁。
所以可以把這個NSTimer對象添加到NSRunLoopCommonModes中,由于NSDefaultRunLoopMode和UITrackingRunLoopMode默認已被標記為commonModes将宪,所以這時不管主線程的Run Loop切換到哪種模式绘闷,這個NSTimer對象都可以正常起作用了。
(6)较坛、這時我們再回頭來看一看1(3)所描述的Run Loop運行循環(huán)印蔗,結(jié)合mode items,可以這么解釋:
如果有Input Source或Timer Source丑勤,Run Loop就會一直在do-while循環(huán)中運作:當Input Source或Timer Source有發(fā)出消息华嘹,Run Loop就處理消息并繼續(xù)循環(huán);當Input Source或Timer Source沒有發(fā)出消息時法竞,Run Loop就停在休眠的代碼處耙厚,等待Input Source或Timer Source發(fā)出消息來喚醒它處理消息并繼續(xù)循環(huán);
如果沒有Input Source或Timer Source岔霸,Run Loop就會直接退出循環(huán)薛躬。
3、Run Loop的運作過程秉剑?
(1)泛豪、由前面已經(jīng)知道,Run Loop的核心是__CFRunLoopRun()函數(shù)侦鹏,這個函數(shù)的主要結(jié)構(gòu)是一個do-while循環(huán)诡曙,并且知道了這個循環(huán)的大致運作方法,那么在這個循環(huán)里具體做了什么呢略水?在蘋果的官方文檔里价卤,對于循環(huán)的運作過程是這么描述的:
Each time you run it, your thread’s run loop processes pending events and generates notifications for any attached observers. The order in which it does this is very specific and is as follows:
每次線程中的Run Loop開始運行,它會先處理正在等待處理的事件(從事件源發(fā)出的消息)渊涝,同時通知相關(guān)的觀察者慎璧。它的具體處理順序如下:
①床嫌、Notify observers that the run loop has been entered.
通知觀察者Run Loop已經(jīng)進入。
②胸私、Notify observers that any ready timers are about to fire.
通知觀察者即將開始處理已就緒的timer的事件厌处。
③、Notify observers that any input sources that are not port based are about to fire.
通知觀察者即將開始處理Source0的事件岁疼。
④阔涉、Fire any non-port-based input sources that are ready to fire.
處理Source0的事件。
⑤捷绒、If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
如果Source1有事件在等待處理瑰排,那么就處理這些事件,然后跳到第⑨步暖侨。
⑥椭住、Notify observers that the thread is about to sleep.
(來到這一步說明Source1沒有等待處理的事件)通知觀察者線程要開始休眠了字逗。
⑦京郑、Put the thread to sleep until one of the following events occurs:
線程開始休眠,直到出現(xiàn)以下的任意情況:
* An event arrives for a port-based input source.
Source1有事件發(fā)出來了扳肛。
* A timer fires.
有timer要啟動了傻挂。
* The timeout value set for the run loop expires.
Run Loop已超時 (do-while循環(huán)是有時限的,不過時限非常非常大)挖息。
* The run loop is explicitly woken up.
Run Loop被手動喚醒金拒。
⑧、Notify observers that the thread just woke up.
通知觀察者線程要被喚醒了套腹。
⑨绪抛、Process the pending event.
處理正在等待處理的事件。
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
如果有timer啟動了电禀,處理timer的事件然后跳到第②步重新進入循環(huán)幢码。
* If an input source fired, deliver the event.
如果有input source啟動了,分發(fā)input source產(chǎn)生的事件尖飞。
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
如果Run Loop是被手動喚醒的症副,并且還沒到Run Loop的超時事件,那么也跳到第②步重新進入循環(huán)政基。
⑩贞铣、Notify observers that the run loop has exited.
(來到這一步說明Run Loop的循環(huán)結(jié)束了)通知觀察者Run Loop結(jié)束了沮明。
這就是一個Run Loop的詳細運作過程辕坝。
4、Run Loop有什么作用荐健?
(1)酱畅、根據(jù)官方文檔琳袄,Run Loop的作用如下:
The only time you need to run a run loop explicitly is when you create secondary threads for your application.
你只有在創(chuàng)建了子線程中這種場景下才需要去啟動一個Run Loop。
For example, if you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop. Run loops are intended for situations where you want more interactivity with the thread. For example, you need to start a run loop if you plan to do any of the following:
(但是并不是說所有子線程都需要啟動Run Loop)比如說纺酸,如果你要使用一條線程去處理一些耗時長但是可以預先確定操作內(nèi)容的任務窖逗,這時候你是不需要啟動Run Loop的(處理完任務讓線程自己掛掉就行了)。Run Loop是用在那些你希望和線程能有更多(無法預先確定的)交互的場景里(即是一些要讓線程一直保持活著的場景里)餐蔬。比如說滑负,你需要在以下場景里去啟用Run Loop:
Use ports or custom input sources to communicate with other threads.
需要通過ports或者custom input sources去和其他線程做交互。
Use timers on the thread.
在這條子線程上使用timer用含。
Use any of the performSelector… methods in a Cocoa application.
(要對這條子線程)使用performSelector開頭的那些方法。
Keep the thread around to perform periodic tasks.
要讓這條子線程去周期性地處理一些任務(那就必須使用Run Loop讓它變成常駐線程)帮匾。
(2)啄骇、如何做一條常駐線程?
要制造一條常駐線程可以在線程里這么處理:
這樣為Run Loop添加一個空的port瘟斜,由于有port缸夹,Run Loop啟動后就不會退出,會一直在do-while循環(huán)里等待port的事件螺句,這條線程就一直活著了虽惭。
如果不添加這個空的port的話,Run Loop在啟動后就會因為沒有Source而直接退出了蛇尚。
5芽唇、其他相關(guān):
(1)、Run Loop在每次啟動的時候會創(chuàng)建AutoreleasePool取劫,然后每次即將進入休眠的時候會釋放舊的AutoreleasePool并創(chuàng)建新的AutoreleasePool匆笤,最后在退出Run Loop的時候會最終釋放AutoreleasePool。
(2)谱邪、NSTimer計時并不是絕對精確的炮捧,如果到了某個需要進行操作的時間點,而Run Loop正在處理一項長時間的任務惦银,那么這個時間點的操作任務就有可能被推遲或者跳過(取決于NSTimer的tolerance 屬性)咆课。如果要使用精確(也有可能會有0.001s量級的誤差)的計時器,可以使用GCD的計時器:
在這個過程中有Run Loop模式的切換扯俱,通過打印可以看到它的計時仍然是很精準的:
(3)书蚪、還有另一種制造常駐線程的方法,不過不推薦使用蘸吓,它是通過不斷地啟動Run Loop來實現(xiàn)的善炫,直到有Source添加到Run Loop中,Run Loop才進入循環(huán)库继。這種方式和4(2)所描述的方法的差別在于:4(2)所描述的方法主動地為Run Loop添加了Source箩艺,這種方式不主動添加Source窜醉,被動等待Source:
輸出結(jié)果如下,直到點擊屏幕才停止打印“Run Loop已啟動”:
(4)艺谆、一個mode item重復加入同一個 mode 時是不會有多重效果的榨惰,相當于只加入一次的效果。
(5) 静汤、CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的琅催,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的虫给。NSRunLoop 是基于 CFRunLoopRef 的封裝藤抡,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的抹估。
(6)缠黍、一條線程在一個時間內(nèi)只能有一個Run Loop,一個Run Loop對應一個mode药蜻。當mode改變的時候瓷式,Run Loop需要先退出,換成新mode的Run Loop重新進入语泽,這時候線程里仍然只有一個Run Loop贸典。
(7)、在使用NSTimer的時候踱卵,下面兩個方法是完全等價的廊驼,記得在子線程的情況下要手動run一下這個Run Loop:
(8)、2(2)提到過切換Run Loop的mode的時候惋砂,Run Loop會先退出蔬充,換個mode再重新進入,可通過以下代碼來驗證這個說法(頁面上有個UITextView可以拖動):
當UITextView拖動時打印出來的內(nèi)容如下:
說明Run Loop確實是退出(128)然后重新進入(1)了班利。
(9)饥漫、2(4)提到過 Run Loop Observer并不是事件源,我們試試對一個Run Loop只添加Run Loop Observer不添加Source罗标,看看它能不能保持循環(huán)庸队,以此來驗證這種說法:
打印的內(nèi)容如下:
可以發(fā)現(xiàn)Run Loop直接就退出了,說明Run Loop Observer確實不是事件源闯割。
(10)彻消、在使用NSTimer重復執(zhí)行任務的過程中,如果沒有其他Source宙拉,Run Loop也是不斷重復“休眠——喚醒”的宾尚,通過下面的打印可以看出:
根據(jù)2(4)的狀態(tài),Run Loop會通知觀察者它即將進入休眠(32),過兩秒后再通知觀察者它即將被喚醒(64)煌贴,然后再執(zhí)行任務御板。
參考文檔:
蘋果官方文檔
http://blog.ibireme.com/2015/05/18/runloop/#comment-664
http://blog.csdn.net/ztp800201/article/details/9240913