Run Loops
下面會用一些陌生的或者容易讓人混淆的字符瓮床,我們先來統(tǒng)一概念再繼續(xù)颤陶,這樣能夠讓你更加愉快的閱讀:
runloop:iOS一個底層機制的專業(yè)術(shù)語拜银。
run loop:一種運行的循環(huán)宴偿。
Handler:指handlePort:
祖灰、customSrc:
含长、mySelector:
券腔、timerFired:
,指開發(fā)者希望當進入run loop的時候要執(zhí)行的操作
run-loop observer:觀察runloop行為的觀察者
run-loop mode:每個runloop都要指定一種mode拘泞,一種mode可以對應(yīng)多個input source
runloop object:runloop相關(guān)的對象纷纫,這里分Cocoa和CoreFoundation中的
Runloop是線程架構(gòu)相關(guān)的一部分,是事件處理的循環(huán)陪腌。你可以用它來安排循環(huán)執(zhí)行任務(wù)辱魁,協(xié)調(diào)相關(guān)的事件(kCFRunLoopEntry、kCFRunLoopExit等)诗鸭。
用幾行示意代碼來展示:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
Runloop實際上是一個管理要處理的事件和消息的對象染簇,并為開發(fā)者提供了一個入口來執(zhí)行run loop的邏輯部分。一直處于類似于“接受消息-->等待-->處理”這樣的一個循環(huán)匯總
Runloop的目的:一般我們自己寫的線程在執(zhí)行完相應(yīng)的任務(wù)就會退出强岸,而runloop讓你的線程在有事干的時候干活锻弓,沒事干的時候休息,節(jié)省系統(tǒng)資源蝌箍。
Runloop的管理不全是自動的青灼,對于自己的線程,必須要自己添加代碼妓盲,并在適當?shù)臅r候啟動Run loop來響應(yīng)接受的事件杂拨。
Cocoa和Core Foundation提供了runloop objects來幫助你配置和管理線程的run loop。你的Application不需要自己創(chuàng)建這些對象(像alloc悯衬、init這些方法)弹沽。每個thread,包括Application的主線程筋粗,都已經(jīng)有runloop object策橘,有方法可以直接獲取這這些對象,類似于這樣的[NSRunLoop currentRunLoop]
亏狰。
/// 全局的Dictionary役纹,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop暇唾。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時促脉,初始化全局Dic辰斋,并先為主線程創(chuàng)建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取瘸味。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時宫仗,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào),當線程銷毀時旁仿,順便也銷毀其對應(yīng)的 RunLoop藕夫。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
蘋果提供了CFRunLoopGetMain
、CFRunLoopGetMain
這兩個方法來創(chuàng)建runloop枯冈,如果它不被調(diào)用毅贮,線程中就不會有runloop。
Cocoa中的
NSRunLoop
,和Core Foundation中的CFRunLoopRef
等Run loop相關(guān)的類尘奏。它們的方法一般也是一一對應(yīng)的滩褥。
唯一不同的是:你自己創(chuàng)建的線程需要明確的代碼來讓runloop跑起來,比如[runLoop run]
這樣的方法炫加。但是對于主線程的runloop瑰煎,系統(tǒng)會在App起來的時候自動設(shè)置并跑主線程里面的runloop,這是Application運行起來的一部分俗孝。
Anatomy of Run Loop(剖析runloop)
“一個運行的循環(huán)”酒甸,看它名字的意思就能猜得到。它會循環(huán)不斷的進入線程赋铝,在runloop進入之后做一些自己的事情插勤。你的code提供了狀態(tài)的控制用來實現(xiàn)runloop真正的循環(huán)部分「锕牵—— 換句話說饮六,你的code提供while
或for
循環(huán)來驅(qū)動run loop。在你的循環(huán)中苛蒲,你使用一個runloop object來運行事件處理的code,runloop接受事件并調(diào)用你已經(jīng)準備好的方法绿满。
/*
這里doFireTimer方法就是你要處理的事件的code臂外,*runloop*在從sleep狀態(tài)被wakeup之后會執(zhí)行改方法。
*/
[NSTimer scheduledTimerWithTimeInterval:0.016
target:self
selector:@selector(doFireTimer:)
userInfo:nil
repeats:YES];
上面我們提到“你使用一個runloop object來運行事件處理的code”喇颁,這里的事件是從哪里來呢漏健?即事件源是什么?
事件源分兩類:
- Input source所驅(qū)動的異步事件橘霎,一般它用來從一個線程向另一個線程發(fā)送事件或者從一個Application向另一個Application蔫浆。
- Timer sources所驅(qū)動的同步事件,事件發(fā)生在一個已經(jīng)預(yù)先設(shè)定好的時間姐叁,或者在重復(fù)的事件間隔發(fā)生瓦盛。
當事件到達的時候洗显,這兩種source會按照系統(tǒng)指定的步驟來處理事件。
下圖展示了一個runloop的概念結(jié)構(gòu)和各種不同的sources原环。Input sources發(fā)送一個異步的事件來執(zhí)行相對應(yīng)的handler(例如挠唆,圖中的hanlePort
、mySelector
等)嘱吗,并通過runUntilDate:
方法(調(diào)用thread相關(guān)的NSRunLoop對象)來退出玄组。Timer sources發(fā)送事件到runloop的handler,但不會引起runloop的退出谒麦。
除了處理輸入源對應(yīng)的handler俄讹,Runloop生成關(guān)于run loop行為的通知。注冊run-loop 的observer接受這些通知绕德。使用它們在thread上做一些其他的操作患膛。你可以在你的線程中使用Core Foundation里面的類來創(chuàng)建run-loop observer。
Run Loop Modes
run-loop mode是被監(jiān)聽的Input sources和Timers的集合迁匠,同時還是被通知的run-loop observer的集合剩瓶,大家可能比較疑惑怎么是兩種東西的集合(source和obaserver),下面是我的理解城丧,從代碼出發(fā):
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
線程的每個Source都要指定一個Mode延曙,這里是NSDefaultRunLoopMode
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopCommonModes);
觀察者是觀察該Runloop的某些Modes,kCFRunLoopCommonModes是mode的集合亡哄,每種mode都可以添加到Common中
每次你運行你的runloop的時候枝缔,要指定一個具體的mode在runloop里面跑。在進入runloop的時候蚊惯,只有和這個mode相關(guān)聯(lián)的source能被對應(yīng)的run-loop observer監(jiān)聽到愿卸,被允許向它們傳遞事件。而其他mode相關(guān)的source會一直等待截型,直到run-loop mode和source指定的mode相對應(yīng)的時候才開始傳遞事件趴荸。
在你的代碼中,你通過具體的名稱來定義mode(kCFRunLoopDefaultMode
宦焦、kCFRunLoopCommonModes
等)发钝。Cocoa和Core Foundation都定義了默認的mode和一些經(jīng)常使用的mode。
你也能通過簡單的字符串來自定義自己的mode波闹。盡管custom mode定義起來好像很隨意酝豪,但使用起來可不能隨意啊。對于任何mode精堕,你必須要確保添加一個或者更多的sources孵淘,timers或者run-loop observer,如果你沒有做到歹篓,run loop就會直接退出瘫证。
//如果沒有第二行代碼揉阎,runloop會自動退出
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
你可以使用mode過濾掉不想要的source。大多數(shù)情況下痛悯,你只需要用系統(tǒng)定義的“default”mode來運行你的runloop余黎。
Note:Mode是根據(jù)事件source來匹配的而不是根據(jù)事件的type。例如载萌,你不可以使用鼠標的按下事件或者鍵盤按下事件來匹配mode惧财。
下面是一些系統(tǒng)定義好的Modes
Mode | Name |
---|---|
Default | NSDefaultRunLoopMode(Cocoa)kCFRunLoopDefaultMode(Core Foundation) |
Connection | NSConnectionReplyMode(Cocoa) |
Modal | NSModalPanelRunLoopMode(Cocoa) |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) |
Common modes | NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation) |
Input Sources
Input source通過異步的方式向你的線程傳遞事件。事件的源取決于input source的類型扭仁。input source大概分為兩類:
-
Port-based port
Port-based port監(jiān)控你application的Mach port垮衷。 -
Custom input source
Custom input source監(jiān)控自定義的事件source。
runloop不關(guān)心是哪種input source乖坠,這兩種input sources系統(tǒng)都有實現(xiàn)搀突。這兩個input source唯一的不同就是它們?nèi)绾伟l(fā)送信號。Port-based port通過內(nèi)核自動發(fā)送信號熊泵。Custom input source只能從其他線程接受手動發(fā)送的信號仰迁。
當你創(chuàng)建一個input source,你把它賦值到runloop的mode顽分。mode會影響被監(jiān)控的input source徐许。大多數(shù)情況,你在NSDefaultRunLoopMode
下運行runloop卒蘸,但是你也可以指定自定義的mode雌隅。如果一個input source不在當前被監(jiān)控的mode中,它產(chǎn)生的一些事件將會被暫時的放在一邊缸沃,直到下一個run loop的mode和input source的mode相同才開始處理這個input source產(chǎn)生的事件恰起。
接下去的部分描述了一些input source
Port-Based Sources
Cocoa和Core Foundation通過提供端口相關(guān)的對象和方法來創(chuàng)建Port-Based Source。例如趾牧,在Cocoa中检盼,你沒必要直接創(chuàng)建input source。你使用NSPort
的方法[NSPort port]
翘单,添加port到run loop中梯皿。Port object會為你創(chuàng)建和配置所需的Port-Based Sources。
在 Core Foundation中县恕,你必須要自己創(chuàng)建port和它的run loop source。用 CFMachPortRef, CFMessagePortRef, CFSocketRef這些不透明指針創(chuàng)建相應(yīng)的對象剂桥。
如何配置和設(shè)置自定義的Port-Based Sources忠烛,請看 Configuring a Port-Based Input Source
Custom Input Sources(Core Foundation)
為了創(chuàng)建自定義的input source,你必須使用Core Foundation中的CFRunLoopSourceRef不透明類相關(guān)的方法权逗。你需要在回調(diào)方法中配置這個Custom Input Sources美尸。Core Foundation會在配置Custom Input Sources冤议、處理輸入事件和當source要從runloop中移除的時候調(diào)用這幾個回調(diào)。
對于如何創(chuàng)建一個custom input source請看這里Defining a Custom Input Source
Cocoa Perform Selector Sources(Cocoa)
除了Port-Based Sources师坎,Cocoa定義了一個custom input source恕酸,這允許你向任何線程中發(fā)送選擇子。和Port-Based Sources一樣胯陋,執(zhí)行選擇子selector的請求在同一個線程上是連續(xù)的蕊温,當多個方法同時向線程發(fā)送請求的時候可能會導(dǎo)致同步的問題。和Port-Based Sources不同的是:一個perform selector source執(zhí)行完它的selector遏乔,它自己會從runloop中移除义矛。
當向另一個線程發(fā)送selector請求時,目標線程必須有一個active的runloop盟萨。這意味著你必須等待凉翻,直到該線程中runloop的狀態(tài)變成 active。因為主線程會自己開始一個runloop捻激,只要系統(tǒng)一調(diào)用Application delegate的applicationDidFinishLaunching:
(這時候系統(tǒng)的runloop就創(chuàng)建出來了)制轰,你就可以向主線程發(fā)送selector請求。每次Runloop會處理所有隊列的selector請求胞谭,而不是只處理一個隊列的請求垃杖。下面列出了一些perform方法
Timer Sources
Timer sources會在將來的某個時間泉瞻,發(fā)送同步事件到你的當前的線程脉漏。Timer是線程用來通知它自己的一種方式。
盡管它是基于時間的通知袖牙,一個timer并不能保證在準確的時間點執(zhí)行事件侧巨。和其他的Input source一樣,Timer和你run loop指定的mode有關(guān)系鞭达。如果Timer的mode不是你當前runloop正在跑的mode司忱,它是不會被觸發(fā)的,直到你的runloop運行的的mode是你的Timer所支持的畴蹭。類似的坦仍,如果一個Timer被觸發(fā),但是當runloop在執(zhí)行Handler叨襟,Timer將會一直等待繁扎,直到下次再次進入runloop的時候才會執(zhí)行。如果這個runloop不再跑了,這個Timer將永遠不會被觸發(fā)梳玫。
你可以配置Timer來運行事件爹梁,一次或者不停重復(fù)。一個重復(fù)的Timer會基于設(shè)定好的觸發(fā)時間提澎,自動重新安排它自己到run loop中姚垃,但這個時間間隔并不一定準確。例如盼忌,如果一個Timer在一個特定時間被觸發(fā)积糯,每5秒重復(fù)一次。即使實際的觸發(fā)時間被延遲了碴犬,被安排的觸發(fā)時間總落在5秒的延遲時間間隔之內(nèi)絮宁。如果觸發(fā)時間被延遲太多,會導(dǎo)致它錯過了一次或者更多被安排到run loop的機會服协。在一個不恰當?shù)臅r機被觸發(fā)之后绍昂,Timer則會被安排到下一次run loop。
關(guān)于配置timer sources的更多信息偿荷,請看Configuring Timer Sources窘游。更多相關(guān)信息NSTimer Class Reference或CFRunLoopTimer Reference。
Run Loop Observers
一般的source是在一個異步或者同步的事件發(fā)生的時候被觸發(fā)的跳纳,而run-loop observer是在runloop它執(zhí)行的時候觸發(fā)的忍饰。你可以通過run-loop observer來準備一些線程待處理的事件,或者在線程sleep之前寺庄,在線程中做一些事艾蓝。run-loop observers可以觀察到下面的一些事件:
- 進入runloop的時候
- 當runloop將要處理timer的時候
- 當runloop將要處理input source的時候
- 當runloop將要sleep的時候
- 當runloop已經(jīng)醒來,還沒有開始處理event的時候
- 當runloop退出的時候
你可以使用Core Foundation向Application添加run-loop observer斗塘。為了創(chuàng)建一個run_loop observer赢织,你需要使用CFRunLoopObserverRef類。這個類會追蹤你自定義callback函數(shù)馍盟、你感興趣的CFRunLoopActivity
于置。
和Timer相似,run-loop observer可以只運行一次贞岭,或者不停重復(fù)八毯。只運行一次的run-loop observer在一次run loop之后就會從runloop中移除。當你創(chuàng)建run-loop observer的時候瞄桨,你要決定r是一次還是不停重復(fù)话速。
對于如何創(chuàng)建一個run-loop observer的例子,看 Configuring the Run Loop,更多相關(guān)信息CFRunLoopObserver Reference
The Run Loop Sequence of Events
每次你運行runloop芯侥,你線程的runloop會處理之前掛起的事件泊交,并且向run-loop observer發(fā)送通知。runloop執(zhí)行的順序如下面所列出的:
- 通知run-loop observer,進入runloop
- 通知run-loop observer活合,已經(jīng)準備好的Timer將要觸發(fā)
- 通知run-loop observer,以及準備好的no-port-based input source將要被觸發(fā)
- 觸發(fā)no-port-based input source
- 如果一個port-based input source準備好等待觸發(fā)物赶,立即處理該事件白指,再從第九步開始執(zhí)行
- 通知run-loop observer,線程將要sleep
-----------------------------------sleepping-------------------------------------
-
線程sleep直到下面的事件發(fā)生:
- port-based input source事件到達
- 一個Timer觸發(fā)了
- runloop超時了
- runloop被手動喚醒
通知run-loop observer酵紫,線程將要被喚醒
-
處理掛起的事件
- 如果一個用戶定義的timer觸發(fā)了罢坝,處理相關(guān)事件翩剪,重新開始循環(huán),回到第二步繼續(xù)執(zhí)行
- 如果input source觸發(fā),傳遞相應(yīng)的事件
- 如果runloop明確的被喚醒祭犯,但是還沒有超時,重新開始循環(huán)勾哩,回到第二步繼續(xù)執(zhí)行
通知run_loop observer岔霸,runloop已經(jīng)退出
Tip:no-port-based input source就是我們常說的source0,port-based input source就是source1
因為Timer和input source的通知是在事件被觸發(fā)之前犬庇,所以這之間有一個過渡時間僧界。如果你要在這個時間做一些事情,你可以使用sleep和 awake-from-sleep 通知來幫助你獲得這段時間的控制權(quán)臭挽。
一個runloop可以通過使用run loop object被喚醒捂襟。其他的事件也可能引起runloop被喚醒。例如欢峰,添加no-port-based input source來喚醒runloop以至于input source可以被立即處理葬荷,而不是等待其他的事件發(fā)生。
iOS系統(tǒng)中有很多用到了RunLoop的地方:
- AutoreleasePool:監(jiān)聽
kCFRunLoopEntry
事件纽帖,保證在所以回調(diào)之前創(chuàng)建自動釋放池宠漩;監(jiān)聽BeforeWaiting
事件,釋放舊的線程池抛计,創(chuàng)建新的線程池哄孤;監(jiān)聽kCFRunLoopExit
事件,保證線程池最后被釋放吹截。- 事件響應(yīng):通過port-based input source監(jiān)聽用戶的事件瘦陈,并轉(zhuǎn)發(fā)給App進程,如用UIControl的事件波俄。
- 手勢識別:監(jiān)聽
kCFRunLoopBeforeWaiting
事件在事件的回調(diào)函數(shù)中標記待處理的手勢晨逝,并處理手勢。- 界面更新:
setNeedsLayout
懦铺、setNeedsDisplay
都會將該view標記為待處理捉貌,等到下一個runloop的時候會布局UI。- 定時器:就是一個CFRunLoopTimerRef
- PerformSelecter:將方法扔到某個線程中,在下一個run loop的時候執(zhí)行趁窃。
- 關(guān)于GCD:dispatch_async(dispatch_get_main_queue(), block)向主線程的run loop發(fā)送消息牧挣,喚醒run loop。并執(zhí)行block中的內(nèi)容醒陆。
PPT總結(jié)一下: