基本認(rèn)識(shí)
顧名思義诈茧,在程序運(yùn)行的過程中循環(huán)做一些事情榜晦。
在開發(fā)的過程中矿酵,我們接觸到的 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è)過程中就會(huì)開啟一個(gè) RunLoop 對(duì)象脑漫,這個(gè)對(duì)象就會(huì)循環(huán)處理一些事情,當(dāng)我們點(diǎn)擊一個(gè)可以交互的 UI 控件的時(shí)候咙崎,程序會(huì)做出響應(yīng)优幸,這都是 RunLoop 的功勞。
所以說 RunLoop 可以保持程序的正常運(yùn)行褪猛,能響應(yīng)各種事件网杆,并節(jié)省 CPU 資源,提高程序性能:沒有事件的時(shí)候待命伊滋,有事件的時(shí)候處理事情碳却。
RunLoop 對(duì)象
iOS 中有 2 套 API 訪問和使用 RunLoop,分別是 Foundation
中的 NSRunLoop
和 Core Foudation
中的 CFRunLoopRef
笑旺,前者是后者的 Objective-C 封裝昼浦。并且 CFRunLoopRef 是開源的,開源地址在這筒主。下面就是獲取當(dāng)前的 RunLoop 對(duì)象:
[NSRunLoop currentRunLoop];
CFRunLoopGetCurrent();
RunLoop 和線程
每條線程都有一個(gè)唯一的一個(gè)與之對(duì)應(yīng)的 RunLoop 對(duì)象关噪,并且 RunLoop 保存在一個(gè)全局的 Dictionary 中,線程為 key乌妙,RunLoop 為 value使兔。
剛創(chuàng)建的線程是沒有 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); // 沒查找到悉盆,創(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)中這一說法盯荤。
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 中沒有任何 source0檩互、source1特幔、timer、observer 則 RunLoop 會(huì)立即退出闸昨。
常見的 Mode
kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
蚯斯,主線程是在這個(gè) Mode 下執(zhí)行的。UITrackingRunLoopMode
饵较,界面跟蹤 Mode拍嵌,用于 ScrollView 追蹤觸摸滑動(dòng),保證滑動(dòng)時(shí)不被其他 Mode 影響循诉。
其他的 Mode 開發(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)擊事件侈百,一開始是由 source1 捕捉,然后分發(fā)給 source0 處理翰铡。
Timers
就是我們熟知的 NSTimer
钝域,另,performSelector: withObject: afterDelay:
也屬于 Timer 范疇锭魔。
obervers
- 用于監(jiān)聽 RunLoop 的狀態(tài)例证;
- UI 刷新;
- Autorelease pool迷捧;
如對(duì) UI 控件進(jìn)行顏色設(shè)置的時(shí)候织咧,并不會(huì)立即生效胀葱,監(jiān)聽器會(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è)例子來監(jiān)聽這些狀態(tài):
// 創(chuàng)建 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放 observe
CFRelease(observer);
observeRunLoopActicities
為 C 語言函數(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;
如果沒有 Source1郭膛,通知 Observers 進(jìn)入休眠狀態(tài)晨抡;
如果有 Source1,通知 Observers 結(jié)束休眠则剃,處理消息事件耘柱;
a. 處理 Timer;
b. 處理 GCD 的隊(duì)列棍现;
c. 處理 Source1调煎;處理 blocks;
-
根據(jù)前面的執(zhí)行結(jié)果己肮,決定如何操作:
- 可能不退出 RunLoop 繼續(xù)從處理 Timer 開始士袄;
- 若退出 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;
// 判斷有沒有 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 的部分是判斷如何醒來的
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)打交道肋乍,并不開放鹅颊,應(yīng)用層面的 API 是開放的,可以進(jìn)行 UI墓造、網(wǎng)絡(luò)層等編程堪伍。
RunLoop 的實(shí)際應(yīng)用
控制線程周期
最典型的開源框架 AFNetworking 就是用了 RunLoop 的技術(shù)來控制子線程的生命周期:創(chuàng)建線程后,一直讓線程處于內(nèi)存中不銷毀觅闽,在某一刻需要執(zhí)行任務(wù)的時(shí)候就讓子線程處理帝雇,在某一刻銷毀子線程的話就停止 RunLoop。
假如蛉拙,在控制器中有這樣一個(gè)需求尸闸,啟動(dòng)控制器的時(shí)候就開啟子線程,并進(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 失效問題
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í)的操作所造成的,這樣可以通過 RunLoop 來檢測(cè)卡頓:添加 Observer 到主線程 RunLoop 中顽冶,通過監(jiān)聽 RunLoop 狀態(tài)的切換的耗時(shí)欺抗,達(dá)到監(jiān)控卡頓的目的。