一.Runloop是什么
如果说RunTime是OC语言的动态特性的实现技术,那么Runloop则是iOS中线程内的事件调度模型。
Runloop的本质就是一个循环代码,只是这个循环会根据是事件驱动的。意味着在没有事件触发的时候,app是不会占用cpu资源的。由此来达到app内部的事件的有效调度。
在Runloop中接受两种事件源:Input Source
和Timer Source
。
其中 Input Source
又可以分为三类:
Port-Based Sources
,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到。Custom Input Sources
,用户手动创建的 Source。Cocoa Perform Selector Sources
,Cocoa 提供的performSelector系列方法,也是一种事件源。
Timer Source
顾名思义就是指定时器事件了(NSTimer)。
二.Runloop 与线程的关系
- Runloop和线程是绑定在一起的。
- 每个线程(包括主线程)都有一个对应的 Runloop 对象。
- 线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
- 我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的Runloop对象(系统帮我们为每个线程创建了Runloop,除了主线程以外默认是不运行的,所以在其他线程使用NSTimer就需要手动开启该线程的Runloop)。
/// 全局的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的执行单位:Runloop Mode
一个RunLoop以单个Mode为执行单位,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个Source/Timer/Observer
。每次调用 RunLoop 的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode
。
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的Source/Timer/Observer
,让其互不影响。
苹果文档中提到的 Mode 有五个,分别是:
- NSDefaultRunLoopMode
- NSConnectionReplyMode
- NSModalPanelRunLoopMode
- NSEventTrackingRunLoopMode
- NSRunLoopCommonModes
iOS 中公开暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。
系统提供的很多API是基于上诉的几种Mode来实现的,用户是无法实现一个完整的Mode(也没有必要)。使用事件调度模型的好处在于提高线程的利用率,我们要实现添加一个自定义事件到Runloop中有两种方法:自定义Input Source和使用performSelector系列API。
performSelector:OnThread
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
perform selector请求会在目标线程上序列化,减缓在单个线程上容易引起的同步问题。perform selector执行完后会自动清除出run loop。值得注意的是,由于它是运行在runloop之上的所以在runloop在未开始运行的时候是不会去执行的。
自定义Input Source
参考:Runloop文档
二.Runloop Mode的内部逻辑
实际上 RunLoop 内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,直到超时或被手动停止,该函数才会返回。
三.Runloop 的常见应用
NSTimer
NSTimer是一个使用Timer Source作为输入源来实现的定时器(底层基于使用mk_timer实现)。NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
我们日常使用的时候,通常就是加入到当前的 Runloop的default mode
中,而 ScrollView 在用户滑动时,主线程 RunLoop
会转到 UITrackingRunLoopMode
,也就是Runloop的Mode进行了切换,位于default mode
的定时器就不能工作了。
有如下两种解决方案:
- 第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于
NSRunLoopCommonModes
,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。 - 第二种: 另一种解决Timer的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新UI。
AutoreleasePool
AutoreleasePool是Objective-C中内存管理的一种方法。AutoreleasePool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。也就是AutoreleasePool所实现的内存自动释放是运行在Runloop之上的。
UI更新
App在启动的时候会向Runloop里面注册一个Observer专门用于监听UI变化的更新。比如修改了frame、调整了UI层(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
NSURLConnection
一旦NSURLConnection设置了delegate会立即创建一个线程com.apple.NSURLConnectionLoader,同时内部启动RunLoop并在NSDefaultMode
模式下添加4个Source0。其中CFHTTPCookieStorage
用于处理cookie ;CFMultiplexerSource
负责各种delegate回调并在回调中唤醒delegate内部的RunLoop(通常是主线程)来执行实际操作。