基本認(rèn)識(shí)
顧名思義,在程序運(yùn)行的過(guò)程中循環(huán)做一些事情。
在開(kāi)發(fā)的過(guò)程中,我們接觸到的 NSTimer 相關(guān)、GCD Async Main Queue、事件響應(yīng)、手勢(shì)識(shí)別、界面刷新、網(wǎng)絡(luò)請(qǐng)求和自動(dòng)釋放池都是基于 RunLoop 實(shí)現(xiàn)。
項(xiàng)目的主程序入口 main
函數(shù)會(huì)返回一個(gè) UIApplicationMain
,在這個(gè)過(guò)程中就會(huì)開(kāi)啟一個(gè) RunLoop 對(duì)象,這個(gè)對(duì)象就會(huì)循環(huán)處理一些事情,當(dāng)我們點(diǎn)擊一個(gè)可以交互的 UI 控件的時(shí)候,程序會(huì)做出響應(yīng),這都是 RunLoop 的功勞。
所以說(shuō) RunLoop 可以保持程序的正常運(yùn)行,能響應(yīng)各種事件,并節(jié)省 CPU 資源,提高程序性能:沒(méi)有事件的時(shí)候待命,有事件的時(shí)候處理事情。
RunLoop 對(duì)象
iOS 中有 2 套 API 訪問(wèn)和使用 RunLoop,分別是 Foundation
中的 NSRunLoop
和 Core Foudation
中的 CFRunLoopRef
,前者是后者的 Objective-C 封裝。并且 CFRunLoopRef 是開(kāi)源的,開(kāi)源地址在這。下面就是獲取當(dāng)前的 RunLoop 對(duì)象:
[NSRunLoop currentRunLoop];
CFRunLoopGetCurrent();
RunLoop 和線程
每條線程都有一個(gè)唯一的一個(gè)與之對(duì)應(yīng)的 RunLoop 對(duì)象,并且 RunLoop 保存在一個(gè)全局的 Dictionary 中,線程為 key,RunLoop 為 value。
剛創(chuàng)建的線程是沒(méi)有 RunLoop 對(duì)象的,RunLoop 會(huì)在第一次獲取它的時(shí)候創(chuàng)建。RunLoop 會(huì)隨著線程的結(jié)束銷毀,主線程比較特殊,會(huì)自動(dòng)創(chuàng)建并獲取 RunLoop。
在源碼中,CFRunLoopGetCurrent 的實(shí)現(xiàn)為:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
我們看到最終調(diào)用的是 _CFRunLoopGet0
方法,該方法中有:
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 先從字典中查找是否有對(duì)應(yīng)的 RunLoop
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 沒(méi)查找到,創(chuàng)建
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
}
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 將新創(chuàng)建的 RunLoop 保存到全局的字典當(dāng)中
loop = newLoop;
}
這驗(yàn)證了 RunLoop 會(huì)存在一個(gè)全局的字典當(dāng)中這一說(shuō)法。
pthreadPointer(t) 為線程。
RunLoop 相關(guān)的類
在 Core Foundation 中和 RunLoop 相關(guān)的有 5 個(gè)類:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopOberverRef
CFRunLoop 的底層結(jié)構(gòu)為:
typedef struct __CFRunLoop* CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}
從結(jié)構(gòu)體可以看出,一個(gè) RunLoop 可以裝著多個(gè) mode,但實(shí)際只有一個(gè) mode 是 currentMode。
__CFRunLoopMode 的結(jié)構(gòu)為:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0; // 對(duì)應(yīng) CFRunLoopSourceRef 對(duì)象
CFMutableSetRef _sources1; // 對(duì)應(yīng) CFRunLoopSourceRef 對(duì)象
CFMutableArrayRef _observers; // 對(duì)應(yīng) CFRunLoopOberverRef 對(duì)象
CFMutableArrayRef _timers; // 對(duì)應(yīng) CFRunLoopTimerRef 對(duì)象
CFMutableDictionaryRef _portToV1SourceMap;
...
};
所以,總體關(guān)系如下:
RunLoop 啟東時(shí)只能選一個(gè) Mode 作為 Current Mode,若要切換 Mode,只能退出當(dāng)前 RunLoop,重新選擇一個(gè) Mode 再進(jìn)入。
這樣做的好處是:不同組的 source0、source1、timer、observer 相互隔離,互不影響。
如果 Mode 中沒(méi)有任何 source0、source1、timer、observer 則 RunLoop 會(huì)立即退出。
常見(jiàn)的 Mode
kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
,主線程是在這個(gè) Mode 下執(zhí)行的。UITrackingRunLoopMode
,界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證滑動(dòng)時(shí)不被其他 Mode 影響。
其他的 Mode 開(kāi)發(fā)中不常用。
并且需要注意的是,主線程切換 Mode 并不會(huì)導(dǎo)致程序退出,切換 Mode 的操作還是在事件循環(huán)中進(jìn)行的,并不會(huì)打破事件循環(huán)。
那么創(chuàng)建一個(gè) RunLoop 并選擇一個(gè) Mode 后,最終處理的就是 Mode 下的 source0、source1、timer、observer。
source0
一般指觸摸事件處理,我們新建一個(gè)空白程序,在初始界面添加觸摸方法,并在注釋位置加斷點(diǎn):
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__); // 加斷點(diǎn)
}
進(jìn)入調(diào)試環(huán)境后借助 bt
命令打印函數(shù)調(diào)用棧:
在上圖 #9 的位置看到了 source0 的影子。
performSelector: onThread:
系列方法也是 source0 的一個(gè)范疇。
source1
- 基于 Port 的線程通信;
- 系統(tǒng)事件的捕捉;
如點(diǎn)擊事件,一開(kāi)始是由 source1 捕捉,然后分發(fā)給 source0 處理。
Timers
就是我們熟知的 NSTimer
,另,performSelector: withObject: afterDelay:
也屬于 Timer 范疇。
obervers
- 用于監(jiān)聽(tīng) RunLoop 的狀態(tài);
- UI 刷新;
- Autorelease pool;
如對(duì) UI 控件進(jìn)行顏色設(shè)置的時(shí)候,并不會(huì)立即生效,監(jiān)聽(tīng)器會(huì)在 RunLoop 休眠之前進(jìn)行 UI 刷新。自動(dòng)釋放池同理。
有時(shí)候,我們也會(huì)手動(dòng)添加 observer,RunLoop 有以下幾種狀態(tài):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠狀態(tài)喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
我們寫個(gè)例子來(lái)監(jiān)聽(tīng)這些狀態(tài):
// 創(chuàng)建 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放 observe
CFRelease(observer);
observeRunLoopActicities
為 C 語(yǔ)言函數(shù):
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
觸摸了一下屏幕發(fā)現(xiàn)打印:
在觸摸函數(shù)調(diào)用之前,RunLoop 的狀態(tài)為 kCFRunLoopBeforeSources 即即將處理 source。
我們將觸摸函數(shù)改為:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"This is a timer");
}];
}
增加了一個(gè)定時(shí)器,運(yùn)行并觸摸屏幕打印結(jié)果為:
在處理定時(shí)器之前,RunLoop 的狀態(tài)為 kCFRunLoopAfterWaiting 即喚醒狀態(tài)。
RunLoop 的運(yùn)行邏輯
首先,通知 Observers 進(jìn)入 Loop;
進(jìn)入 Loop 后,再次通知 Observers,即將處理 Timers;
-
通知 Observers 即將處理 Sources;
- 處理 blocks;
- 處理 Source0,并且可能會(huì)再次處理 blocks;
如果沒(méi)有 Source1,通知 Observers 進(jìn)入休眠狀態(tài);
如果有 Source1,通知 Observers 結(jié)束休眠,處理消息事件;
a. 處理 Timer;
b. 處理 GCD 的隊(duì)列;
c. 處理 Source1;處理 blocks;
-
根據(jù)前面的執(zhí)行結(jié)果,決定如何操作:
- 可能不退出 RunLoop 繼續(xù)從處理 Timer 開(kāi)始;
- 若退出 RunLoop,會(huì)通知 Observers 退出 Loop;
通知 Observers 退出 Loop;
執(zhí)行邏輯源碼解讀
在 CFRunLoop.c
中,SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
為整個(gè) RunLoop 的入口。
去除細(xì)節(jié)和加鎖代碼,為:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 通知 Observers 進(jìn)入 Loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 主要的運(yùn)行邏輯
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // 通知 Observers 退出 Loop
return result;
}
在 __CFRunLoopRun
中有非常復(fù)雜的邏輯:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
...
...
int32_t retVal = 0;
do {
...
// 通知 Observers 即將處理 Timbers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers 即將處理 Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm); //再次處理 blocks
}
...
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 判斷有沒(méi)有 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Source1,跳轉(zhuǎn)到 handle_msg
goto handle_msg;
}
}
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl); // 即將休眠
...
do {
...
// 等待別的消息喚醒當(dāng)前線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
...
__CFRunLoopSetIgnoreWakeUps(rl);
__CFRunLoopUnsetSleeping(rl);
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 通知 Observers 結(jié)束休眠
handle_msg:
...
...
// if - else if - ... - else 的部分是判斷如何醒來(lái)的
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { // 被 Timers 喚醒
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())){
__CFArmNextTimerInMode(rlm, rl);
}
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { // 被 Timers 喚醒
CFRUNLOOP_WAKEUP_FOR_TIMER();
else if (livePort == dispatchPort) { // 被 GCD 喚醒
...
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); // 處理 GCD 相關(guān)
...
} else { // 其余都被認(rèn)定是 Source1 喚醒
...
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply); // 處理 Source1
...
} while (0 == retVal); // 整個(gè) do-while 就是循環(huán)處理事件的部分
...
...
}
需要注意的是,在通知線程進(jìn)入休眠的狀態(tài)時(shí)候并非傳統(tǒng)意義上的阻塞,而是真正的進(jìn)入了休眠狀態(tài),也就是內(nèi)核層面的休眠。
內(nèi)核層面的 API 和操作系統(tǒng)打交道,并不開(kāi)放,應(yīng)用層面的 API 是開(kāi)放的,可以進(jìn)行 UI、網(wǎng)絡(luò)層等編程。
RunLoop 的實(shí)際應(yīng)用
控制線程周期
最典型的開(kāi)源框架 AFNetworking 就是用了 RunLoop 的技術(shù)來(lái)控制子線程的生命周期:創(chuàng)建線程后,一直讓線程處于內(nèi)存中不銷毀,在某一刻需要執(zhí)行任務(wù)的時(shí)候就讓子線程處理,在某一刻銷毀子線程的話就停止 RunLoop。
假如,在控制器中有這樣一個(gè)需求,啟動(dòng)控制器的時(shí)候就開(kāi)啟子線程,并進(jìn)行線程保活在點(diǎn)擊停止按鈕的時(shí)候,就終止線程的 RunLoop,那么實(shí)現(xiàn)為:
#import "ViewController.h"
#import "VThread.h"
@interface ViewController ()
@property (nonatomic, strong) VThread* thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.stopped = NO;
__weak typeof(self) weakSelf = self;
self.thread = [[VThread alloc] initWithBlock:^{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 向 RunLoop 中添加 Observers、Timers、Sources
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode: NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; // 永不超時(shí),RunLoop 永遠(yuǎn)執(zhí)行
}
NSLog(@"==== end ====");
}];
}
- (IBAction)stopButtonDidClick:(id)sender {
if (!self.thread) return;
// 子線程執(zhí)行終止 RunLoop
// YES 標(biāo)識(shí)表示等待 stopRunLoop 執(zhí)行完再繼續(xù)走下面的邏輯
[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone: YES];
}
- (void)dealloc {
self.thread = nil;
[self stopRunLoop];
}
// 終止子線程的 RunLoop
- (void)stopRunLoop {
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
// 清空線程
self.thread = nil;
}
@end
NSTimer 失效問(wèn)題
NSTimer 在默認(rèn)情況是 NSDefaultRunLoopMode 模式的,那么在復(fù)雜的 UI 控制其中,在滑動(dòng) UIScrollView
及其子類的時(shí)候模式會(huì)切換為 UITrackingRunLoopMode 模式,造成只能在NSDefaultRunLoopMode 模式下工作的 Timer 的停止工作,進(jìn)而失效。
NSTimer 的
scheduled....
系列方法都是設(shè)置的默認(rèn)模式,所以不建議使用。
那么解決辦法就是:
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// ....
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
kCFRunLoopCommonModes
并不是一個(gè)真正的全新的模式,僅僅作為標(biāo)記的作用,標(biāo)記著任何模式下都是通用的、可行的。
在底層結(jié)構(gòu)中,CFRunLoop 的結(jié)構(gòu)體中:
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}
_commonModes
包裝的就是 kCFRunLoopCommonModes 和 UITrackingRunLoopMode。
其他應(yīng)用
- 監(jiān)控應(yīng)用卡頓
- 性能優(yōu)化
這里的卡頓檢測(cè)主要是針對(duì)在主線程執(zhí)行了耗時(shí)的操作所造成的,這樣可以通過(guò) RunLoop 來(lái)檢測(cè)卡頓:添加 Observer 到主線程 RunLoop 中,通過(guò)監(jiān)聽(tīng) RunLoop 狀態(tài)的切換的耗時(shí),達(dá)到監(jiān)控卡頓的目的。