此文章的意圖:當(dāng)你完全細(xì)心閱讀之后,對runloop認(rèn)知,會(huì)成為你作為一名ios開發(fā)人員潛意識里的一部分
一、官方一張圖開始

官方文檔開宗介紹
Run loops are part of the fundamental infrastructure associated with threads.
runloop是與線程相關(guān)的基礎(chǔ)架構(gòu)的一部分,說白了runloop是與線程密不可分的,離開線程,runloop無從談起
A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events.
runloop是一個(gè)事件處理循環(huán),你可以使用它安排工作,對接收進(jìn)來的事件進(jìn)行統(tǒng)籌處理
The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
runloop的目的 - 為了達(dá)到這樣一種效果,有工作就處理,沒有工作就休眠
這幾句就夠了,回答了哲學(xué)三問 WDP:What -> runloop是什么?Do -> runloop干嘛用?Purpose -> runloop目的?
二、雖然官方英文描述很晦澀,但是為了準(zhǔn)確,還是對官方說明做個(gè)解釋
(一) runloop 描述
runloop的管理不是完全自動(dòng)的,你必須設(shè)計(jì)線程代碼,在合適的時(shí)機(jī)啟動(dòng)runloop,對接收進(jìn)來的事件進(jìn)行響應(yīng)。
Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)均提供了runloop對象幫助你配置和管理你自己線程的runloop; 你的應(yīng)用不需要顯示創(chuàng)建這些runloop對象
每個(gè)線程都有一個(gè)相關(guān)聯(lián)的runloop對象
應(yīng)用程序框架會(huì)自動(dòng)在主線程上建立并運(yùn)行run循環(huán),作為應(yīng)用程序啟動(dòng)過程的一部分
只有子線程需要顯式地運(yùn)行runloop
(二)接下來 官方剖析了你如何為你的應(yīng)用配置runloop
1)意識形態(tài)
線程進(jìn)入循環(huán),運(yùn)行事件處理程序,響應(yīng)runloop接收到的事件
runloop控制如何實(shí)現(xiàn) - 通過while 或者 for循環(huán) 來驅(qū)動(dòng)runloop, 運(yùn)行事件處理程序
-
runloop從兩種不同的source接收事件: Input sources 和 Timer sources
Input sources 提供
異步事件, 事件 來自于其他線程或進(jìn)程Timer sources 提供
同步事件, 事件 按照預(yù)定時(shí)間或者重復(fù)的間隔發(fā)生
image.pngInput sources: runloop對象執(zhí)行 runUntilDate: 方法,然后 runloop 退出
Timer sources: 不會(huì)引起runloop退出
-
runloop接收到Input sources,會(huì)觸發(fā)一個(gè)通知
-
對于Input Sources, 如果你想要處理更多,那就自己注冊runloop觀察者 來接收這個(gè)通知
- 你可以通過Core Foundation在你的線程里配置 runloop 觀察者
-
2)Run Loop Modes
runloop Mode,可以通俗的來講兩個(gè)集合,就是要監(jiān)視的對象集合 和 要通知的對象集合
-
監(jiān)視的對象,當(dāng)然就是 兩種Sources了, Input 和 Timer
- 為什么要監(jiān)視,其實(shí)可以理解為監(jiān)聽,有事件進(jìn)來,就處理響應(yīng)
-
通知的對象,自然就是要通知給 觀察者了
- 一般情況下,事件本身不關(guān)注自己什么時(shí)候被處理,就等著runloop處理,等到什么時(shí)候算什么時(shí)候, 但架不住好管閑事,比如我就想在處理事件之前打印一個(gè)信息 就需要注冊觀察者接收通知了
每次運(yùn)行runloop,指定一個(gè)mode或使用默認(rèn)mode, 這樣只有與指定mode相關(guān)聯(lián)的 sources(存在兩種source)會(huì)被監(jiān)聽,同樣與mode相關(guān)聯(lián)的observers會(huì)被通知
runloop 的幾種mode
-
Default
- 默認(rèn)
-
Connection
- 你應(yīng)該很少用到這種mode
-
Modal
- Cocoa使用此模式來識別用于模態(tài)面板的事件
-
Event tracking
- 在鼠標(biāo)拖動(dòng)和其他類型的用戶界面交互跟蹤期間,Cocoa通過這種模式 來限制傳入的事件
-
Common modes (有點(diǎn)費(fèi)解,仔細(xì)理解下)
- 是個(gè)集合,Cocoa默認(rèn)為 集合(或者group) [Default, Modal, Event tracking],Core Foundation默認(rèn)為[Default]; 如果一個(gè)Input Source 與 Default關(guān)聯(lián),則如果指定mode為 Common modes,同樣也就與 Modal關(guān)聯(lián),也與Event tracking關(guān)聯(lián),蘋果提供了 CFRunLoopAddCommonMode 方法往集合里添加 其他mode
3)Input Sources
Input Sources 往線程交付異步事件,分為兩種
基于Port的 source監(jiān)視Mach ports ,由內(nèi)核自動(dòng)signal
自定義source 監(jiān)視自定義事件, 由另一個(gè)線程手動(dòng) signal
在任何時(shí)刻,Modes都會(huì)影響 Input sources
通常情況下,runloop在 default mode下run,也可以指定 自定義modes
如果Input sources不處于當(dāng)前關(guān)聯(lián)mode,則它生成的任何event都將保持,直到Input sources處于關(guān)聯(lián)的mode
4)Timer Sources
Timer Sources 在未來一個(gè)預(yù)設(shè)的時(shí)間同步地向你的線程交付事件
雖然Timer Sources 產(chǎn)生了一個(gè)基于時(shí)間的通知,但這個(gè)timer并不是一個(gè)實(shí)時(shí)機(jī)制
如果Timer sources 不處于當(dāng)前監(jiān)視的關(guān)聯(lián)mode,則timer不會(huì)被觸發(fā)
如果timer觸發(fā)時(shí),runloop正在執(zhí)行handler處理,則timer自己的handler處理將等待下一次time到來執(zhí)行
如果runloop沒run起來,則timer不會(huì)被觸發(fā)
timer根據(jù)計(jì)劃的時(shí)間間隔重新調(diào)度自己,并不根據(jù)實(shí)際觸發(fā)時(shí)間,即使觸發(fā)時(shí)間比計(jì)劃延時(shí)了
換句話說就是設(shè)定5秒觸發(fā)一次,從0開始,等到8秒才執(zhí)行,下一次還會(huì)在10秒調(diào)度執(zhí)行
如果觸發(fā)時(shí),已經(jīng)錯(cuò)過了多個(gè)5秒間隔,timer會(huì)按照計(jì)劃的時(shí)間間隔,自動(dòng)空過已錯(cuò)過的計(jì)劃間隔,也就是錯(cuò)過了多個(gè)5秒,比如4個(gè)5秒,這4個(gè)5秒內(nèi),只執(zhí)行一次
5)Run Loop Observers
Sources VS Runloop Observers
Sources在同步或異步事件發(fā)生時(shí) 觸發(fā)
runloop observers在 runloop本身執(zhí)行到特殊的位置觸發(fā)
你可以使用runloop Observers準(zhǔn)備你的線程來處理給定的事件
你也可以在線程進(jìn)入休眠之前準(zhǔn)備線程
你可以將runloop Observers與以下事件關(guān)聯(lián)
- The entrance to the run loop. 【進(jìn)入runloop】
- When the run loop is about to process a timer. 【runloop即將處理timer】
- When the run loop is about to process an input source. 【runloop即將處理input source】
- When the run loop is about to go to sleep. 【runloop即將休眠】
- When the run loop has woken up, but before it has processed the event that woke it up. 【runloop被喚醒時(shí), 但是在runloop處理喚醒它的事件之前】
- The exit from the run loop. 【退出runloop】
你可以通過 Core Foundation 添加 runloop Observers,可以根據(jù)自己感興趣的事件,設(shè)置自定義回調(diào) 和 活動(dòng)
創(chuàng)建一個(gè)runloop Observer時(shí),你可以指定 是一次性 還是 重復(fù)的(once or repeatedly)
- once observer在觸發(fā)后,將自身從runloop中移除
- repeatedly observer在觸發(fā)后,仍然附加在runloop中
6)runloop事件序列
事件序列:
-
Notify observers that the run loop has been entered.
通知observer 已經(jīng)進(jìn)入runloop
-
Notify observers that any ready timers are about to fire.
通知observer 即將處理timer
-
Notify observers that any input sources that are not port based are about to fire.
通知observer 即將處理非基于port的 input source
-
Fire any non-port-based input sources that are ready to fire.
通知observer 處理 非基于port的 input source
-
If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
如果基于port的 input source 已ready,等待觸發(fā),則立即處理事件。執(zhí)行步驟9
-
Notify observers that the thread is about to sleep.
通知observer 線程即將休眠
-
Put the thread to sleep until one of the following events occurs:
線程休眠 直到以下幾個(gè)事件之一發(fā)生
-
An event arrives for a port-based input source.
基于port的 input source事件到來
-
A timer fires.
timer 觸發(fā)
-
The timeout value set for the run loop expires.
runloop 設(shè)置的超時(shí) 過期
-
The run loop is explicitly woken up.
runloop 被顯式喚醒
-
-
Notify observers that the thread just woke up.
通知observer 線程被喚醒
-
Process the pending event.
處理掛起的事件
-
If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
如果用戶定義的timer觸發(fā),處理timer事件,并重啟runloop 跳轉(zhuǎn)2
-
If an input source fired, deliver the event.
如果input source觸發(fā),交付事件
-
If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
如果runloop被顯式喚醒,但還沒有超時(shí),則重啟runloop 跳轉(zhuǎn)2
-
Notify observers that the run loop has exited.
通知observer 退出runloop
由于timer和input sources的observer通知 在事件發(fā)生之前,所以通知和事件實(shí)際發(fā)生有時(shí)間縫隙
可以用sleep 和 awake-from-sleep 通知來幫助關(guān)聯(lián)實(shí)際事件之間的間隔
由于timer和其他周期性事件是在runloop run時(shí)交付的,因此繞過該循環(huán)將中斷這些事件的交付
7)何時(shí)使用runloop
主線程runloop自動(dòng)啟動(dòng),你不需要主動(dòng)調(diào)用run
子線程
如果運(yùn)行長時(shí)任務(wù),很可能要避免啟動(dòng)runloop
-
以下情形 啟動(dòng)runloop
-
Use ports or custom input sources to communicate with other threads
使用端口或 自定義input sources與其他線程通訊
-
Use timers on the thread.
線程中使用timers
-
Use any of the
performSelector… methods in a Cocoa application.Cocoa應(yīng)用程序中 使用 任何performSelector 方法
-
Keep the thread around to perform periodic tasks
保持線程執(zhí)行周期性任務(wù)
-
8)創(chuàng)建一個(gè)runloop observer
image.png
image.png
CFRunLoopObserverContext 結(jié)構(gòu)體
image.png
好了代碼有了,不妨做個(gè)測試,當(dāng)下我用的M1電腦 模擬器
image.png
此時(shí),下面的控制臺(tái)是沒有任何額外輸出的,也就是 打印停在了44次
我不做任何操作 ,控制臺(tái)依舊是安靜的
這個(gè)時(shí)候 我從鍵盤上隨便按下一個(gè)鍵 (注意:此時(shí)模擬器應(yīng)該在前臺(tái)) 控制臺(tái)打印追加到了 observer 回調(diào) 60次, 較上一次,增加了16次,記住這個(gè)差值16
接下來控制太依舊安靜下來
控制臺(tái)安安靜靜,而且模擬器什么也不做,也不觸發(fā)什么操作,這不就是線程休眠么
我們按下任意鍵,控制臺(tái)接著打印,這不就是線程喚醒么,觀察者收到了runloop的通知,16次通知,具體每次信息,我們沒做打印,暫且按下不表,繼續(xù)往后分析
根據(jù)初步測試runloop,通過Core Foundation,我們在主線程注冊了一個(gè) runloop 觀察者,設(shè)置了observer 回調(diào)函數(shù),成功接收到了 runloop的通知
起碼 我們查探runloop 的方向是確立了,runloop給了一定的響應(yīng)通知信息
(1)主線程Runloop Observer通知信息
由于我的M1 xcode一查看堆棧就崩潰,所以改用我的x86 mac,打印信息有出入的地方,相信你們可以忽略掉
以下為boserver每次通知堆棧信息,以下是嚴(yán)格按照順序的,請耐心









..... kCFRunLoopoBeforeTimers
..... kCFRunLoopBeforeSources

。。。。。。接下來線程休眠


activity -
- 0x20: kCFRunLoopBeforeWaiting
- 0x40: kCFRunLoopAfterWaiting
- 0x1: kCFRunLoopEntry
- 0x2: kCFRunLoopBeforeTimers
- 0x4: kCFRunLoopBeforeSources
- 0x80: kCFRunLoopExit
通過主線程注冊observer,我們得到了一個(gè)runloop的序列活動(dòng)流程

你會(huì)發(fā)現(xiàn) [runloop run] , kCRunLoopExit 之后,馬上又 kCFRunLoopEntry,也就是runloop進(jìn)入之后,基本上不會(huì)退出了,因?yàn)橥顺鲋?馬上又entry了,感興趣可以自己測試體驗(yàn)下
這個(gè) [runloop run]
(2)run vs runUntilDate
上面的測試中使用runloop run,線程休眠后, 點(diǎn)擊屏幕 控制臺(tái)是沒有打印的,也就是touch事件并沒有喚醒線程
真的是這樣嗎?
此時(shí)模擬器是黑的,view還未正常load出來呀,我們驗(yàn)證下
我們在threadMain 方法結(jié)束之前添加 一句打印

此時(shí)我們發(fā)現(xiàn) [runloop run] 后面的打印并未在控制臺(tái)打印出來,說明 [runloop run] 直接阻塞了后面代碼的執(zhí)行
改用 runloop runUntilDate:

有些不一樣了
我們添加的打印正常執(zhí)行了 這時(shí)并沒有阻塞后面代碼的執(zhí)行 窗口不是黑背景了 說明view正常加載了
我們還發(fā)現(xiàn) 打印語句之前,最后一次打印的activity 為0x80, 正是 kCFRunLoopExit,也就是runloop退出了,所以后面的打印才可能正常執(zhí)行
-
有個(gè)細(xì)節(jié)可以關(guān)注下
-
線程休眠情況下,按下任意鍵 發(fā)現(xiàn)追加的打印 observer追加回調(diào)次數(shù) 變?yōu)?code>12次,還記得上面的
16次么- 如果你自己親自測試的話,你會(huì)發(fā)現(xiàn),activity 并沒有出現(xiàn)kCFRunLoopExit
既然任意鍵還能喚醒線程,observer還能收到通知,說明runloop肯定是又entry了,這個(gè)時(shí)候observer能夠收到的通知信息就很有限了 再次entry的通知并沒有收到
這個(gè)疑問,我們就得依賴swift Foundation源碼 間接揣測 CoreFoundation 來查看分析了
休眠情況下,觸摸屏幕,observer追加回調(diào)次數(shù) 變?yōu)?code>18次,說明事件不一樣 必然會(huì)影響 observer通知回調(diào)有些差別
-
(3)給 runloop runUntilDate 循環(huán)多次看看

發(fā)現(xiàn)第一次runloop runUntilDate之后,runloop 退出,再次執(zhí)行 runloop runUntilDate,再次exit
原來runloop可以這樣操作,這些細(xì)節(jié) 其實(shí)是了解runloop的關(guān)鍵,因?yàn)橛行┟恢^腦的東西 不仔細(xì)揣摩這些細(xì)節(jié) 是沒辦法get到的
(4)創(chuàng)建一個(gè)timer

加個(gè)timer之后,你就會(huì)發(fā)現(xiàn),不需要再按鍵或touch,控制臺(tái)observer通知回調(diào)會(huì)自動(dòng)打印,也就是說 timer會(huì)不停喚醒線程
(5)配置長的聲明周期線程
為一個(gè)長的聲明周期線程配置runloop時(shí),最好至少添加一個(gè)Input Source 來接收消息
盡管您可以只附加一個(gè)timer進(jìn)入運(yùn)行循環(huán),但一旦timer觸發(fā),它通常會(huì)失效,這將導(dǎo)致runloop退出
附加一個(gè)重復(fù)timer可以使runloop運(yùn)行更長的時(shí)間,但是需要定期觸發(fā)timer來喚醒線程,這實(shí)際上是輪詢的另一種形式
相比之下,Input Source等待事件發(fā)生,在事件發(fā)生之前保持線程睡眠




