RunLoop , 運(yùn)行循環(huán), App 可以在程序運(yùn)行過(guò)程中做一些事情.
RunLoop 是什么?
為了說(shuō)明, 我們分別用 Xcode 創(chuàng)建兩個(gè)項(xiàng)目, 一個(gè)是 Command Tool, 一個(gè)是Single View App, 眾所周知, 運(yùn)行 Command Tool 程序, 只會(huì)在控制臺(tái)輸出結(jié)果, 并且只是一次性的, 運(yùn)行 App, 程序會(huì)借助 模擬器/真機(jī) 運(yùn)行.
這兩者最大的區(qū)別在于, 在 main.m 文件中
Command Tool
int main(int argc, char * argv[]) {
return 0
}
App
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
App 之所以能在模擬器/真機(jī)中長(zhǎng)期保持運(yùn)行 狀態(tài), 而不會(huì)終止, 在于
UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))
原因:
- UIApplicationMain() 內(nèi)部會(huì)創(chuàng)建一個(gè) runloop. 使得程序不會(huì)馬上退出, 而是保持保持運(yùn)行狀態(tài).
- 這里面會(huì)處理App的各種事件(定時(shí)器事件, 用戶交互事件等)
RunLoop對(duì)象
iOS中有兩套API來(lái)訪問(wèn)和使用Runloop.
- Foundation: NSRunLoop
- Core Foundation: CFRunLoopRef
// viewDidLoad 這個(gè)方法是在主線程中調(diào)用的, 當(dāng)前線程就是主線程
// 所以 mainRunLoop, currentRunLoop獲得的 runloop 對(duì)象的地址是一樣的.
NSLog(@"%p, %p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
// 0x600003748600, 0x600003748600
NSLog(@"%p, %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
// 0x600002f4c900, 0x600002f4c900
NSRunLoop
是基于 CFRunLoopRef
的一層OC包裝, 官方開(kāi)源了Core Foundation 的源碼實(shí)現(xiàn).
在源碼中, 我們查看一下 CFRunLoopGetCurrent() 到底做了什么?
過(guò)程:
- 調(diào)用
_CFRunLoopGet0()
, 并傳入?yún)?shù) 當(dāng)前線程. - 其中
__CFRunLoops
, 是存放以pthread
為key,RunLoop
為 value 的字典. - 如果從字典中未找到 Runloop對(duì)象, 則 調(diào)用
__CFRunLoopCreate
為這條線程創(chuàng)建新的RunLoop , 并存儲(chǔ)到字典中.
由此我們知道了Runloop 和 線程 的關(guān)系
- 每條線程都有與之對(duì)應(yīng)的 RunLoop 對(duì)象.
- 線程剛創(chuàng)建的時(shí)候是沒(méi)有 Runloop 的, 程序在運(yùn)行的過(guò)程中, 會(huì)為這條線程創(chuàng)建對(duì)應(yīng)的 RunLoop 對(duì)象, RunLoop 隨著線程結(jié)束而銷(xiāo)毀
- 線程 和 runloop 分別以鍵值對(duì)的形式存儲(chǔ)在字典中, 方便程序管理.
Core Foundation中關(guān)于RunLoop的5個(gè)類(lèi)
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
這是 CFRunLoopRef 的實(shí)現(xiàn), 圖中摘取了幾個(gè)比較在意的成員變量.
CFRunLoopModeRef 代表 RunLoop 的運(yùn)行模式
常用到的有兩種
kCFRunLoopDefaultMode (Mode的名字)
App的默認(rèn)Mode, 通常主線程是在這個(gè)Mode下運(yùn)行的UITrackingRunLoopMode (Mode的名字)
界面追蹤Mode, 用于ScrollView 追蹤觸摸滑動(dòng), 保證界面滑動(dòng)時(shí)不受其他Mode影響
-
RunLoop
啟動(dòng)時(shí)只能選擇其中的一個(gè)Mode
, 作為currentMode
. - 如果需要切換
Mode
, 只能退出當(dāng)前Loop
, 再重新選擇一個(gè)Mode
進(jìn)入. - 不同
Model
的 Source0/Source1/Timer/Observer 分隔開(kāi)來(lái), 互不影響. - 如果
Mode
中沒(méi)有任何 Source0/Source1/Timer/Observer,RunLoop
會(huì)立馬退出.
- Source0: 觸摸事件處理, performSelector: OnThread 等.
- Source1: 基于 Port 的線程間通信, 處理系統(tǒng)事件捕捉等.
- Timers: NSTimer操作, performSelector:withObject:afterDelay:等
- Observers: 監(jiān)聽(tīng)RunLoop的狀態(tài), UI刷新(BeforeWaiting), Autorelease pool(BeforeWaiting).
當(dāng)設(shè)置完view的背景色時(shí), 這段代碼不會(huì)立即生效, 而是等待 RunLoop 即將休眠的時(shí)候, 刷新界面
Mode | Name | Description |
---|---|---|
Default |
NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) |
默認(rèn)模式是用于大多數(shù)操作的模式. 大多數(shù)情況下雷滚,您應(yīng)該使用此模式啟動(dòng)運(yùn)行循環(huán)并配置輸入源. |
Connection |
NSConnectionReplyMode (Cocoa) |
Cocoa將此模式與NSConnection對(duì)象結(jié)合使用以監(jiān)視回復(fù). 很少使用此模式. |
Modal |
NSModalPanelRunLoopMode (Cocoa) |
Cocoa使用此模式來(lái)識(shí)別用于模態(tài)面板的事件. |
Event tracking |
NSEventTrackingRunLoopMode (Cocoa) |
Cocoa使用此模式在鼠標(biāo)拖動(dòng)循環(huán)和其他種類(lèi)的用戶界面跟蹤循環(huán)期間限制傳入事件. (拖動(dòng)scrollView) |
Common modes |
NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) |
這是一組可配置的常用模式. 將輸入源與此模式相關(guān)聯(lián)也會(huì)將其與組中的每個(gè)模式相關(guān)聯(lián). 對(duì)于Cocoa應(yīng)用程序, 此集合默認(rèn)包括默認(rèn), 模態(tài)和事件跟蹤模式. Core Foundation最初只包含默認(rèn)模式. 您可以使用CFRunLoopAddCommonMode函數(shù)將自定義模式添加到集合中. |
詳解RunLoop
前面我們從源碼層面了解RunLoop, 現(xiàn)在我們從整體再來(lái)看.
有幾點(diǎn)我們需要注意:
-
- RunLoop 從兩種不同類(lèi)型的源接收事件
-
Input sources
提供異步事件, 通常來(lái)自另外一個(gè)線程的消息, -
Timer sources
提供同步事件, 發(fā)生在預(yù)定時(shí)間, 或重復(fù)間隔.
-
- RunLoop Modes 是要監(jiān)視的 Input sources 和 Timer sources 的集合恢共,以及要通知的RunLoop observer的集合.
- 每次運(yùn)行 RunLoop 時(shí), 都顯示/隱式 指定特定的運(yùn)行模式.
- 模式是根據(jù)事件的來(lái)源而不是事件的類(lèi)型進(jìn)行區(qū)分的, 比如不會(huì)使用模式僅匹配鼠標(biāo)按下事件或僅匹配鍵盤(pán)事件.
-
- Input sources 中通常有兩類(lèi).
- 基于端口的 Input source 監(jiān)視應(yīng)用程序的 Mach 端口, 它是由內(nèi)核自動(dòng)發(fā)出信號(hào).
- 自定義 Input source 處理自定義事件源, 它必須由另一個(gè)線程手動(dòng)發(fā)信號(hào)給自定義源.
- Cocoa 還定義了一個(gè)自定義輸入源, Cocoa Perform Selector Sources, 它允許我們?cè)谌魏尉€程上執(zhí)行選擇器, 并且執(zhí)行其選擇器后將其自身從 RunLoop 中移除.
- runUtilDate: 是 NSRunLoop 類(lèi)的對(duì)象方法, 用來(lái)運(yùn)行 RunLoop.
- handlePort:, customSrc:, mySelector:, timeFired 是來(lái)自不同的源的事件(消息).
- Timer sources 在將來(lái)的預(yù)設(shè)時(shí)間將事件同步傳遞給你的線程蓝谨。定時(shí)器是線程通知自己做某事的一種方式.
補(bǔ)充說(shuō)明: Loop Observer
與在發(fā)生適當(dāng)?shù)漠惒交蛲绞录r(shí)觸發(fā)的源不同铺罢,RunLoop observer 在執(zhí)行 RunLoop 期間, 在特殊位置觸發(fā).
RunLoop的多種狀態(tài):
- kCFRunLoopEntry: 即將進(jìn)入 RunLoop
- kCFRunLoopBeforeTimers: 即將處理 Timer
- kCFRunLoopBeforeSources: 即將處理 Sources
- kCFRunLoopBeforeWaiting: RunLoop 即將休眠
- kCFRunLoopAfterWaiting: RunLoop 即將喚醒
- kCFRunLoopExit: 即將退出RunLoop
RunLoop的事件處理
每次運(yùn)行 RunLoop 時(shí), 線程的RunLoop都會(huì)處理掛起的事件, 并且為任何附加的觀察者生成通知. (App一啟動(dòng), 會(huì)自動(dòng)在主線程設(shè)置并運(yùn)行RunLoop, 稱(chēng)之為 主循環(huán))
- Notify observers: 進(jìn)入運(yùn)行循環(huán).
- Notify observers: 即將處理 Timer.
- Notify observers: 即將處理Sources
- 處理Source0: 觸發(fā)任何準(zhǔn)備觸發(fā)的基于非端口的輸入源, 跳到第 9 步:
- 處理Source1: (如果基于端口的輸入源準(zhǔn)備就緒并等待觸發(fā)), 就跳到第 9 步:
- Notify observers: 線程即將休眠(等待消息喚醒)
-
Notify observers: 線程結(jié)束休眠(被下面的消息喚醒)
- 處理Timer
- 處理Source1: 事件到達(dá)基于端口的輸入源
- RunLoop 被明確喚醒
- 為 RunLoop 設(shè)置的超時(shí)值到期
- Notify observers: 線程剛剛醒來(lái).
-
處理 Blocks:
- 如果輸入源被觸發(fā)瓢棒,則傳遞事件.
- 如果觸發(fā)了用戶定義的計(jì)時(shí)器,則處理計(jì)時(shí)器事件并重新RunLoop。轉(zhuǎn)到第2步.
- 如果運(yùn)行循環(huán)被明確喚醒但尚未超時(shí),請(qǐng)重新RunLoop, 轉(zhuǎn)到第2步
- Notify observers: RunLoop 已退出
使用 RunLoop
我們需要顯示運(yùn)行 RunLoop 的唯一時(shí)機(jī)是為應(yīng)用程序創(chuàng)建輔助線程, 對(duì)于輔助線程, 如果確定需要運(yùn)行循環(huán), 那么需要配置并運(yùn)行它.
- 在線程上使用 Timer.
- 保持線程以執(zhí)行定期任務(wù)(線程迸ブ耄活).
- 使用端口或自定義輸入源與其他線程通信.
1. 解決NSTimer在滑動(dòng)時(shí)停止工作的問(wèn)題
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"==>%d",_count++);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
原因:
RunLoop 處理事件的默認(rèn) Mode 是 Default, 當(dāng) app 同時(shí)有計(jì)時(shí)器事件和scrollView滾動(dòng)事件時(shí), 優(yōu)先處理 scrollView 滾動(dòng)事件(Event tracking Mode), 處理完才會(huì)再來(lái)處理計(jì)時(shí)器事件.
解決辦法:
將 計(jì)時(shí)器事件 與 Common Mode 綁定, RunLoop 內(nèi)部會(huì)自動(dòng)切換 Tracking Mode 和 Default Mode, 來(lái)處理計(jì)時(shí)器事件 和 scrollView 滾動(dòng)事件, 使得兩者看似同時(shí)在工作.
2. 線程保活
LCThread 類(lèi)是一個(gè)繼承自 NSThread 的類(lèi), 在里面我們實(shí)現(xiàn)了 dealloc 方法, 為了監(jiān)測(cè)線程是否被銷(xiāo)毀的情況.
self.thread = [[LCThread alloc] initWithBlock:^{
// 一直在運(yùn)行. 線程逼噬牛活
NSLog(@"----begin----%s", __func__);
// 當(dāng)前runloop開(kāi)始睡眠, 當(dāng)前線程被阻塞了
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"----end----%s", __func__);
}];
// 啟動(dòng)此線程
[self.thread start];
保證線程不立刻被銷(xiāo)毀, 我們?cè)诖似陂g制定任務(wù)
比如: 點(diǎn)擊屏幕. 打印此線程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
手動(dòng)釋放線程
- (void)stopThread{
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)stop
{
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空線程
self.thread = nil;
}
3. 監(jiān)控界面卡頓
通過(guò) RunLoop observer 來(lái)監(jiān)控目標(biāo) RunLoop 的狀態(tài), 如果頻繁出現(xiàn) kCFRunLoopBeforeSources, kCFRunLoopAfterWaiting, 檢測(cè)出現(xiàn)次數(shù), timeCount, 超過(guò)指定次數(shù)可認(rèn)為App卡頓 .
因?yàn)檫@兩個(gè)狀態(tài)是要去處理事件的狀態(tài).