![](https://img.haomeiwen.com/i453168/de49598796ee33b5.png)
基本认识
顾名思义,在程序运行的过程中循环做一些事情。
![](https://img.haomeiwen.com/i453168/10581a557416e202.png)
在开发的过程中,我们接触到的 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;
...
};
所以,总体关系如下:
![](https://img.haomeiwen.com/i453168/4efddd840210b540.png)
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
命令打印函数调用栈:
![](https://img.haomeiwen.com/i453168/5a9ba7be0fae247c.png)
在上图 #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;
}
}
触摸了一下屏幕发现打印:
![](https://img.haomeiwen.com/i453168/098c078b97643e8b.png)
在触摸函数调用之前,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");
}];
}
增加了一个定时器,运行并触摸屏幕打印结果为:
![](https://img.haomeiwen.com/i453168/e7ff703776a2e65b.png)
在处理定时器之前,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 状态的切换的耗时,达到监控卡顿的目的。
网友评论