深入理解RunLoop和使用案例
RunLoop 是一种可以让线程能随时处理事件而不退出的机制。
代码逻辑如:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
1.RunLoop 有什么作用
RunLoop可以:
- 保持程序持续运行
- 处理APP中的各种事件(比如触摸事件、定时器事件、SEL事件)
- 节省CPU资源,提高程序性能:有事做事,没事休息
程序中的main函数里面:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在UIApplicationMain
里面就开启了一个RunLoop,这个默认启动的RunLoop是跟主线程关联的。它就可以处理我们上面说的那些事情,说白了就是让CPU有时间休息,没事的时候帮我们省电。
2.怎么访问 RunLoop
iOS中有2套API来访问和使用RunLoop
- Foundation中:NSRunLoop(API 不是线程安全的)
- Core Foundation中:CFRunLoopRefNSRunLoop(API 都是线程安全的)
2.1两者的关系:
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
RunLoop源码中的定义
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
2.1如何获得RunLoop对象:
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
3.RunLoop和线程的关系
每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
4.RunLoop的结构:
如图所示:
RunLoop的结构.png
一个RunLoop包含若干个Mode,而每个Mode又包含若干个Source、Timer、Observer。
对应的是:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
每个RunLoop启动时,只能指定一种Model,并且切换Mode时,只能先退出RunLoop,这样是为了分隔开不同组的Source、Timer、Observer。
4.1 RunLoop有5种Mode
系统默认注册了5个Mode:
NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行,可以把这个理解为一个”过滤器“,我们可以只对自己关心的事件进行监视。
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
5.RunLoop 的内部类
每个Mode又包含若干个Source、Timer、Observer,他们对应的类如下:
5.1 CFRunLoopTimerRef
- CFRunLoopTimerRef是基于时间的触发器(CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响)
- GCD的定时器不受RunLoop的Mode影响
5.2 CFRunLoopSourceRef
- CFRunLoopSourceRef是事件源(输入源)
- 按照官方文档,Source的分类
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
- 按照函数调用栈,Source的分类
Source0:非基于Port的, 用于用户主动触发事件
Source1:基于Port的,通过内核和其他线程相互发送消息
5.3 CFRunLoopObserverRef
可以监听的时机有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚才休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- 添加Observer
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
PS:`可以在此处做APM检测(kCFRunLoopBeforeTimers||kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 之间)程序掉帧上报server` 或者 `在CPU空闲时(kCFRunLoopBeforeWaiting)执行特定任务`
6.RunLoop 内部的逻辑
大致如图示:
RunLoop的内部逻辑.png
其内部代码整理如下:
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
总结
RunLoop 接收到两种事件就会去调用相应的方法处理
事件,两种事件分别是输入源(input source)和定时源 (timer source),换句话说,RunLoop就是所有要监视的输入源和定时源以及要通知的 run loop 注册观察 者的集合。
8.苹果用 RunLoop 实现的功能
- AutoreleasePool
- 事件响应
- 手势识别
- 界面更新
- 定时器
- PerformSelecter
- 关于GCD
- 关于网络请求
9. RunLoop 实际应用举例
9.1 常驻线程
AFNetworking 创建了一个线程,用于接收Delegate 回调;并在这个线程中启动了一个 RunLoop,使之成为常驻线程。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
9.2 AsyncDisplayKit
AsyncDisplayKit是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版
,绘制
,UI对象操作
。
排版
通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制
一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作
通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中排版
、绘制
可以通过各种方法扔到后台线程执行,而UI对象操作
只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
具体的代码可以看这里:_ASAsyncTransactionGroup。
9.3 定时器
- NSTimer
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
//1.将NSTimer添加在Default模式, 定时器只会运行在Default Mode下, 当拖拽时Mode切换为Tracking模式所以没反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 2.将NSTimer添加在Tracking模式, , 定时器只会运行在Tracking Mode下,当停止时Mode切换为Default模式所以没反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3.将NSTimer添加为被标记为Common的模式, Default和Tracking都被标记为了Common, 所以都有反应
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 4.scheduled创建的定时器默认添加在Default模式, 所以不用手动添加, 但是后期也可以修改
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
CADisplayLink
CADisplayLink
是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉
// 创建displayLink
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doSomething)];
// 将创建的displaylink添加到runloop中,否则定时器不会执行
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 停止定时器
[displayLink invalidate];
displayLink = nil;
- GCD定时器
有这么一个需求,需要这么一个定时器,误差几乎为0的定时器,但是无论是
NSTimer
还是CADisplayLink
都会有误差,而且误差都比较大。
可以使用GCD创建一个尽可能精确的定时器。
Dispatch Source Timer 是一种与 Dispatch Queue 结合使用的定时器。当需要在后台 queue 中定期执行任务的时候,使用 Dispatch Source Timer 要比使用 NSTimer 更加自然,也更加高效(无需在 main queue 和后台 queue 之前切换)。
Dispatch Source Timer 首先其实是 Dispatch Source 的一种。下面是苹果官方文档里给出的创建 Dispatch Timer 的代码:
创建 Timer
dispatch_source_t CreateDispatchTimer(uint64_t interval,//时间间隔
uint64_t leeway, //允许误差
dispatch_queue_t queue,//block的执行队列
dispatch_block_t block)//绑定的block
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
有几个地方需要注意:
- Dispatch Source Timer 是间隔定时器,也就是说每隔一段时间间隔定时器就会触发。在 NSTimer 中要做到同样的效果需要手动把 repeats 设置为 YES。
-
dispatch_source_set_timer
中第二个参数,当我们使用dispatch_time
或者DISPATCH_TIME_NOW
时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用dispatch_walltime
可以让计时器按照真实时间间隔进行计时。关于dispatch_walltime
和dispatch_time
的区别,可参考 StackOverflow 上的这个回答。 -
dispatch_source_set_timer
的第四个参数leeway
指的是一个期望的容忍时间,将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的leeway
值。需要注意,就算指定leeway
值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。 - event handler block 中的代码会在指定的 queue 中执行。当 queue 是后台线程的时候,dispatch timer 相比
NSTimer
就好操作一些了。因为NSTimer
是需要 Runloop 支持的,如果要在后台 dispatch queue 中使用,则需要手动添加 Runloop。使用 dispatch timer 就简单很多了。 -
dispatch_source_set_event_handler
这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而NSTimer
第一次执行是到计时器触发之后。这也是和NSTimer
之间的一个显著区别。
停止 Timer
停止 Dispatch Timer 有两种方法,一种是使用 dispatch_suspend,另外一种是使用 dispatch_source_cancel。
dispatch_suspend 严格上只是把 Timer 暂时挂起,它和 dispatch_resume 是一个平衡调用,两者分别会减少和增加 dispatch 对象的挂起计数。当这个计数大于 0 的时候,Timer 就会执行。在挂起期间,产生的事件会积累起来,等到 resume 的时候会融合为一个事件发送。
需要注意的是,dispatch source 并没有提供用于检测 source 本身的挂起计数的 API,也就是说外部不能得知一个 source 当前是不是挂起状态,在设计代码逻辑时需要考虑到这一点。
dispatch_source_cancel
则是真正意义上的取消 Timer。被取消之后如果想再次执行 Timer,只能重新创建新的 Timer。这个过程类似于对 NSTimer
执行 invalidate
。
关于取消 Timer,另外一个很重要的注意事项,dispatch_suspend
之后的 Timer,是不能被释放的!下面的代码会引起崩溃:
// 暂停 Timer
- (void)stopTimer
{
dispatch_suspend(_timer);
_timer = nil; // EXC_BAD_INSTRUCTION 崩溃
}
因此使用 dispatch_suspend
时,Timer 本身的实例需要一直保持。使用 dispatch_source_cancel
则没有这个限制
// 清除 Timer
- (void)clearTimer
{
dispatch_source_cancel(_timer);
_timer = nil; // OK
}
重启 Timer
- (void)resumeTomer
{
dispatch_resume(_timer);
}
9.4 PerformSelecter
- performSelecter:afterDelay:
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
// 如果该句代码所在的线程没有RunLoop,则doSomething不会被调用
[self performSelector:@selector(doSomething) withObject:nil afterDelay:1.0];
- performSelector:onThread:
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
// 如果 One_Thread 没有RunLoop,则doSomething不会被调用
[self performSelector:@selector(doSomething) onThread:One_Thread withObject:nil waitUntilDone:NO];
9.5 添加RunLoop监听
- 有这么一个需求:在CPU空闲时,执行特定任务
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
if (activity == kCFRunLoopBeforeWaiting) {
NSLog(@"===== CPU 空闲状态 ======= do something");
// TODO
// ...
}else {
NSLog(@"***** CPU 其他状态 *******");
}
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- 如何监控UI线程卡顿
监控卡顿,最直接就是找到主线程都在干些啥玩意儿。通过 NSRunLoop
的内部逻辑不难发现 NSRunLoop
调用方法主要就是在kCFRunLoopBeforeSources
和 kCFRunLoopBeforeWaiting
之间,还有 kCFRunLoopAfterWaiting
之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。
参考:
https://opensource.apple.com/source/CF/CF-855.17/CFRunLoop.c