前言
这篇文章是两年前在公司内部分享时候写的,自己比较满意文章的成色,就分享出来。
最近在进行社招的过程中,发现很多iOS五年以上的老司机对Runloop机制都不能很好的理解,而Runloop是iOS中非常重要的一个组件,很多性能优化,都需要对Runloop机制有深刻的理解才能进行。所以整理了以前的笔记,抽丝剥茧的分析Runloop的核心机制。
当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop就是控制线程生命周期并接收事件进行处理的机制。是iOS、OSX开发中比较基础的一个概念。但它也是事件响应与任务处理的核心机制,贯穿整个系统。
RunLoop 基本概念
为什么引入RunLoop机制
一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop可以让线程随时处理事件但不退出。
RunLoop并不是iOS平台的专属概念,在任何平台的多线程编程中,为控制线程的生命周期,接收处理异步消息都需要类似RunLoop的循环机制实现,Android的Looper就是类似的机制。这种模型还通常被称作Event Loop。Event Loop在很多系统和框架里都有实现。比如Node.js 的事件处理,Windows程序的消息循环。
实现这种模型的关键点在于:
- 如何管理事件和消息
- 如何让线程在没有处理消息时休眠,避免无谓的资源占用
- 如何在有消息到来时立即被唤醒。
引入Runloop机制的目的是利用RunLoop机制的特点实现整体省电的效果,并且让系统和应用可以流畅的运行,提高响应速度,达到极致的用户体验。
本质上RunLoop是什么
进程是一家工厂,线程是一个流水线,Run Loop就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,Run Loop就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,Run Loop就会暂时停下流水线,节约资源。
RunLoop管理流水线,流水线才不会因为无所事事被工厂销毁;而不需要流水线时,就会辞退RunLoop这个主管,即退出线程,把所有资源释放。
RunLoop实质上是一个对象,这个对象管理了其需要处理的的事件和消息,并提供了入口函数来执行Event Loop 的逻辑。线程执行这个函数后,会一直处于这个函数内部:接受消息->等待->执行 的循环中,直到这个循环结束。
iOS、OSX系统提供了两个RunLoop系统:
- Foundation: NSRunLoop 是基于CFRunLoopRef的封装,提供了面向对象的API,这些API不是线程安全的
- Core Foundation: CFRunLoopRef 核心部分,代码开源,C 语言编写,跨平台。所有API都是线程安全的
CFRunLoopRef源代码: http://opensource.apple.com/tarballs/CF/
RunLoop的特性
- 主线程的RunLoop在应用启动的时候就会自动创建
- 其他线程则需要在该线程下自己启动
- 不能直接创建RunLoop
- RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
- RunLoop负责管理autorelease pools
- RunLoop负责处理消息事件,即输入源事件和计时器事件
RunLoop与线程的关系
iOS开发中有两个线程对象:pthread_t和NSThread。过去NSThread只是pthread_t的封装,现在它们都是直接包装自最底层的mach thread。
pthread_t同NSThread是一一对应的。可以通过 pthread_main_np() 或 [NSThread mainThread]获取主线程。也可以通过 pthread_self() 或 [NSThread currentThread] 获取当前线程。CFRunLoop是基于pthread来管理的,NSRunLoop是基于NSThread管理的。
苹果不允许直接创建RunLoop,但是提供了两个获取RunLoop的函数。CFRunLoopGetMain()和CFRunLoopGetCurrent()。
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果不主动获取,一直不会有。RunLoop的创建是发生在第一次获取时
RunLoop的内部组件
一个RunLoop可以包含多个Mode,每个Mode有包含多个Source、Timer、Observer。每次调用RunLoop主入口函数时,只能指定一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,需要退出本次循环,再重新指定一个Mode进入。
Source、Timer、Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
Source
事件源,在CF中的类型是 CFRunLoopSourceRef
事件源是事件产生的地方,有两个类型:Source0 和 Source1。
- Source0 只包含了一个回调函数,这个回调函数是一个函数指针,它不能主动触发事件。在使用时,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用,然后调用 CFRunLoopWakeUp(runloop) ,唤醒RunLoop。
- Source1 包含了一个Mach_port 和一个回调函数。主要用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程。
Timer
计时器,在CF中的类型是 CFRunLoopTimerRef
CF计时器同NSTimer是toll-free bridged 的,可以互相转换。
包含了一个时间长度和一个回调函数(IMP)。当它加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒执行timer中的函数指针。
Observer
观察者,在CF中的类型是CFRunLoopObserverRef
用于监听RunLoop的状态变化
包含了一个回调函数,当RunLoop的状态发生变化的时候,观察者可以通过回调接收到这个变化。
有以下状态:
- kCFRunLoopEntry 即将进入Loop
- KCFRunLoopBeforeTimers 即将处理 Timer
- KCFRunLoopBeforeSources 即将处理 Source
- KCFRunLoopBeforeWaiting 即将进入休眠
- KCFRunLoopAfterWaiting 从休眠中唤醒
- KCFRunLoopExit 退出Loop
Mode
定义
RunLoop Mode就是流水线上支持生产的产品类型,流水线在一个时刻只能在一种模式下运行,生产某一类型的产品。mode item就是订单。mode主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。
结构
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
};
Mode 类型
- 1、Default:NSDefaultRunLoopMode,默认模式,在Run Loop没有指定Mode的时候,默认就跑在Default Mode下
- 2、Event tracking:UITrackingRunLoopMode,拖动事件
- 3、Connection:NSConnectionReplyMode,用来监听处理网络请求NSConnection的事件
- 4、Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
- 5、 UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- 6、 GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode。
- Common mode:NSRunLoopCommonModes,是一个模式集合,当绑定一个事件源到这个模式集合的时候就相当于绑定到了集合内的每一个模式。
Common Mode
将ModeName添加到RunLoop结构体中的commonModes集合中, 这个mode就具有Common属性,成为了CommonMode。当RunLoop发生变化或者发生事件的时候,会将commonModeItems中的Source、Timer、Observer同步到具有Common属性的Mode中。
主线程有两个预置Mode。kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个Mode都已经具有Common属性。timer加入到DefaultMode中,默认情况会被调用,但是当ScrollView滚动的时候,RunLoop会将Mode切换为UITrackingRunLoopMode,这是因为为了更好的用户体验,在主线程中Event tracking模式的优先级最高。这时timer不会被调用。如果需要这个timer在两个Mode中都能得到调用,可以将timer分别加入这两个Mode中。也可以将timer加入到commonModelItems中,这样会被所有具有Common属性的Mode调用。
解决方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerHandler:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//或
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
特定时间执行mode
RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:)
RunLoop 实现逻辑分析
RunLoop核心代码
// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, 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。
// Observer会创建AutoReleasePool _objc_autoreleasePoolPush()
__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 即将退出。
// observer 释放AutoReleasePool _objc_autoreleasePoolPop()
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
实际上 RunLoop 其内部是一个 do-while 循环。RunLoop的核心就是一个MachMessage的调用过程,RunLoop调用这个函数去接收消息,如果没有别人发送port消息过来,RunLoop会进入休眠状态。内核会将线程置于等待状态,这个时候whild循环是停止的,相比于一直运行的while循环,会很节省CPU资源,进而达到省电的目的。
因为Source1注册了MachPort端口,当有Source1事件进来的时候会通过这个port端口唤醒RunLoop继续执行,当计时器到了时候也会唤醒RunLoop,RunLoop唤醒后会先通过Observer 当前已经处于唤醒状态,之后会先执行Source1事件,执行完成后再执行timer、Source0。执行完所有的Source0事件后,如果有Source1事件则执行Source1,如果没有则通知Observe 进入到休眠状态。完成一个循环。
RunLoop 底层机制分析
操作系统架构
iOS/OSX操作系统核心是Darwin,Darwin包括了硬件层、XNU内核和Darwin库等组成部分。
XNU内核由IOKit、BSD、Mach三部分组成。
- IOKit 主要负责IO提供硬件通讯功能
- BSD,是一个类Unix系统,负责进程管理、文件系统、网络等功能。
- Mach是微内核,提供处理器调度、IPC等基础服务。
Mach通讯
RunLoop的底层实现基于Mach内核通讯实现的。在Mach中,所有的组件都是一个对象。进程、线程、虚拟内存都是对象。Mach对象之间不能直接调用,对象间的通讯是通过消息机制实现的。
Mach 的 IPC 的核心就是消息(message)在两个端口(port)之间传递。
消息实质上是一个二进制数据包,在头部定义了当前端口和目标端口。
消息的发送接收通过mach_msg()函数实现,mach_msg()函数内部会通过调用mach_msg_trap()函数将现在的用户态切换到内核态。进入到内核态后,会调用Mach内核的mach_msg()函数完成消息传递工作,注意此mach_msg和第一个mach_msg虽然函数名相同,但是实现和意义是完全不同的。
RunLoop的核心机制就是通过调用 mach_msg 等待接受 mach_port 的消息。线程进入休眠, 直到被下面某一个事件唤醒。事件通过mach_port 发送 mach_msg 唤醒RunLoop。
总结
RunLoop是一个do-while 循环,又不是一个do-while 循环。他的工作模式是一个循环,但是他基于mach_port和mach_msg的 休眠\唤醒 机制确保了他可以在无任务的时候休眠,有任务的时候及时唤醒,相比于一个普通循环,不会空转,不会浪费系统资源。RunLoop又通过不同的工作mode隔离了不同的事件源,使他们的工作互不影响。这才是RunLoop实现省电,流畅,响应速度快,用户体验好的根本原因;进而基于RunLoop的组件如计时器、GCD、界面更新、自动释放池能高效运转的根本原因。
面试考察点
- 为什么引入Runloop机制,有什么作用或者好处?
引入Runloop机制的目的是利用RunLoop机制的特点实现整体省电的效果,并且让系统和应用可以流畅的运行,提高响应速度,达到极致的用户体验。
- 为什么省电?
主要有两点:一、因为不做任何操作的时候主线程Runloop会处于退出状态,不会执行任何空转逻辑,不执行代码自然不消耗CPU资源,自然省电。二、Runloop提供一种班车机制,限制如页面刷新等任务的执行频率,一次Runloop只执行一次,防止多次重复执行代码带来的性能损耗。
- 为什么可以流程运行?
一个app流畅与否的决定性因素是主线程的阻塞率,在iOS系统中runloop每秒执行60次,理论上主线程runloop达到55帧以上的刷新频率用户就感觉不到卡顿。
Mode机制,同一时间只执行一个Mode内的Source或者Timer,比如拖动的时候只指定拖动Mode,其他Mode 如Default Mode中的源不会被执行,确保了高速滑动的时候不会有其他逻辑阻碍主线程刷新。
Runloop做的是管理视图刷新频率,防止重复运算。由于视图更新必须在主线程,视图的重布局和重绘都会占用主线程的执行时间,一次Runloop循环只执行一次可以最大限度的防止重复运算导致的计算浪费。
管理核心动画。核心动画有三个树,其中render tree 是私有的,应用开发无法访问到。render tree在专用的render server 进程中执行,是真正用来渲染动画的地方,线程优先级高于主线程。所以即使app主线程阻塞,也不会影响到动画的绘制工作。既节省了主线程的计算资源,又使动画可以流畅的执行。
支持异步方法调用,将耗时操作分发到子线程中进行。RunLoop是performSelector的基础设施。我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中。
还有其他的点,这里不展开,详情可阅读下文应用实践。
当然Runloop不是万能的,如果代码质量差,在一次Runloop循环中执行的时间过久一样会导致卡顿,所以解决卡顿问题也是程序员能力的体现。
- 如何提高响应速度?
当发生系统事件时,如触碰事件,系统通过Mach Port 发送 Mach消息主动唤醒Runloop。Mach是抢占式操作系统内核,Mach系统IPC机制就是依靠消息机制实现的,所以效率非常高。
iOS系统中RunLoop的应用实践在下一篇文章中阐述:深入浅出 RunLoop (2) — 应用实践