iOS 之 Runloop

runloop 是一个圆环, 应用在启动时即开启一个 runloop, runloop 控制着整个 App 的生命周期.
用户点击屏幕时, 设备传递信号给 App 的 runloop, runloop 即将被唤醒 -> runloop 被唤醒 -> 开始检查事件 -> 将事件添加到队列中 -> 运行事件 -> 移除事件 -> runloop 即将休眠 -> App 进入休眠状态.
联想: 图形的显示极大地依赖于 runloop, 其在 runloop 中的即将休眠 beforeWaiting 和退出 exit 两个状态中注册了检查方法, 这个检查方法在 runloop 进入到这两个状态时会遍历所有需要更新的视图 (需要更新的图形标记 setNeedDisplay), 然后对需要更新的图形执行图形绘制的相关方法 display
定义
Runloop 是通过内部维护的事件循环来对时间消息进行管理的一个 对象
runloop 的作用
- 保证程序不会退出
- 监听事件, 自动让程序进入睡眠状态, 只在有需要的时候唤醒 (节省 CPU 开销)
- 并保证在用户交互时传递事件, 以及监听一些计时器事件
线程与 runloop 的关系

- 线程与
runloop并不是包含的关系, 而是对应的关系. - 而且每个线程只能有一个对应的
runloop. - 线程被创建出来后如果不创建不调用
runloop的话其是没有对应的runloop的; 如果你需要更多的线程交互的话可以手动调用runloop(调用时即被创建了出来), 否则的话如果只是想执行一个长时间的已确定的任务则不需要创建 runloop - 主线程在创建时默认已经被创建了一个
mainRunLoop - 线程被销毁时, 对应的
runloop也会跟着销毁 RunLoop并不保证线程安全. 我们只能在当前线程内部操作当前线程的RunLoop对象, 而不能在当前线程内部去操作其他线程的RunLoop对象方法.
Runloop 与 CFRunloop 的关系

iOS 提供了两个 api 来管理 runloop
Runloop(在 objc 中也叫NSRunloop)CFRunloop(在 objc 中也叫CFRunLoopRef)
先说结论: Runloop 是 CFRunloop 的部分封装 (不是全部封装, 有些操作还是只能通过 CFRunloop 来执行)
print(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())) // __C.CFRunLoopMode(rawValue: kCFRunLoopDefaultMode)
print(RunLoop.current.currentMode) // __C.NSRunLoopMode(_rawValue: kCFRunLoopDefaultMode)刚开始感觉很混乱, 有了一个为什么还要提供第二个, 看了下实现的方法发现, 封装是为了更好用, 更易用. 比如:
- 在默认的
CFRunloop中是没有普通模式, 滑动模式的区分的, 只有default与common区分, 但是Runloop中设置了tracking模式, 用于滑动时使用.
有些功能在 Runloop 中不能完成, 需要借助 CFRunloop, 比如:
- 将自定义
mode添加到commonMoode(Runloop中的common模式只是对CFRunloop中common模式的调用, 目前还没有在Runloop中实现添加mode的方法)
一般情况下, 均使用 Runloop 类别, 因为其 api 也更简单易读, 是经过优化封装的部分 CFRunloop, 只有在 Runloop 不能实现相关功能时才使用 CFRunloop
runloop 涉及的类别
因为 Runloop 只是对 CFRunloop 的部分封装, 因此这里按照 CFRunloop 列出相应类别
CFRunLoop: 代表RunLoop的对象CFRunLoopMode: 代表RunLoop的运行模式CFRunLoopTimer: 就是RunLoop模型图中提到的定时源CFRunLoopSource: 就是RunLoop模型图中提到的输入源 / 事件源CFRunLoopObserver: 观察者, 能够监听RunLoop的状态改变

一个 runloop 可以被设置为不同的 mode (同一时间只能使用一个, 如果想改变只能退出, 然后以新模式进入), 不同的模式下有各自的事件, 观察器, 计时器.
CFRunLoop
可以通过如下方式获得 / 创建当前 runloop
Runloop.current: 返回RunloopRunloop.current.getCFRunloop: 返回CFRunloopCFRunLoopGetCurrent(): 返回CFRunloop
CFRunLoopMode
所有的 runloop 都有有五种模式, 同一时间只能使用一种模式. 如果要切换运行模式只能退出当前 runloop, 然后再重新指定一个运行模式进入.
defaultMode默认的
mode, 主线程在此mode下运行UITrackingRunLoopMode界面跟踪
mode, 用于scrollView追踪触摸滑动, 保证界面滑动时不受其他mode影响.commonMode一种占位用的 mode, 不是一种真正的
mode, 而是一种模式组合. 一个操作Common标记的字符串.一个
Mode可以将自己标记为Common属性 (通过将其ModeName添加到RunLoop的commonModes中). 每当 RunLoop 的内容发生变化时, RunLoop 都会自动将添加到commomMode里的Source/Observer/Timer同步到具有Common标记的所有Mode里.commonMode中默认已经添加了除UIInitializationRunloopMode以外的所有 mode(UITrackingRunLoopMode,kCFRunLoopDefaultMode,GSEventReceiveRunLoopMode,kCFRunLoopCommonModes), 可以创建自定义mode然后添加到commonModeswiftlet customMode = CFRunLoopMode.init("custom" as CFString) // 创建一个新的自定义 mode CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), customMode) // 将自定义 mode "custom" 添加到 common 中 let modes = CFRunLoopCopyAllModes(CFRunLoopGetCurrent()) // 取得当前 runloop 下的所有 mode print(modes) // <__NSArrayM 0x6000026d9b60>(UITrackingRunLoopMode, GSEventReceiveRunLoopMode, kCFRunLoopDefaultMode, kCFRunLoopCommonModes, custom) let timer = Timer(timeInterval: 2, target: self, selector: ##selector(logCurrentRunloopMode), userInfo: nil, repeats: true) // 将 timer 添加到 commom 中, commonModes 中的所有 mode 都会执行此 timer; 比如现在添加打印 mode 方法不仅在正常模式下会打印 // 在滑动模式下也可以正常打印 RunLoop.current.add(timer, forMode: .common) @objc func logCurrentloopMode() { // 不间断打印当前 mode Log(Runloop.current.currentMode) }例如可以将一个持续事件添加到主线程的
commomModeItems中, 那么这个持续事件在两种模式defaultMode与UITrackingRunLoopMode下都会无缝地连续运行UIInitializationRunLoopMode在刚启动 App 时第进入的第一个
Mode, 启动完成后就不再使用GSEventReceiveRunLoopMode接受系统事件的内部
Mode, 通常用不到
CFRunLoopTimer
基于时间的触发器, 基本上就是 NSTimer. 如果要在 runloop 的某个模式下添加定时器, 可以用如下方法.
let timer = Timer(timeInterval: 2,
target: self,
selector: #selector(logCurrentRunloopMode),
userInfo: nil,
repeats: true)
RunLoop.current.add(timer, forMode: .common) // 将 timer 添加到 commom 中, commonModes 中的所有 mode 都会执行此 timer
@objc func logCurrentloopMode() { // 不间断打印当前 mode
Log(Runloop.current.currentMode)
}Timer 类有一个 scheduledTimer 方法, 这个方法就是将计时器加入到 runloop 的 default 模式下, 因此如果想在 default 模式下添加计时器的话此方法与上述方法等效, 而且还更为简便.
func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> TimerCFRunLoopSource
按照函数调用栈来分来一共有两种 source
source0: 非基于mach_port的处理事件, 什么叫非基于mach_port的呢? 就是说你这个消息不是其他进程或者内核直接发送给你的(二手的!).source1: 基于mach_port的, 来自系统内核或者其他进程或线程的事件, 可以主动唤醒休眠中的 RunLoop(iOS 里进程间通信开发过程中我们一般不主动使用).mach_port大家就理解成进程间相互发送消息的一种机制就好.
总结来说: source1 基本就是系统事件, source0 基本就是应用层事件
简单举个例子: 一个 APP 在前台静止着, 此时, 用户用手指点击了一下 APP 界面, 那么过程就是下面这样的:
我们触摸屏幕, 先摸到硬件 (屏幕), 屏幕表面的事件会先包装成 Event, Event 先告诉 source1(mach_port), source1 唤醒 RunLoop, 然后将事件 Event 分发给 source0, 然后由 source0 来处理.
CFRunloopObserver
观察者, 用于监听 runloop 的状态 (也仅用于观察状态了). 状态有以下几种:
entry: 进入runloop后 -1beforeTimers: 即将处理Timers事件 -2beforeSources: 即将处理source事件 -4 (runloop会连续发送这两个通知给observer然后再开始依次执行timer和source)beforeWaiting: 即将进入休眠状态 -32afterWaiting: 被唤醒后 -64exit: 退出了runloop-128allActivities: 监听全部状态变化
使用如下方式添加观察者
let block = { (ob: CFRunLoopObserver?, ac: CFRunLoopActivity) in
if ac ==.entry {
NSLog("进入 Runlopp")
} else if ac == .beforeTimers {
NSLog("即将处理 Timer 事件")
} else if ac == .beforeSources {
NSLog("即将处理 Source 事件")
} else if ac == .beforeWaiting {
NSLog("Runloop 即将休眠")
} else if ac == .afterWaiting {
NSLog("Runloop 被唤醒")
} else if ac == .exit {
NSLog("退出 Runloop")
}
}
let ob = try createRunloopObserver(block: block)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), ob, .defaultMode)也可以使用
CFRunLoopObserverCreateWithHandler(_ allocator: CFAllocator!,
_ activities: CFOptionFlags,
_ repeats: Bool,
_ order: CFIndex,
_ block: ((CFRunLoopObserver?, CFRunLoopActivity) -> Void)!) -> CFRunLoopObserver!runloop 生命环

- 通知观察者
RunLoop已经启动 - 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源 source0
- 启动任何准备好的非基于端口的源 source0
- 如果基于端口的源准备好并处于等待状态, 立即启动; 并进入步骤 9
- 通知观察者线程进入休眠状态
- 将线程置于休眠知道任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
RunLoop设置的时间已经超时RunLoop被显示唤醒
- 通知观察者线程将被唤醒
- 处理未处理的事件
- 如果用户定义的定时器启动, 处理定时器事件并重启
RunLoop. 进入步骤 2 - 如果输入源启动, 传递相应的消息
- 如果
RunLoop被显示唤醒而且时间还没超时, 重启RunLoop. 进入步骤 2
- 如果用户定义的定时器启动, 处理定时器事件并重启
- 通知观察者
RunLoop结束.
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
/// 5. GCD处理main block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}Ref
本博客文章采用 CC 4.0 协议,转载需注明出处和作者。
