我所理解的Run Loop

1、什么是Run Loop?

(1)、Run Loop是線程的一項基礎配備,它的主要作用是來讓某一條線程在有任務的時候工作、沒有任務的時候休眠。
(2)、線程和 Run Loop 之間的關系是一一對應的,但是并不是說新開一條線程就會自動生成這條線程對應的Run Loop,每一條線程里的Run Loop都是需要主動去獲取,并且啟動它,它才會開始運作的。主線程的Run Loop之所以不用我們手動去獲取啟動它,是因為在App啟動的時候它已經默認啟動了。
(3)、Run Loop的核心是__CFRunLoopRun()函數,它的主要結構是一個do-while循環,當一個Run Loop啟動后,它在這個do-while循環里的基本運作如下:

如果有事件源(見后文詳解),它就會一直在這個do-while循環中運作:當事件源有發出消息,它就處理消息并繼續循環;當事件源沒有發出消息時,它就停在休眠的代碼處,等待事件源發出消息來喚醒它處理消息并繼續循環;
  如果沒有事件源,它就會直接退出循環。

(4)、線程里的Run Loop是在第一次獲取的時候才會去創建,而它的銷毀發生在以下3種情況下:

線程結束;
  因為沒有任何事件源而退出循環;
  手動結束了這個Run Loop。

2、Run Loop的結構?

(1)、Run Loop的結構需要涉及到以下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。關于這個的演示詳見后文5(8)。
  最常見的Run Loop Mode有兩個:NSDefaultRunLoopMode和UITrackingRunLoopMode,主線程的RunLoop 里預置了這兩個 Mode。其中NSDefaultRunLoopMode在App平常狀態下主線程Run Loop的mode,UITrackingRunLoopMode是在拖動UIScrollView的時候主線程Run Loop的mode。即是說,在App平常狀態下,主線程的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就不會退出,它會一直循環并處理或等待Input Source或Timer Source發出來的消息,也即是1(3)所描述的運作。
  Run Loop、Run Loop Mode和mode items的關系如下圖:



(2)、Input Source是用來傳送event給線程的,它是線程中的Run Loop最主要的事件源。在蘋果官方文檔中將Input Source分為3種類型:Port-Based Sources、Custom Input Sources和Cocoa Perform Selector Sources。而在實際中,根據函數調用棧,Input Source實際只被區分為兩種:Source0和Source1。
  Source1是基于Port的,即是蘋果官方分類的Port-Based Sources,這一類事件源通過內核和其他線程進行通信,它處理的消息是系統事件。它接收或分發系統事件,比如屏幕觸摸之類的硬件操作產生的事件,就會先由Source1接收再進行分發等處理;
  Source0是非基于port的,比如performSelector:onThread: withObject: waitUntilDone:之類方法產生的事件,便歸結到Source0里。
  有一個需要注意的地方:按鈕點擊事件,它從函數調用棧來看是Source0事件。但是實際上這個事件是從觸摸屏幕開始產生的,觸摸屏幕產生了event,Source1接收了這個event, 再由Source1將事件分發給了Source0,所以最終在函數調用棧里可以看到Source0的調用。
(3)、Timer Source會在預設的時間點向Run Loop發送消息,并且可以重復,其實就是NSTimer產生的事件。所以同時可以發現,NSTimer所在的線程必須有正在運行的Run Loop才能生效。由于主線程有默認啟動的Run Loop,所以主線程上的NSTimer有時可以不考慮Run Loop相關的操作;但是如果NSTimer運作在新開的線程上,那就必須要在當前線程上啟動Run Loop,不然NSTimer不會生效。
(4)、Run Loop Observer并不是事件源,它是用在Run Loop本身運作的時候往外發送消息的,Run Loop通過Observer隨時告知外界它現在所處的狀態。Run Loop會在以下幾個事件點觸發Observer:



(5)、由2(2)我們可以知道,Run Loop每次只能運行在一個mode 下,要切換mode必須先退出再換一個mode 進入。假設我們在主線程有一個NSTimer對象,它默認被添加到NSDefaultRunLoopMode這個mode中,那么當頁面中有UIScrollView被拖動的時候,主線程中NSDefaultRunLoopMode的Run Loop退出,換成UITrackingRunLoopMode的Run Loop進入,這時候這個NSTimer對象就不起作用了,因為它所添加到的Run Loop已經退出了。
  處理這種情況,就需要用到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運行循環,結合mode items,可以這么解釋:

如果有Input Source或Timer Source,Run Loop就會一直在do-while循環中運作:當Input Source或Timer Source有發出消息,Run Loop就處理消息并繼續循環;當Input Source或Timer Source沒有發出消息時,Run Loop就停在休眠的代碼處,等待Input Source或Timer Source發出消息來喚醒它處理消息并繼續循環;
  如果沒有Input Source或Timer Source,Run Loop就會直接退出循環。

3、Run Loop的運作過程?

(1)、由前面已經知道,Run Loop的核心是__CFRunLoopRun()函數,這個函數的主要結構是一個do-while循環,并且知道了這個循環的大致運作方法,那么在這個循環里具體做了什么呢?在蘋果的官方文檔里,對于循環的運作過程是這么描述的:

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開始運行,它會先處理正在等待處理的事件(從事件源發出的消息),同時通知相關的觀察者。它的具體處理順序如下:
①、Notify observers that the run loop has been entered.
  通知觀察者Run Loop已經進入。
②、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:
  線程開始休眠,直到出現以下的任意情況:
  * An event arrives for a port-based input source.
     Source1有事件發出來了。
  * A timer fires.
     有timer要啟動了。
  * The timeout value set for the run loop expires.
     Run Loop已超時 (do-while循環是有時限的,不過時限非常非常大)。
  * 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的事件然后跳到第②步重新進入循環。
  * If an input source fired, deliver the event.
     如果有input source啟動了,分發input source產生的事件。
  * 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的超時事件,那么也跳到第②步重新進入循環。
⑩、Notify observers that the run loop has exited.
  (來到這一步說明Run Loop的循環結束了)通知觀察者Run Loop結束了。

這就是一個Run Loop的詳細運作過程。

4、Run Loop有什么作用?

(1)、根據官方文檔,Run Loop的作用如下:

The only time you need to run a run loop explicitly is when you create secondary threads for your application.

你只有在創建了子線程中這種場景下才需要去啟動一個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)比如說,如果你要使用一條線程去處理一些耗時長但是可以預先確定操作內容的任務,這時候你是不需要啟動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循環里等待port的事件,這條線程就一直活著了。
  如果不添加這個空的port的話,Run Loop在啟動后就會因為沒有Source而直接退出了。

5、其他相關:

(1)、Run Loop在每次啟動的時候會創建AutoreleasePool,然后每次即將進入休眠的時候會釋放舊的AutoreleasePool并創建新的AutoreleasePool,最后在退出Run Loop的時候會最終釋放AutoreleasePool。
(2)、NSTimer計時并不是絕對精確的,如果到了某個需要進行操作的時間點,而Run Loop正在處理一項長時間的任務,那么這個時間點的操作任務就有可能被推遲或者跳過(取決于NSTimer的tolerance 屬性)。如果要使用精確(也有可能會有0.001s量級的誤差)的計時器,可以使用GCD的計時器:



  在這個過程中有Run Loop模式的切換,通過打印可以看到它的計時仍然是很精準的:



(3)、還有另一種制造常駐線程的方法,不過不推薦使用,它是通過不斷地啟動Run Loop來實現的,直到有Source添加到Run Loop中,Run Loop才進入循環。這種方式和4(2)所描述的方法的差別在于:4(2)所描述的方法主動地為Run Loop添加了Source,這種方式不主動添加Source,被動等待Source:

  輸出結果如下,直到點擊屏幕才停止打印“Run Loop已啟動”:

(4)、一個mode item重復加入同一個 mode 時是不會有多重效果的,相當于只加入一次的效果。
(5) 、CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。
(6)、一條線程在一個時間內只能有一個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拖動時打印出來的內容如下:

  說明Run Loop確實是退出(128)然后重新進入(1)了。
(9)、2(4)提到過 Run Loop Observer并不是事件源,我們試試對一個Run Loop只添加Run Loop Observer不添加Source,看看它能不能保持循環,以此來驗證這種說法:

  打印的內容如下:

  可以發現Run Loop直接就退出了,說明Run Loop Observer確實不是事件源。

(10)、在使用NSTimer重復執行任務的過程中,如果沒有其他Source,Run Loop也是不斷重復“休眠——喚醒”的,通過下面的打印可以看出:



  根據2(4)的狀態,Run Loop會通知觀察者它即將進入休眠(32),過兩秒后再通知觀察者它即將被喚醒(64),然后再執行任務。

參考文檔:
  蘋果官方文檔
  http://blog.ibireme.com/2015/05/18/runloop/#comment-664
  http://blog.csdn.net/ztp800201/article/details/9240913

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容