Reference:
深入理解RunLoop
RunLoop 详解
程序运行时代码都是顺序执行的,执行完毕程序就结束退出了,了解APP工作机制的一个基础,是弄清楚APP如何实现在内存中常驻的。
概述
代码在程序的线程中顺序执行,当程序所有的线程中代码都运行完毕,程序就结束并退出了,iOS与MacOS中通过RunLoop机制来让线程可以一直保持运行,并循环的处理各类事件,从而让APP常驻在内存中持续工作 。系统的某些任务也依托RunLoop来完成,了解RunLoop的运行机制将有助于了解系统任务的机制,同时理解了RunLoop能清楚一个线程中(特别是主线程中)各类任务的代码执行顺序,还将有助于多线程并发编程时的程序设计。
概览
要点
- RunLoop就是让线程保持一直运行,并可以循环处理事件的机制;
- RunLoop线程一一对应,代码中不能直接创建,而是提供获取主线程和当前线程RunLoop的API,在第一次调取API时,API内部负责创建RunLoop,子线程的RunLoop需要手动启动;
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer;
- RunLoop必须指定一个Mode进行运行,运行后如果Mode中一个items都没有,将会退出运行;
- kCFRunLoopCommonModes是一个伪Mode,用它添加的items,在其他“CommonMode”运行时,也能得到触发,NSTimer通常需要通过此Mode添加到RunLoop中;
- Source分为Source0和Source1,Source0需要手动显示唤醒RunLoop来处理事件,可用于线程间发送消息,Source1是基于端口的,端口有消息能自动唤醒RunLoop来处理事件,用于监听内核端口的消息;
- Timer是在计算好的预设时间点由内核发送时间通知,唤醒RunLoop来处理Timer事件;
- Observer是RunLoop的观察者,RunLoop在某些活动节点会触发其回调,可以据此在对应的时机安排自己APP需要执行的任务;
- RunLoop的休眠/唤醒是通过Mach陷阱切换程序的应用态/内核态来实现的;
- Perform Selector系列方法中延迟派发、派发给其他线程、以及当前线程异步派发时,是通过创建源并加入到对应RunLoop中来实现的,必须要目标线程的RunLoop运行,派发的Selector才会被执行,Perform Selector添加的源会在执行后从RunLoop中移除;
- 用GCD为主线程派发任务,也是包装成待处理的Source1,再添加到主线程RunLoop中来实现的,但GCD为子线程(并发队列)派发任务,不是通过RunLoop实行的;
- UIEvent是UIKit在主线程中注册一个Source1,通过端口接收到Spring Board转发的IOHIDEvent,再在回调中处理,最后包装成UIEvent的;
详解
1.什么是RunLoop
RunLoop是一种iOS与MacOS中让线程保持一直运行,并可以循环处理事件的机制。RunLoop在CoreFoundation中通过CFRunLoopRef对象来实现,其提供纯C函数的、线程安全的API;同时还有基于CFRunLoopRef更上层的封装:NSRunLoop,其提供面向对象的API,但不是线程安全的。
2.RunLoop与线程的关系
RunLoop与线程是一一对应的,CoreFoundation与NSFoundation中都不提供直接创建RunLoop的API,而是提供了获取主线程RunLoop和当前线程RunLoop的API,在第一次调取线程的RunLoop获取API时,函数内部才为线程创建其RunLoop,并以线程为key,RunLoop为value,保存在一个全局字典中。
3.RunLoop的相关概念
RunLoop中的结构如图:
一个RunLoop中包含若干个Mode,每个Mode中又包含若干个Source、Timer和Observer,Source、Timer和Observer统称为items,一个item可以添加到不同的Mode,重复添加到同一个Mode没有效果;启动RunLoop时需要指定一个Mode,Mode中至少要有一个item,否则RunLoop会立即退出;RunLoop运行期间指定Mode下的items的回调会触发,如果指定运行的Mode属于”CommondMode”,则标记为 kCFRunLoopCommonModes的items的回调也会触发;要切换Mode,必需停止RunLoop,再指定新的Mode重新启动RunLoop。
在CoreFoundation中Mode和items对应的类为:CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef,下面将分别介绍它们的特性。
CFRunLoopModeRef
CFRunLoopModeRef类没有对外暴露,而是提供了一系列根据mode name来管理Mode和Mode下的items(Source、Timer和Observer)的API:
1 | // 管理Mode的接口 |
提供的接口只能通过mode name来管理mode,当传入一个新的mode name,而RunLoop内又没有对应的Mode时,RunLoop会自动创建对应的CFRunLoopModeRef;并没有提供删除Mode的API,所以RunLoop的Mode只能增加不能删除。
系统为主线程的RunLoop默认注册了五个Mode:
- kCFRunLoopDefaultMode:默认Mode,主线程通常也是在此Mode下运行;
- UITrackingRunLoopMode:界面追踪Mode,当需要追踪触摸滑动时,主线程会被切换到此Mode,避免其他items对界面滑动的影响;
- UIInitializationRunLoopMode:APP刚启动时使用的Mode,启动完成后就不再使用;
- GSEventReceiveRunLoopMode:接收系统事件的内部Mode,开发中通常用不到;
- kCFRunLoopCommonModes:占位Mode,不是一个真正的Mode,可以用来添加items,不能用来启动RunLoop;
上述Mode中开发相关的有:kCFRunLoopDefaultMode、UITrackingRunLoopMode和kCFRunLoopCommonModes,其中kCFRunLoopDefaultMode和UITrackingRunLoopMode是真正的Mode,而kCFRunLoopCommonModes是一个占位Mode。用kCFRunLoopCommonModes添加的items,在RunLoop以一个“CommondMode”运行时,也能得到触发。可以用CFRunLoopAddCommonMode将一个Mode标记为”CommondMode“(既将Mode添加到CFRunLoopRef的_commonModes集合中),kCFRunLoopDefaultMode和UITrackingRunLoopMode都是”CommondMode“。
事件队列中判断Source和Timer的Block是否触发代码逻辑大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) {
... ...
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
if (!doit) prev = curr;
if (doit) {
... ...
}
}
... ...
return did;
}
CFRunLoopSourceRef
RunLoop的事件源有两个:输入源和定时源,输入源即Source,可以通过为Mode添加Source,来为RunLoop添加需要处理的事件,在Cocoa层官方将Source分为三类:
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
CoreFoundation中输入源则分为非基于端口的Source0和基于端口的Source1,分别对应Custom Input Sources和Port-Based Sources,Cocoa Perform Selector Sources是Perform Selector系列方法创建的源,Perform Selector延迟派发时创建的是定时源,派发给其他线程或当前线程异步派发时创建的Source0,Perform Selector创建的源,在执行完后会自动从RunLoop中移除。
- Source0:只包含一个回调(函数指针),需要应用手动触发,不能主动唤醒RunLoop,使用时需要先调用CFRunLoopSourceSignal(source),将Source标记为待处理,再手动调用CFRunLoopWakeUp(runloop),唤醒RunLoop处理Source的事件。
- Source1:包含一个mach_port和一个回调(函数指针),由mach_port驱动,可以主动唤醒RunLoop,通常用于通过内核和其他线程互相发送消息。
CFRunLoopTimerRef
Timer就是定时源,是RunLoop的另一个事件源,也可以通过为Mode添加Timer,来为RunLoop添加需要处理的事件。CFRunLoopTimerRef包含一个时间长度和一个回调(函数指针),它与NSTimer是toll-free bridged的,可以混用。当Timer加入到RunLoop之后,会基于XNU内核的mk_timer或GCD计时器(根据CoreFoundation的版本不同而不同),在计算好的预设时间点由内核发送通知,唤醒RunLoop处理Timer的事件。
CFRunLoopObserverRef
Observer是RunLoop的观察者,包含一个回调(函数指针),当RunLoop运行到特定状态时会触发回调,并将状态传递给回调函数,会触Observer回调的状态包括:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
4.RunLoop的核心逻辑
CoreFoundation的版本不同,RunLoop的核心源码可能不一致,但逻辑不会有太大的出入,整体可以参考以下对CoreFoundation CF-855.17版本源码删减后的逻辑整理:
1 | /// 公开接口一:用DefaultMode启动 |
值得一提的是CF-855.17版本是CoreFoundation使用mk_timer和GCD计时器的一个过渡版本,源码中有分使用mk_timer和GCD计时器的条件编译,以上代码逻辑的整理中,只参照了使用GCD计时器的源码。
对RunLoop核心代码逻辑的关键流程概述如下:
- 通知Observer:RunLoop进入Loop;
- 通知Observer:RunLoop开始处理Timer;
- 通知Observer:RunLoop即将处理Source0;
- 执行Source0加入到事件队列中的Block;
- 如果是主线程RunLoop,检查是否有通过GCD派发给主线程的待处理的Source1:
- 没有:继续下一步;
- 有:跳到第9步去处理Source1;
- 通知Observer:RunLoop即将进入休眠;
- 休眠,等待被唤醒:
- 某个基于port的Source1有消息到达;
- 某个Timer的时间到了;
- RunLoop为自身设定的超时时间到了;
- 被其他调用者手动显式唤醒;
- 通知Observer:RunLoop刚被唤醒;
- 开始处理未处理的事件:
- 超时唤醒,不用做任何处理;
- Timer到时了,处理Timer的事件;
- 如果是主线程,处理GCD派发过来的事件;
- 处理基于port的Source1;
- 通知Observer:已经退出Loop;
RunLoop的代码内部是一个do-while循环,循环的执行上述流程的第2-9步,以下情况会导致此次流程后跳出循环,退出RunLoop:
- 启动RunLoop时的参数指明:执行一遍事件处理流程就退出循环;
- 超过了启动RunLoop时指定的超时时间;
- 被外部调用者手动显式的终止了RunLoop;
- RunLoop被标记为已停止(_stopped)了;
- 当前Mode中Source/Timer/Observer都被移除了;
RunLoop在需要时被唤醒,在不需要时进行休眠,其原理是通过mach_msg( )调用了一个Mach陷阱:mach_msg_trap( ),将线程由用户态切换为内核态,在内核态下等待消息,当有消息到达时又返回用户态进行处理,其逻辑示意如图:
5.系统中对RunLoop的应用
Perform Selector
Perform Selector系列方法延迟派发创建的是定时源,派发给其他线程或当前线程异步派发时创建的Source0,再添加到指定线程的RunLoop中。所以如果使用Perform Selector延迟派发、派发给其他线程、以及当前线程异步派发时,要注意判断目标线程的RunLoop是否在运行,如果未运行则派发的任务就不会被执行。
GCD
使用GCD为主线程派发任务,会唤醒主线程RunLoop,并对派发的事件进行处理,在每次主线程RunLoop休眠前,还会检查是否有GCD派发过来的事件,如果有,则会跳过休眠,开始对事件进行处理。但以上RunLoop中的逻辑,仅限于主线程中的RunLoop,通过GCD为其他线程派发的任务,则是由libDispatch处理,所以GCD将任务派发给子线程时,也不需要子线程的RunLoop处于运行状态。
UIEvent
系统为APP注册了一个Source1用来接收操作系统的事件。当一个硬件事件(触摸/锁屏/摇晃等)发生后,先由IOKit.framework生成一个IOHIDEvent,并由SpringBoard接收,SpringBoard再通过mach_port转发给需要的APP;APP中系统注册的那个Source1接收到mach_port中传递过来的消息后,唤醒主线程RunLoop,在其回调中调用_UIApplicationHandleEventQueue( )进行APP内部的分发;_UIApplicationHandleEventQueue( )中会把IOHIDEvent包装成UIEvent,再通过APP的响应链传递给对应的响应者。
NSTimer
NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridged的(Toll-Free Bridged Types)。根据版本不同RunLoop的Timer可能基于内核的mk_timer或者GCD的timer来实现的,Timer并不是绝对准时的,而是有一个宽容度(Tolerance),实际执行的时间可能在这个宽容度允许的误差之内,如果因为执行队列中其他事件造成时间超过了宽容度所允许的误差,那么这次Timer的block不会延后执行,而是会被跳过。
NSTimer默认是添加到RunLoop的kCFRunLoopDefaultMode中的,如果需要NSTimer在RunLoop切换为其他Mode运行时也会正确触发,需要通过kCFRunLoopCommonModes来添加NSTimer。
AutoreleasePool
APP启动后,系统为主线程RunLoop注册了两个Observer来管理主线程的自动释放池:
- 第一个Observer只观察kCFRunLoopEntry:在其回调中调用_objc_autoreleasePoolPush( )创建自动释放池;
- 第二个Observer观察两个事件:在其回调中观察到kCFRunLoopBeforWaiting,会调用_objc_autoreleasePoolPop( ),再调用_objc_autoreleasePoolPush( ),以释放旧池并创建新池;回调中观察到kCFRunLoopExit,会调用_objc_autoreleasePoolPop( )来释放自动释放池;
子线程中的自动释放池会在使用时懒加载,在线程退出时释放,所以一般子线程中也无需考虑内存自动释放的问题,但是如果通过运行子线程的RunLoop来让子线程常驻,可能需要考虑为某些自动释放对象添加自动释放池来避免内存泄漏。
UIKit
当更改了UI之后,比如改变了Frame、更新了UIView/CALayer的层次、或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay等,这个UIView/CALayer会被标记为待处理,然后提交到一个全局容器中去。系统为主线程注册了一个Observer来刷新UI,在其回调中,当观察到kCFRunLoopBeforWaiting和kCFRunLoopExit时,会调用一个函数去遍历所有待处理的UIView/CALayer,为它们执行实际的绘制和调整,来完成界面的刷新。
NSURLConnection与NSURLSession
NSURLConnection已经被NSURLSession取代,但底层的部分逻辑却在沿用,同时NSURLConnection对RunLoop的运用也可作为多线程编程设计的参考:开始网络任务时,会为NSURLConnection设置一个delegate,除了这个开始任务时的delegate线程,NSURLConnection又注册了两个新线程com.apple.NSURLConnectionLoader和com.apple.CFSocket.private,三个线程间的消息交流如图所示:
CFSocket线程负责处理底层socket连接,NSURLConnectionLoader则负责居中调度。开始一个网络任务时,NSURLConnection创建了4个Source0,并添加到了delegate线程RunLoop的DefaultMode中,NSURLConnectionLoader线程本身通过一个Source1来接收底层CFSocket线程根据网络连接产生的通知,在NSURLConnectionLoader线程的Source1回调中,又通过delegate线程的4个Source0把相关的通知传递给delegate线程,在这些Source0的回调中再执行真正的delegate回调方法。
6.三方库对RunLoop的应用
AFNetworking
在AFNetworking2.x中通过开启子线程的RunLoop来创建了一条常驻线程,相关代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29+ (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;
}
- (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];
}
AFNetworking2.x是基于NSURLConnection的,AFNetworking在常驻子线程中异步的start所有NSURLConnection,也在此线程中接收所有NSURLConnection的delegate回调,所以需要子线程常驻。
由于NSURLConnection需要delegate线程保持活跃来接收回调,如果维持一个常驻子线程来管理NSURLConnection的网络请求则开销太大了,在NSURLSession中已经改为指明delegateQueue(NSOperationQueue)来处理网络回调,AFNetworking3.x之后网络请求改为使用NSURLSession,所以也不再需要常驻子线程来处理网络回调了。
值得一提的是NSURLSession的delegateQueue的maxConcurrentOperationCount通常需要设为1,来将代理队列设置为一个串行队列,这是因为即使设置为并发队列,在回调中通常也需要加锁来处理,实际也是串行执行,直接设置为串行队列,则无需再添加加锁等操作。
ReactiveCocoa
在RAC的一个Testing方法中,通过如下方法来使用RunLoop:
1 | do { |
上面代码是用来满足RAC单元测试时的需求:短暂的运行起主线程,以接收通过主线程派发的RACSignal。这种在do-while循环中设置一个超时时间来运行RunLoop的方式值得参考,在现在的ARC下,它其实起到了定时释放旧池(自动释放池)并创建新池的作用(每一次循环,ARC会自动为其添加自动释放池的创建与释放)。
Texture
Texture原名AsyncDisplayKit,是一个界面性能优化框架,相较于UIKit把界面元素的创建、绘制、渲染、销毁等任务都在主线程中顺序完成,它把界面显示的相关任务进行了拆分,再把可以放到子线程的任务并发的执行,把可以推迟的任务延后执行,把可以合并的任务合并显示,以此来保证界面的流畅性。
Texture中也有对RunLoop的相关应用,相关逻辑如图所示:
Texture把界面元素中必须在主线程完成的部分任务,先封装到一个全局容器中,然后在主线程RunLoop中注册了一个Observer,观察的事件和CoreAnimation的Observer观察的事件相同:kCFRunLoopBeforeWaiting和kCFRunLoopBeforeExit,但回调优先级低于CoreAnimation的回调,这样在RunLoop每次休眠前或退出时,在CoreAnimation的回调处理完成后,Texture再执行自己全局容器中提交的任务,此时这些必须在主线程完成的任务需要依赖的任务,便可能已经异步、并发的完成,此处Texture也完成了将这些异步、并发的操作同步到主线程中去。