Runloop原理(一)

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

一、官方一張圖開始

image.png

官方文檔開宗介紹

  • 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.png
    • Input 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事件序列

事件序列:

  1. Notify observers that the run loop has been entered.

    通知observer 已經(jīng)進(jìn)入runloop

  2. Notify observers that any ready timers are about to fire.

    通知observer 即將處理timer

  3. Notify observers that any input sources that are not port based are about to fire.

    通知observer 即將處理非基于port的 input source

  4. Fire any non-port-based input sources that are ready to fire.

    通知observer 處理 非基于port的 input source

  5. 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

  6. Notify observers that the thread is about to sleep.

    通知observer 線程即將休眠

  7. 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 被顯式喚醒

  8. Notify observers that the thread just woke up.

    通知observer 線程被喚醒

  9. 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

  10. 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)格按照順序的,請耐心

image.png
image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

..... kCFRunLoopoBeforeTimers

..... kCFRunLoopBeforeSources

image.png

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

image.png
image.png

activity -

  • 0x20: kCFRunLoopBeforeWaiting
  • 0x40: kCFRunLoopAfterWaiting
  • 0x1: kCFRunLoopEntry
  • 0x2: kCFRunLoopBeforeTimers
  • 0x4: kCFRunLoopBeforeSources
  • 0x80: kCFRunLoopExit

通過主線程注冊observer,我們得到了一個(gè)runloop的序列活動(dòng)流程

image.png

你會(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é)束之前添加 一句打印

image.png

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

改用 runloop runUntilDate:

image.png

有些不一樣了

我們添加的打印正常執(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)多次看看

image.png

發(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

image.png

加個(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ā)生之前保持線程睡眠

Runloop原理(二)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容