前言
文章主要分為四個部分
- 一篷店、RunLoop 簡介
- 二叶沛、RunLoop 相關(guān)接口
- 三瘪弓、RunLoop 相關(guān)邏輯流程
- 四劫映、RunLoop 休眠實(shí)現(xiàn)原理
- 五乓旗、RunLoop 實(shí)際應(yīng)用
一府蛇、RunLoop 簡介
1.1 RunLoop 基本概念
RunLoop 顧名思義 運(yùn)行循環(huán),在程序運(yùn)行過程中循環(huán)做一些事情屿愚。比如定時器汇跨、GCD、事件響應(yīng)妆距、界面刷新穷遂、手勢識別、AutoreleasePool 等都是基于 RunLoop 的基礎(chǔ)之上娱据,沒有 RunLoop 任何事都無法做蚪黑。
一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出。RunLoop 機(jī)制能讓線程隨時處理事件但并不退出忌穿。這里說的隨時是指:程序需要運(yùn)行時就保持程序的持續(xù)運(yùn)行抒寂,不需要的時候就進(jìn)入休眠狀態(tài)。下述 1.2 是一個典型的案列伴网。
NSRunLoop 和 CFRunLoopRef 都是和RunLoop 機(jī)制相關(guān)的類蓬推。CFRunLoopRef 基于 CoreFoundation 框架內(nèi),是純 C 函數(shù)的 API澡腾,所有這些 API 都是線程安全的沸伏。CFRunLoopRef 的代碼是開源的。NSRunLoop 是基于 CFRunLoopRef 动分,提供了面向?qū)ο蟮?API毅糟,但是這些 API 不是線程安全的。
1.2 為什么 main
函數(shù)不會 return
掉 澜公?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
上面main
函數(shù)同一般函數(shù)相比姆另,啟動程序后并不會立刻 return 掉。其中UIApplicationMain 函數(shù)內(nèi)部默認(rèn)開啟了主線程的 RunLoop 坟乾,并執(zhí)行了一段類似無限循環(huán)的代碼迹辐。UIApplicationMain 函數(shù)一直沒有返回,所以運(yùn)行程序之后會 保持持續(xù)運(yùn)行狀態(tài)甚侣,節(jié)省CPU資源明吩,提高程序性能,該做事時做事殷费,該休息時休息印荔。
//無限循環(huán)代碼模式
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 執(zhí)行各種任務(wù),處理各種事件
// ......
} while (running);
return 0;
}
1.3 RunLoop 和 線程的關(guān)系
iOS中有2套API來訪問和使用RunLoop:
- Foundation:NSRunLoop
- Core Foundation:CFRunLoopRef
關(guān)于RunLoop 和線程之間的關(guān)系要知道以下幾點(diǎn):
- 1详羡、 線程和 RunLoop 是一一對應(yīng)的仍律,其關(guān)系是保存在一個全局的 Dictionary 里。線程作為 key实柠,RunLoop 作為 value
- 2水泉、線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建窒盐。4個自動獲取的函數(shù):CFRunLoopGetMain()草则、[NSrunLoop mainRunLoop] 和 CFRunLoopGetCurrent()、[NSrunLoop currentRunLoop] 登钥。主線程的RunLoop對象獲取是在 UIApplicationMain 內(nèi)部,而子線程的RunLoop對象需要我們自己去獲取娶靡。
- 3牧牢、銷毀則是在線程結(jié)束的時候。
- 4、只能在當(dāng)前線程中操作當(dāng)前線程的RunLoop塔鳍,而不能去操作其他線程的RunLoop伯铣。
二、RunLoop 相關(guān)接口
2.1 RunLoop 的結(jié)構(gòu)
從上圖可以看出RunLoop 中包含thread轮纫,即 RunLoop 和 線程一一對應(yīng)腔寡。
和 RunLoop 相關(guān)的主要涉及五個類:
- CFRunLoopRef:RunLoop對象
- CFRunLoopModeRef:運(yùn)行模式
- CFRunLoopSourceRef:輸入源/事件源
- CFRunLoopTimerRef:定時源
- CFRunLoopObserverRef:觀察者
從上圖可以看出,RunLoop 對象中可以包含多個 Mode掌唾,每個 Mode 又包含多個 Source放前、Timer、Observer糯彬,RunLoop 運(yùn)行過程中實(shí)際上就是去處理當(dāng)前 mode 中的 source凭语、timer、observer撩扒。
2.2 RunLoop 中的 Mode
關(guān)于Mode首先要知道:
- 一個RunLoop 對象中可能包含多個Mode似扔。
- 每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode(CurrentMode)搓谆。如RunLoop啟動時只能選擇其中一個Mode炒辉,作為currentMode。
- 如果需要切換Mode泉手,只能退出當(dāng)前Loop黔寇,再重新選擇一個Mode進(jìn)入。主要是為了分隔開不同的 Source螃诅、Timer啡氢、Observer,讓它們之間互不影響术裸。參考
- RunLoop啟動時只能選擇其中一個Mode倘是,作為currentMode
總共是有五種Mode:
-
kCFRunLoopDefaultMode
:默認(rèn)模式,主線程是在這個運(yùn)行模式下運(yùn)行 -
UITrackingRunLoopMode
:跟蹤用戶交互事件(用于 ScrollView 追蹤觸摸滑動袭艺,保證界面滑動時不受其他Mode影響) -
UIInitializationRunLoopMode
:在剛啟動App時第進(jìn)入的第一個 Mode搀崭,啟動完成后就不再使用 -
GSEventReceiveRunLoopMode
:接受系統(tǒng)內(nèi)部事件,通常用不到 -
kCFRunLoopCommonModes
:偽模式猾编,不是一種真正的運(yùn)行模式瘤睹,實(shí)際是kCFRunLoopDefaultMode
和UITrackingRunLoopMode
的結(jié)合。
有這樣一個場景答倡,假設(shè)自己封裝一個無限輪播視圖轰传,很有可能會出現(xiàn)這樣一種情況:當(dāng)你滑動輪播視圖時,輪播視圖的定時器不再起作用瘪撇,不能通過定時器調(diào)整UIScrollView的偏移值获茬。之所以會出項(xiàng)上述現(xiàn)象港庄,是因?yàn)橹骶€程的 RunLoop 里有兩個 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。默認(rèn)情況下是defaultMode
恕曲,但是當(dāng)滑動UIScrollView
時鹏氧,RunLoop 會將 mode 切換為 TrackingRunLoopMode
,這時 Timer 就不會被執(zhí)行佩谣。這樣區(qū)分 mode 分隔開不同的 Source把还、Timer、Observer茸俭,讓它們之間互不影響吊履。這樣做的好處是讓不同模式下專心做自己的事情,可以更好的提高應(yīng)用性能瓣履。當(dāng)然如果想在滑動的時候不讓定時器失效率翅,可以使用CommonMode來解決。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
參考
實(shí)際NSRunLoopCommonModes并不是一種模式袖迎,它只是一種標(biāo)記冕臭,它能夠讓timer在放在NSRunLoopCommonModes中的所有模式下運(yùn)行。而NSRunLoopCommonModes正好放了NSDefaultRunLoopMode和UITrackingRunLoopMode兩種模式燕锥,所以timer就能在兩種模式下運(yùn)行了辜贵。
2.3 Mode 中的 CFRunLoopSourceRef
CFRunLoopSourceRef是事件源,主要有兩種有Source0 和 Source1归形。
Source0 :非基于 Port托慨。
- 觸摸事件處理
- performSelector:onThread:
只包含了一個回調(diào)(函數(shù)指針),不能主動觸發(fā)事件暇榴。使用時厚棵,需先調(diào)用CFRunLoopSourceSignal(source)
,將 Source 標(biāo)記為待處理蔼紧,然后手動調(diào)用CFRunLoopWakeUp(runloop)
喚醒 RunLoop婆硬,讓其處理這個事件。
Source1:基于Port奸例。
- 線程間通信彬犯。每個線程都有一個 port ,不同線程之前通過這個 port 進(jìn)行通信查吊。
- 事件捕捉谐区。如點(diǎn)擊屏幕,Source1 先進(jìn)行事件捕捉逻卖,會放在隊(duì)列中宋列,然后再一次包裝為 Source0 觸摸事件處理。
如上圖评也,創(chuàng)建一個按鈕炼杖,添加點(diǎn)擊事件戈鲁,并在按鈕回調(diào)事件添加斷點(diǎn),當(dāng)執(zhí)行到斷點(diǎn)出左側(cè)會出現(xiàn)相關(guān)棧調(diào)用信息嘹叫。從上圖可以看出:點(diǎn)擊事件就是在
Sources0
中處理的。至于 Source1
主要是用來接收诈乒、分發(fā)系統(tǒng)事件罩扇,然后再分發(fā)到Sources0
中處理。Sources0:
2.4 Mode 中的 CFRunLoopTimerRef
NSTimer 和 performSelector:withObject:afterDelay: 都是通過CFRunLoopTimerRef
處理的怕磨。CFRunLoopTimerRef
是定時源喂饥,你可以簡單把它理解為NSTimer
。其包含一個時間點(diǎn)和一個回調(diào)(函數(shù)指針)肠鲫。當(dāng)被加入到 RunLoop 時员帮,RunLoop 會注冊對應(yīng)的時間點(diǎn),當(dāng)時間到時导饲,RunLoop 會執(zhí)行對應(yīng)時間點(diǎn)的回調(diào)捞高。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2.5 Mode 中的 CFRunLoopObserverRef
RunLoop 的狀態(tài)、UI界面的刷新(BeforeWaiting)渣锦、autorelease pool都是通過 CFRunLoopObserverRef
處理的硝岗。
CFRunLoopObserverRef
是觀察者,主要用來監(jiān)聽RunLoop 的狀態(tài)袋毙,主要有以下幾種狀態(tài)型檀。
- kCFRunLoopEntry : 即將進(jìn)入RunLoop
- kCFRunLoopBeforeTimers :即將處理Timer
- kCFRunLoopBeforeSources:即將處理Source
- kCFRunLoopBeforeWaiting :即將進(jìn)入休眠
- kCFRunLoopAfterWaiting:即將從休眠中喚醒
- kCFRunLoopExit :即將從RunLoop中退出
- kCFRunLoopAllActivities:監(jiān)聽全部狀態(tài)改變
可以通過以下代碼驗(yàn)證RunLoop的幾種狀態(tài):
// 創(chuàng)建觀察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"監(jiān)聽到RunLoop發(fā)生改變---%zd",activity);
});
// 添加觀察者到當(dāng)前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放observer
CFRelease(observer);
除了用于監(jiān)聽 RunLoop 的狀態(tài)之外,UI刷新(BeforeWaiting)听盖、Autorelease pool(BeforeWaiting)都與其有關(guān)系胀溺。
三、RunLoop 相關(guān)邏輯流程
上圖是筆者從網(wǎng)上找到的一張 RunLoop 運(yùn)行的相關(guān)流程邏輯圖皆看。具體來說主要執(zhí)行邏輯是這樣的:
- 1仓坞、通知觀察者 RunLoop 已經(jīng)啟動。
- 2悬蔽、通知觀察者即將要開始定時器扯躺。
- 3、通知觀察者任何即將啟動的非基于端口的源蝎困。
- 4录语、啟動任何準(zhǔn)備好的非基于端口的源(Source0)。
- 5禾乘、如果基于端口的源(Source1)準(zhǔn)備好并處于等待狀態(tài)澎埠,進(jìn)入步驟9。
- 6始藕、通知觀察者線程進(jìn)入休眠狀態(tài)蒲稳。
- 7氮趋、將線程置于休眠狀態(tài),知道下面的任一事件發(fā)生才喚醒線程江耀。
. 某一事件到達(dá)基于端口的源
. 定時器啟動剩胁。
. RunLoop 設(shè)置的時間已經(jīng)超時。
. RunLoop 被喚醒祥国。 - 8昵观、通知觀察者線程將被喚醒。
- 9舌稀、處理未處理的事件啊犬。
.如果用戶定義的定時器啟動,處理定時器事件并重啟RunLoop壁查。進(jìn)入步驟2觉至。
.如果輸入源啟動,傳遞相應(yīng)的消息睡腿。
.如果RunLoop被顯示喚醒而且時間還沒超時语御,重啟RunLoop。進(jìn)入步驟2 - 10席怪、通知觀察者RunLoop結(jié)束沃暗。
四、RunLoop 休眠實(shí)現(xiàn)原理
首先有一點(diǎn)肯定的是何恶,RunLoop 的休眠不是一個類似 while(1).... 一直在循環(huán)的代碼孽锥,因?yàn)檫@種屬于線程阻塞,是需要消耗資源的细层。 一般休眠的實(shí)現(xiàn)和操作系統(tǒng)層面(內(nèi)核層面)有關(guān)系惜辑。API 一般分為兩種內(nèi)核API和應(yīng)用層面API。應(yīng)用層面的 API 一般是提供給開發(fā)者直接使用疫赎,內(nèi)核 API 開發(fā)者不能直接調(diào)用盛撑。RunLoop 休眠實(shí)現(xiàn)原理是用戶態(tài)低調(diào)用 mach_msg, 繼而轉(zhuǎn)去調(diào)用內(nèi)核態(tài)的 mach_msg,實(shí)現(xiàn)真正的休眠捧搞。當(dāng)有消息需要處理時抵卫,內(nèi)核態(tài) mach_msg 會喚醒用戶態(tài),讓線程去處理任務(wù)胎撇。
五介粘、RunLoop 實(shí)際應(yīng)用
4.1 線程保活
借助RunLoop可以實(shí)現(xiàn)線程蓖硎鳎活的功能姻采,關(guān)鍵是在于兩行代碼,具體請看如下代碼爵憎。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
[self.thread start];
}
- (void) runOne{
NSLog(@"----任務(wù)1-----");
// 下面兩句代碼可以實(shí)現(xiàn)線程笨祝活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 測試是否開啟了RunLoop婚瓜,如果開啟RunLoop,則來不了這里刑棵,因?yàn)镽unLoop開啟了循環(huán)巴刻。
NSLog(@"未開啟RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 利用performSelector,在self.thread的線程中調(diào)用run2方法執(zhí)行任務(wù)
[self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) runTwo{
NSLog(@"----任務(wù)2------");
}
實(shí)現(xiàn)了上述代碼之后蛉签,每次點(diǎn)擊屏幕都會打印----任務(wù)2------,這說明子線程處于活躍狀態(tài)冈涧。如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出正蛙,上述代碼中的 [NSPort port]
相當(dāng)于往 RunLoop 中添加 Source1。[[NSRunLoop currentRunLoop] run]
相當(dāng)于開啟了一個無限循環(huán)营曼,默認(rèn)是 defaultMode乒验,相應(yīng)的線程永遠(yuǎn)也不會釋放。即使調(diào)用CFRunLoopStop(CFRunLoopGetCurrent)
也只能停止其中的一次 [[NSRunLoop currentRunLoop] run]
蒂阱,并不能持續(xù)有效锻全。
在一些分析AFNetworking
源碼的文章中,也經(jīng)常會出現(xiàn)如下這些代碼录煤。其核心也是為了實(shí)現(xiàn)線程后臺常駐鳄厌。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
當(dāng)后臺線程執(zhí)行任務(wù)時,通過 performSelector:onThread:..
方法將任務(wù)放在后臺線程的 RunLoop 中妈踊。正常來說了嚎,一個線程執(zhí)行完任務(wù)后就退出了。開啟runloop是為了防止線程退出廊营。一方面避免每次請求都要創(chuàng)建新的線程歪泳;另一方面,因?yàn)閏onnection 的請求是異步的露筒,如果不開啟runloop呐伞,線程執(zhí)行完代碼后不會等待網(wǎng)絡(luò)請求完的回調(diào)就退出了,這會導(dǎo)致網(wǎng)絡(luò)回調(diào)的代理方法不執(zhí)行慎式。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
4.2 AutoreleasePool
應(yīng)用程序一旦啟動伶氢,主線程 RunLoop 里注冊了兩個 Observer。一個 Observer 監(jiān)聽即將進(jìn)入 Loop 事件瘪吏,回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池癣防,并保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。另外一個 Observer 監(jiān)視了兩個事件 (RunLoop即將進(jìn)入休眠和即將退出 RunLoop 事件) 掌眠,前者會調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池劣砍;后者會調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池,并保證釋放自動釋放池事件發(fā)生在其它回調(diào)之后扇救。
4.3 卡頓監(jiān)測
所謂的卡頓一般是在主線程做了耗時操作刑枝,卡頓監(jiān)測的主要原理是在主線程的 RunLoop 中添加一個 observer香嗓。然后,創(chuàng)建一個持續(xù)的子線程專門用來監(jiān)控主線程的 RunLoop 狀態(tài)装畅。檢測是否一直處于 即將處理Source(kCFRunLoopBeforeSources)
狀態(tài)(一直處于忙碌狀態(tài)無法進(jìn)入休眠狀態(tài))靠娱、 是否一直處于即將從休眠中喚醒 (kCFRunLoopAfterWaiting)
狀態(tài)(一直無法從休眠狀態(tài)喚醒去處理事件) 。如果花費(fèi)的時間大于某一個闕值掠兄,則認(rèn)為卡頓像云,此時可以輸出對應(yīng)的堆棧調(diào)用信息。觸發(fā)卡頓的時間閾值蚂夕,我們可以根據(jù) WatchDog 機(jī)制來設(shè)置迅诬。WatchDog 在不同狀態(tài)下設(shè)置的不同時間,如下所示:
- 啟動(Launch):20s婿牍;
- 恢復(fù)(Resume):10s侈贷;
- 掛起(Suspend):10s;
- 退出(Quit):6s等脂;
- 后臺(Background):3min(在 iOS 7 之前俏蛮,每次申請 10min; 之后改為每次申請 3min上遥,可連續(xù)申請搏屑,最多申請到 10min)。
通過 WatchDog 設(shè)置的時間粉楚,我認(rèn)為可以把啟動的閾值設(shè)置為 10 秒辣恋,其他狀態(tài)則都默認(rèn)設(shè)置為 3 秒。其中的 3s 也可以理解為 3次 * 1s模软。
網(wǎng)上很多抓去卡頓堆椧值常或多或少存在問題,比較正確的解決方案可參考這篇文章
參考撵摆。
4.3 UI 刷新
在 iOS 和 macOS 中底靠,UI 刷新主要由主線程上的 RunLoop 管理,而 UI 刷新的具體執(zhí)行時機(jī)通常是在 RunLoop 的 beforeWaiting 階段特铝。這種設(shè)計(jì)的主要原因是為了確保 UI 更新的效率和響應(yīng)性暑中。
beforeWaiting 時機(jī)的優(yōu)勢
beforeWaiting 是 RunLoop 在即將進(jìn)入休眠之前的最后一個階段。這個時機(jī)特別適合 UI 刷新鲫剿,原因包括:
- 避免不必要的重復(fù)刷新:UI 刷新操作是昂貴的操作鳄逾。如果 UI 刷新放在 RunLoop 的其他階段,可能會在同一個 RunLoop Cycle 中多次觸發(fā)刷新灵莲。而在 beforeWaiting 階段雕凹,由于 RunLoop 即將進(jìn)入休眠狀態(tài),這時觸發(fā) UI 刷新可以確保當(dāng)前所有的 UI 更新操作都已經(jīng)合并完成,不會浪費(fèi)資源進(jìn)行多次刷新枚抵。
- 最大化響應(yīng)性:當(dāng) RunLoop 即將休眠時线欲,說明當(dāng)前沒有其他高優(yōu)先級的任務(wù)需要立即處理。這時觸發(fā) UI 刷新汽摹,能最大限度地利用主線程的空閑時間來更新 UI李丰,從而提升應(yīng)用的響應(yīng)性和用戶體驗(yàn)。
- 避免 UI 更新被阻塞:如果 UI 刷新放在其他階段逼泣,例如定時器或輸入源階段趴泌,UI 刷新可能會被其他更高優(yōu)先級的事件(如用戶交互事件)阻塞。而放在 beforeWaiting 階段拉庶,則是確保所有輸入事件處理完畢后嗜憔,再進(jìn)行 UI 刷新,從而使 UI 刷新不被其他任務(wù)阻塞氏仗。
補(bǔ)充
是否可以自定義Model吉捶?
參考
可以自定義Model。
對于開發(fā)者而言經(jīng)常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實(shí)這個并不是某種具體的Mode廓鞠,而是一種模式組合,在iOS系統(tǒng)中默認(rèn)包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是說Runloop會運(yùn)行在kCFRunLoopCommonModes這種模式下谣旁,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode床佳。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)。
我們常常還會碰到一些系統(tǒng)框架自定義Mode榄审,例如Foundation中NSConnectionReplyMode砌们。還有一些系統(tǒng)私有Mode,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件搁进,UIInitializationRunLoopMode App啟動過程中初始化Mode浪感。
在代碼中,可以通過名稱識別 mode饼问。Cocoa影兽、Cocoa Touch、Core Foundation 均提供了 default mode 和常用 mode莱革,也可以通過為 mode 名稱指定自定義字符串來創(chuàng)建自定義 mode峻堰。雖然 mode 名稱可以任意指定,但 mode 內(nèi)容不是任意的盅视,必須提供 input source捐名、timer、observer 中的一個或多個闹击。Mode 可用于過濾不需要的 source镶蹋。大部分情況下,使用系統(tǒng)定義的 default mode 即可。UIScrollView滑動時主線程會進(jìn)入U(xiǎn)ITrackingRunLoopMode贺归。 創(chuàng)建 input source 時淆两,可以將其分配給一種或多種 mode。mode 決定任一時刻監(jiān)控哪些輸入源
牧氮。通常琼腔,在 default mode 運(yùn)行 run loop,但也可以指定自定義 mode踱葛。如果 input source 不在當(dāng)前監(jiān)控的 mode丹莲,其產(chǎn)生的事件將被保留,等 run loop 在該 mode 運(yùn)行時才傳遞尸诽。