什么是Runloop
- 運(yùn)行循環(huán)
- 跑圈
- 內(nèi)部類似一個 do-while 循環(huán), 在循環(huán)內(nèi)部不斷處理各種任務(wù) (Source, Observe, Timer)
- 一個線程對應(yīng)一個 RunLoop
用途
- 保持程序持續(xù)運(yùn)行
- 處理 APP 各種事件 (觸摸事件, 定時器事件, Selector事件)
- 節(jié)省 CPU 資源, 提高程序性能: 該做事情的時候做事情, 該休息時休息
沒有RunLoop
程序一啟動就結(jié)束了
int main(int argc, char * argv[]) {
NSLog(@"execute main function");
return 0;
}
如果有了 RunLoop
程序大致是這樣子,但是要更加復(fù)雜
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 執(zhí)行各種任務(wù),處理各種事件
// ......
} while (running);
return 0;
}
由于 main 函數(shù)里面啟動了一個 RunLoop, 因此程序不會馬上退出, 會保持程序的運(yùn)行狀態(tài)
main 函數(shù)中的 RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- UIApplicationMain 函數(shù)內(nèi)部啟動了一個 RunLoop 對象
- UIApplicationMain 函數(shù)一直沒有返回, 保持了程序的運(yùn)行
- 這個默認(rèn)啟動的 RunLoop 是與主線程相關(guān)
程序一旦啟動
- 執(zhí)行UIApplicationMain 函數(shù)
- 默認(rèn)啟動一個 RunLoop
- 這個 RunLoop 會一直處理主線程相關(guān)的事情
- 這個 RunLoop 會一直遍歷, 監(jiān)聽用戶事件
- 這就是主線程的事件響應(yīng)的這么快的原因
RunLoop 要想跑圈
- 模式(Mode)里面要有東西 (事件源 / Observer / 定時器)
- RunLoop 要啟動 (主線程默認(rèn)創(chuàng)建并啟動, 子線程需要手動啟動)
- 沒有事件源, 沒有定時器, RunLoop 就會進(jìn)入睡眠狀態(tài)
RunLoop 對象
iOS 中提供了兩套 API 來訪問和使用 RunLoop
- Foundation : NSRunLoop
- Core Foundation : CFRunLoopRef
NSRunLoop 是基于 CFRunLoopRef 的OC 包裝, 如果研究 RunLoop 內(nèi)部結(jié)構(gòu), 需要研究 CFRunLoopRef
RunLoop 與線程
- 每條線程都有唯一一個與之對應(yīng)的 RunLoop 對象
- 主線程的 RunLoop 已經(jīng)創(chuàng)建好, 子線程的 RunLoop 需要手動創(chuàng)建
- RunLoop 在第一次獲取時創(chuàng)建, 在線程結(jié)束時銷毀
- RunLoop 對象是使用字典存儲, 以線程作為 key
RunLoop 相關(guān)類
01 - CFRunLoopModeRef
- CFRunLoopModeRef 代表著RunLoop的運(yùn)行模式
- 一個RunLoop包含若干個Mode,每個Mode又包含若干個 Source/Timer/Observer
- 每次RunLoop啟動時, 都會指定其中一個Mode, 這個Mode被稱作CurrentMode
- 如果需要切換 Mode, 只能退出 RunLoop, 再重新指定一個 Mode 進(jìn)入
系統(tǒng)默認(rèn)注冊了 5 個 Mode :
- kCFRunLoopDefultMode : APP 的默認(rèn) Mode, 通常主線程是在這個 Mode下
- UITrackingRunLoopMode :
界面跟蹤 Mode, 用于 scrollView 跟蹤觸摸滑動, 保證界面不受其他 Mode 影響 (添加定時器不好使) - UIInitializationRunLoopMode :
在剛啟動 APP 時進(jìn)入的第一個 Mode, 啟動完就不再使用 - GSEventReceiveRunLoopMode :
接收系統(tǒng)事件的內(nèi)部 Mode, 通常用不到 - kCFRunLoopCommonModes :
這是一個占位用的 Mode, 不是一個真正的 Mode (也就說 RunLoop 無法啟動此模式)
02 - CFRunLoopTimerRef
- CFRunLoopTimerRef 是基于時間的觸發(fā)器
- 基本上相當(dāng)于 NSTimer
- 定時器會跑在 common modes 模式下
- 標(biāo)記為 common modes 的模式有:
- kCFRunLoopDefultMode
- UITrackingRunLoopMode
定時器添加到 kCFRunLoopDefultMode
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
將定時器添加到 NSDefaultRunLoopMode , 滑動 scollView 的時候, 定時器就會停止執(zhí)行, RunLoop 此時會自動切換到 UITrackingRunLoopMode 模式, 定時器就會停止執(zhí)行
定時器添加到 NSRunLoopCommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
將定時器添加到 NSRunLoopCommonModes, 此時就不會停止執(zhí)行
03 - CFRunLoopSourceRef
- CFRunLoopSourceRef是事件源(輸入源)
以前的分法
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
現(xiàn)在的分法
- Source0:非基于Port的
- Source1:基于Port的
04 - CFRunLoopObserverRef
- CFRunLoopObserverRef是觀察者臀突,能夠監(jiān)聽RunLoop的狀態(tài)改變
- (void)observer
{
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
});
// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
}
- 可以監(jiān)聽的時間點有以下幾個
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將推出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件
};
RunLoop 處理邏輯
下圖簡介了 RunLoop 處理過程, 一個線程的 RunLoop 在存在事件源 / 定時器的條件下, 會不斷的處理事件, 處理的事件包括
- 處理基于 port 的 CFRunLoopSourceRef
- 處理 customer 自定義事件源
- 處理 selector 事件
- 處理定時器執(zhí)行
官方的圖解很清楚, RunLoop 在不停的跑圈, 跑圈的前提是滿足以下條件之一:
- 輸入源 (事件源), 即 CFRunLoopSourceRef, 基于端口的輸入源 (port) 和 自定義輸入源 (custom), 當(dāng)然還包含 performSelector:onThread...
- 擁有添加在 RunLoop 內(nèi)的定時器
RunLoop 實際應(yīng)用
(1) 常駐線程
即讓子線程處于 "不消亡" 的狀態(tài), 一直在后臺處理某些頻發(fā)事件 / 等待其他線程發(fā)來消息
- 在子線程監(jiān)控網(wǎng)絡(luò)狀態(tài)
- 在子線程開啟一個定時器
- 在子線程長期監(jiān)控其他行為
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];
}
}
摘自 AFNetworking 源代碼, AFN這樣做的原理在于子線程下默認(rèn)不開啟 RunLoop, 需要手動開啟, 而 RunLoop 不斷跑圈需要滿足以下條件之一 :
- RunLoop有事件源(輸入源), 包含基于端口 (port) 的事件源 / custom 事件源等
- RunLoop存在定時器
因此, AFN為RunLoop的default模式增加了一個NSMachPort端口(實際上也可以是其他端口),也就相當(dāng)于為RunLoop添加了事件源, 因此RunLoop可以不斷的跑圈, 保證線程的不死狀態(tài)
順便提一下, AFN保持一個常駐線程的原因, 第一是因為子線程默認(rèn)不會開啟RunLoop, 它會像一個C語言程序一樣運(yùn)行完所有代碼后退出線程, 而網(wǎng)絡(luò)請求是異步的, 這就可能會出現(xiàn)通過網(wǎng)絡(luò)請求獲取到數(shù)據(jù)之后, 線程已經(jīng)退出, 無法執(zhí)行請求成功/失敗的代理方法, 因此AFN開啟了一個RunLoop, 保活了線程
(2) 控制定時器在特定模式下運(yùn)行
即可以將計時器 timer 添加到 kCFRunLoopDefultMode 下, 如果 RunLoop 切換到 UITrackingRunLoopMode (UIScrollView 滾動過程中), 那么定時器就會暫停執(zhí)行, 等到滾動結(jié)束, 定時器就會繼續(xù)執(zhí)行
也可以將定時器 timer 添加到 NSRunLoopCommonModes 下, 此時不管有無 scrollView 滑動, 都不會影響 timer 的執(zhí)行
(3) 控制某些事件在特定模式下執(zhí)行
即可以讓某個 selector 在某個線程 (key) 的 RunLoop 下的特定模式下執(zhí)行 (數(shù)組中包含 Mode)
通過以下的 API :
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
(4) 添加 Observer 監(jiān)聽 RunLoop 狀態(tài), 可以監(jiān)聽點擊事件的處理 (在所有點擊事件之前做一些事情)
調(diào)用 C 語言函數(shù) CFRunLoopObserverCreateWithHandler () 創(chuàng)建 Observer, 監(jiān)聽某個 RunLoop 狀態(tài), 注意要手動釋放
關(guān)于自動釋放池與 RunLoop
Autorelease pool
在沒有手加Autorelease Pool的情況下,Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時釋放的捐腿,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop
也就意味著, 在@autorelasepool 中的代碼, 默認(rèn)都是加在了一個自動釋放池當(dāng)中, 這個自動釋放池是與主線程的 RunLoop 相關(guān), 內(nèi)部所有對象會在自動釋放池釋放的時候?qū)?nèi)部所有對象進(jìn)行一次 release 操作
至于主線程 RunLoop 下的自動釋放池什么時候釋放, 是在主線程 RunLoop 迭代 (睡眠)之前釋放, 這個 RunLoop 什么時候睡眠呢? 是在沒有接收任何輸入源(事件源)/定時器的條件下
自動釋放池什么時候釋放?
在 RunLoop 睡眠之前釋放 (KCFRunLoopBeforeWaiting), 也有人說 Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時釋放的, 實際是一個意思
什么時候用@autoreleasepool
根據(jù)Apple的文檔较店,使用場景如下:
- 寫基于命令行的的程序時捞奕,就是沒有UI框架,如AppKit等Cocoa框架時桥狡。
- 寫循環(huán)搅裙,循環(huán)里面包含了大量臨時創(chuàng)建的對象。(本文的例子)
- 創(chuàng)建了新的線程总放。(非Cocoa程序創(chuàng)建線程時才需要)
- 長時間在后臺運(yùn)行的任務(wù)呈宇。
RunLoop 研究資料
- 蘋果官方文檔
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html - CFRunLoopRef 是開源的
http://opensource.apple.com/source/CF/CF-1151.16/
參考資料
- 李明杰關(guān)于 RunLoop 的研究
- sunnyxx-http://blog.sunnyxx.com/2014/10/15/behind-autorelease/