基本认识
顾名思义,在程序运行的过程中循环做一些事情。
在开发的过程中,我们接触到的 NSTimer 相关、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求和自动释放池都是基于 RunLoop 实现。
项目的主程序入口 main
函数会返回一个 UIApplicationMain
,在这个过程中就会开启一个 RunLoop 对象,这个对象就会循环处理一些事情,当我们点击一个可以交互的 UI 控件的时候,程序会做出响应,这都是 RunLoop 的功劳。
所以说 RunLoop 可以保持程序的正常运行,能响应各种事件,并节省 CPU 资源,提高程序性能:没有事件的时候待命,有事件的时候处理事情。
RunLoop 对象
iOS 中有 2 套 API 访问和使用 RunLoop,分别是 Foundation
中的 NSRunLoop
和 Core Foudation
中的 CFRunLoopRef
,前者是后者的 Objective-C 封装。并且 CFRunLoopRef 是开源的,开源地址在这。下面就是获取当前的 RunLoop 对象:
[NSRunLoop currentRunLoop];
CFRunLoopGetCurrent();
RunLoop 和线程
每条线程都有一个唯一的一个与之对应的 RunLoop 对象,并且 RunLoop 保存在一个全局的 Dictionary 中,线程为 key,RunLoop 为 value。
刚创建的线程是没有 RunLoop 对象的,RunLoop 会在第一次获取它的时候创建。RunLoop 会随着线程的结束销毁,主线程比较特殊,会自动创建并获取 RunLoop。
在源码中,CFRunLoopGetCurrent 的实现为:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
我们看到最终调用的是 _CFRunLoopGet0
方法,该方法中有:
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 先从字典中查找是否有对应的 RunLoop
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 没查找到,创建
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
}
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 将新创建的 RunLoop 保存到全局的字典当中
loop = newLoop;
}
这验证了 RunLoop 会存在一个全局的字典当中这一说法。
pthreadPointer(t) 为线程。
RunLoop 相关的类
在 Core Foundation 中和 RunLoop 相关的有 5 个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopOberverRef
CFRunLoop 的底层结构为:
typedef struct __CFRunLoop* CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}
从结构体可以看出,一个 RunLoop 可以装着多个 mode,但实际只有一个 mode 是 currentMode。
__CFRunLoopMode 的结构为:
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; // 对应 CFRunLoopSourceRef 对象
CFMutableSetRef _sources1; // 对应 CFRunLoopSourceRef 对象
CFMutableArrayRef _observers; // 对应 CFRunLoopOberverRef 对象
CFMutableArrayRef _timers; // 对应 CFRunLoopTimerRef 对象
CFMutableDictionaryRef _portToV1SourceMap;
...
};
所以,总体关系如下:
RunLoop 启东时只能选一个 Mode 作为 Current Mode,若要切换 Mode,只能退出当前 RunLoop,重新选择一个 Mode 再进入。
这样做的好处是:不同组的 source0、source1、timer、observer 相互隔离,互不影响。
如果 Mode 中没有任何 source0、source1、timer、observer 则 RunLoop 会立即退出。
常见的 Mode
kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
,主线程是在这个 Mode 下执行的。UITrackingRunLoopMode
,界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证滑动时不被其他 Mode 影响。
其他的 Mode 开发中不常用。
并且需要注意的是,主线程切换 Mode 并不会导致程序退出,切换 Mode 的操作还是在事件循环中进行的,并不会打破事件循环。
那么创建一个 RunLoop 并选择一个 Mode 后,最终处理的就是 Mode 下的 source0、source1、timer、observer。
source0
一般指触摸事件处理,我们新建一个空白程序,在初始界面添加触摸方法,并在注释位置加断点:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__); // 加断点
}
进入调试环境后借助 bt
命令打印函数调用栈:
在上图 #9 的位置看到了 source0 的影子。
performSelector: onThread:
系列方法也是 source0 的一个范畴。
source1
- 基于 Port 的线程通信;
- 系统事件的捕捉;
如点击事件,一开始是由 source1 捕捉,然后分发给 source0 处理。
Timers
就是我们熟知的 NSTimer
,另,performSelector: withObject: afterDelay:
也属于 Timer 范畴。
obervers
- 用于监听 RunLoop 的状态;
- UI 刷新;
- Autorelease pool;
如对 UI 控件进行颜色设置的时候,并不会立即生效,监听器会在 RunLoop 休眠之前进行 UI 刷新。自动释放池同理。
有时候,我们也会手动添加 observer,RunLoop 有以下几种状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠状态唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
我们写个例子来监听这些状态:
// 创建 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放 observe
CFRelease(observer);
observeRunLoopActicities
为 C 语言函数:
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;
}
}
触摸了一下屏幕发现打印:
在触摸函数调用之前,RunLoop 的状态为 kCFRunLoopBeforeSources 即即将处理 source。
我们将触摸函数改为:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"This is a timer");
}];
}
增加了一个定时器,运行并触摸屏幕打印结果为:
在处理定时器之前,RunLoop 的状态为 kCFRunLoopAfterWaiting 即唤醒状态。
RunLoop 的运行逻辑
首先,通知 Observers 进入 Loop;
进入 Loop 后,再次通知 Observers,即将处理 Timers;
-
通知 Observers 即将处理 Sources;
- 处理 blocks;
- 处理 Source0,并且可能会再次处理 blocks;
如果没有 Source1,通知 Observers 进入休眠状态;
如果有 Source1,通知 Observers 结束休眠,处理消息事件;
a. 处理 Timer;
b. 处理 GCD 的队列;
c. 处理 Source1;处理 blocks;
-
根据前面的执行结果,决定如何操作:
- 可能不退出 RunLoop 继续从处理 Timer 开始;
- 若退出 RunLoop,会通知 Observers 退出 Loop;
通知 Observers 退出 Loop;
执行逻辑源码解读
在 CFRunLoop.c
中,SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
为整个 RunLoop 的入口。
去除细节和加锁代码,为:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 通知 Observers 进入 Loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 主要的运行逻辑
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // 通知 Observers 退出 Loop
return result;
}
在 __CFRunLoopRun
中有非常复杂的逻辑:
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,跳转到 handle_msg
goto handle_msg;
}
}
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl); // 即将休眠
...
do {
...
// 等待别的消息唤醒当前线程
__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 结束休眠
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 相关
...
} else { // 其余都被认定是 Source1 唤醒
...
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply); // 处理 Source1
...
} while (0 == retVal); // 整个 do-while 就是循环处理事件的部分
...
...
}
需要注意的是,在通知线程进入休眠的状态时候并非传统意义上的阻塞,而是真正的进入了休眠状态,也就是内核层面的休眠。
内核层面的 API 和操作系统打交道,并不开放,应用层面的 API 是开放的,可以进行 UI、网络层等编程。
RunLoop 的实际应用
控制线程周期
最典型的开源框架 AFNetworking 就是用了 RunLoop 的技术来控制子线程的生命周期:创建线程后,一直让线程处于内存中不销毁,在某一刻需要执行任务的时候就让子线程处理,在某一刻销毁子线程的话就停止 RunLoop。
假如,在控制器中有这样一个需求,启动控制器的时候就开启子线程,并进行线程保活在点击停止按钮的时候,就终止线程的 RunLoop,那么实现为:
#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]]; // 永不超时,RunLoop 永远执行
}
NSLog(@"==== end ====");
}];
}
- (IBAction)stopButtonDidClick:(id)sender {
if (!self.thread) return;
// 子线程执行终止 RunLoop
// YES 标识表示等待 stopRunLoop 执行完再继续走下面的逻辑
[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 在默认情况是 NSDefaultRunLoopMode 模式的,那么在复杂的 UI 控制其中,在滑动 UIScrollView
及其子类的时候模式会切换为 UITrackingRunLoopMode 模式,造成只能在NSDefaultRunLoopMode 模式下工作的 Timer 的停止工作,进而失效。
NSTimer 的
scheduled....
系列方法都是设置的默认模式,所以不建议使用。
那么解决办法就是:
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// ....
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
kCFRunLoopCommonModes
并不是一个真正的全新的模式,仅仅作为标记的作用,标记着任何模式下都是通用的、可行的。
在底层结构中,CFRunLoop 的结构体中:
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}
_commonModes
包装的就是 kCFRunLoopCommonModes 和 UITrackingRunLoopMode。
其他应用
- 监控应用卡顿
- 性能优化
这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。