Runloop循环是线程基础的一部分,它维持了一个事件处理循环,用于计划工作和协调传入的事件,它的目标就是为了让线程有序高效的运行,在有工作的时候要保持忙碌状态,没有工作的时候让线程保持休眠状态。
每个线程(包括应用程序的主线程)都有一个关联的运行循环对象,在App启动的时候,系统会默认的创建一个主线程的RunLoop
循环,用于处理主线程的任务,以及接收用户事件。子线程则需要显式地运行其运行循环。
RunLoop实现
-
RunLoop
它是用于处理线程活动状态,响应当前线程传入事件的循环,它是线程的一部分,本质上就是一个doWhile循环, 它处理了2种不同的事件,一种是输入源传递的异步事件,通常来自另外一个线程或者是其他应用程序的消息; 另外一种时候计时器提供的在预定事件重复或间隔发送的同步事件。
这里需要重点区分soure0和soure1,以及runLoop各种事件之间的关系
RunLoop Modes
- 它是用来的指定
Source
和Timer
的运行模式,也是定义RunLoop
观察着的模式,将不同类型的事件标记为不同的Mode
,每个mode有着各自的优先级,方便CPU进行调度和管理,合理的利用CPU的时间片去处理各自的任务。所以Mode是用来影响任务执行的时间片
Mode | Name | Description |
Default | kCFRunLoopDefaultMode/NSDefaultRunLoopMode | 默认的运行模式 |
Connection | NSConnectionReplyMode | NSConnection监听回调时间所用的模式 |
Modal | NSModalPanelRunLoopMode | 识别模态面板时所指定的模式 |
Event tracking | NSEventTrackingRunLoopMode | 鼠标拖动,手势跟踪时所用的模式 |
Common modes | NSRunLoopCommonModes/kCFRunLoopCommonModes | 共同模式,共同处理多个mode的模式 |
Source0和Source1的区别
从代码上来看source1的context中比source0多了一个port
在官方文档中https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1也有这样一句话印证了这一点
Port-based sources are signaled automatically by the kernel, and custom sources must be signaled manually from another thread.
NSTimer
基于runLoop运行,它的底层是由XNU 内核的 mk_timer来驱动的,NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件,RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性
- CADisplayLink
优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
缺点容易不小心造成循环引用。selector循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息 - dispatch_source_t
基于GCD,精度高,不依赖runloop,简单好使,最喜欢的计时器
需要注意的点是使用的时候必须持有计时器,不然就会提前释放。
Run Loop Observers
- 监听
RunLoop
的生命周期,
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
- 对应6种状态的监听
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
- 监听的流程也是RunLopp的运行流程
- 通知观察者运行循环已经进入
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
- 通知观察者事件触发器已经就绪
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
- 通知观观察者非端口事件已经就绪
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
- 执行非端口且准备完成的事件源,(block事件)
__CFRunLoopDoBlocks(rl, rlm);
- 如果一个基于口的输入源以及准备完毕并且等待执行,跳转到第9步
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
- 通知观察者
RunLoop
将要开始休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting)
,__CFRunLoopSetSleeping(rl)
- 保持线程一直处于睡眠状态,知道接收到如下事件
- 一个机基于端口的事件
__CFRunLoopServiceMachPort
- timer事件触发
if (livePort == rl->_wakeUpPort)
- 为运行循环设置的超时值过期
- RunLoop显示的被唤醒
- 通知观察者线程被唤醒
- 处理pending的事件
- 处理timer事件
- 处理input source事件
- 通知观察者RunLoop已经退出运行循环
RunLoop与GCD的关系
- GCD是Grand Center Dispatch,由系统底层API调用,精度更高,在RunLoop的实现中,才用了GCD来监听RunLoop是否超时
- 执行GCD派发的block任务时,如
dispatch_async(dispatch_get_main_queue(), block)
会向主线成的RunLoop
发送信息,并在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
执行具体的block事件 - 其他线程如果有设置
RunLoop
也是如此.主要是看GCD的block派发到哪个线程,子线程默认不会开启RunLoop
RunLoop与内存关系
- 通过在RunLoop开启和结束注册2个不同Observers来实现的RunLoop创建和结束监听
- 监听到开始事件时通过
_objc_autoreleasePoolPush
创建自动释放池, - 监听到结束事件时通过
_objc_autoreleasePoolPop
销毁释放池
RunLoop与事件响应
- 系统注册了一个Source1的输入源,用来接收系统事件,其回调函数是
__IOHIDEventSystemClientQueueCallback
,当接收到来自硬件的事件时,首先由IOKit.framework
生成一个IOHIDEvent
事件,并交由SpringBoard
接收, - SpringBoard接收按键(锁屏/静音等),触摸,加速,接近传感器等事件,通过mach port 转发给需要的App进程。
- 进而触发
Source1
事件的回调,通过_UIApplicationHandleEventQueue
将事件派发到应用程序的事件对类上,UIApplication接收到事件后再往下传递 -
_UIApplicationHandleEventQueue
会将IOHIDEvent
包装成UIEvent
进行分发。
利用RunLoop保持线程常驻
- 例如在AFNetworking中开启了一个常驻线程用来处理网络请求事件,避免了线程的频繁创建销毁所带来的开销
- 在对应的线程开启一个RunLoop,设置MatchPort作为它的输入源,然后开启一个运行循环
- addPort:forMode: 在RunLoop指定模式下添加一个输入源
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//通常情况其它线程访问这个pot就能给这个线程的RunLoop发送消息,此处只是为了让那个RunLoop有事件源被监听,避免退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
- 通过dispatch once token,原子执行,安全的生成一个线程,下面的例子开启了一个信息的线程
AFNetworking
(在这个线程内部的bloc块中已经设置了它的名字)
+ (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卡顿监测方案
第一种方案简单粗暴,才用子线程去ping主线程,如果超时没有回应则说明卡住
第二种方案,捕获machPort消息发送前后的观察事件,如果卡住,则runLoop长时间应该是处于挂起状态的
kCFRunLoopBeforeSources, // Source0 回调
kCFRunLoopBeforeWaiting, // 等待 mach_port
kCFRunLoopAfterWaiting, // 接收 mach_port