RunLoop 是 iOS 比较的核心之一,本篇用于梳理 RunLoop 相关的概念和底层实现。
什么是 RunLoop?
对于一个线程,一般情况下一次只执行一个任务,如果任务执行完成线程也会立刻退出。而对于一个 App,我们希望的是,即使 App 内没有任何的任务需要线程去处理了,但不能够让这些线程退出。即不能退出应用。这时就需要一种机制既能让线程能随时处理事件,且在处理完成后不退出。比如主线程,在我们启动应用之后就会一直存在,当有交互发生时,对应去处理相关的事件。RunLoop 就很好了满足了这个需求,它能够在没有收到消息的时候让线程休眠以避免资源浪费,当有消息来时会立刻唤醒线程处理。
这也是我们需要 RunLoop 的原因。
RunLoop 与线程之间的关系
苹果不允许直接创建 RunLoop,它提供了两个自动获取的函数:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。CFRunLoop 是基于 pthread 来管理的。
// 全局的 Dictionary, |
可以看到,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。第一次进入时,初始化全局 Dictionary,并先为主线程创建一个 RunLoop,将关系保存在 Dictionary 中。线程结束时销毁 RunLoop。我们只能在一个线程的内部获取其 RunLoop(主线程除外)。
NSRunLoop 和 CFRunLoopRef 的关系
NSRunLoop 是基于 CFRunLoopFef 封装。CGRunLoopRef 属于 CoreFoundation 框架,提供的是纯 c 函数的 API,这些 API 是线程安全的。NSRunLoop 提供的是面向对象的 API,这些 API 不是线程安全的。
RunLoop 的内部实现逻辑
RunLoop 的内部实现逻辑大致如下:
内部实现逻辑的代码如下:
// 用 DefaultMode 启动 |
RunLoop 的 Mode
开发过程中常遇到的一个场景是,在我们写 NSTimer 时为了避免滑动事件影响 NSTimer 的回调,我们通常会把 mode 置为 NSRunLoopCommonModes
。
这里的 NSRunLoopCommonModes
是什么?
先看下 CFRunLoopMode
和 CFRunLoop
的结构:
struct __CFRunLoopMode { |
主线程的 RunLoop 里有两种 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。这两种 Mode 都被标记为Common
属性。App 平常所处的 mode 是 kCFRunLoopDefaultMode
;当有滑动事件触发时,RunLoop 会切换 mode 到 UITrackingRunLoopMode
。
前面提到创建一个 Timer,默认会被加到 DefaultMode,正常情况下 Timer 会得到重复回调。但如果此时滑动一个 ScrollView,RunLoop 就会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被正常的回调。当然,此时 Timer 也不会影响到滑动操作。
到这里,我们大概知道了 RunLoop 系统预置的 model 有这几种:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认模式
- UITrackingRunLoopMode:滑动模式
- NSRunLoopCommonModes:通用模式,无论处于滑动或默认状态都能够响应实践
CommonModes
一个 Mode 可以通过将其 ModeName 添加到 RunLoop 的 commonModes
中,来将自己设置为 CommonMode
。每当 RunLoop 的内容发生变化时,RunLoop 都会将事件同步到 CommonMode 的 mode item,什么是 mode item 后文会讲到。这也解释了为什么 timer 加入到 NSRunLoopCommonModes 中会被正确的回调。
在 NSRunLoop 这一层没有提供操作 Mode 的接口,在 CFRunLoopRef 对外提供了两个操作 Mode 的接口,只有增加 mode 的接口:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); |
mode item
struct __CFRunLoopMode { |
在 __CFRunLoopMode
结构体中的 _sources0
、_sources1
、_observers
、_timers
都属于 mode item。结构图如下:
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
Timer 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
Observer 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
一个 mode item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是无效的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
RunLoop 启动和退出
启动
这边以 NSRunLoop 为例,CFRunLoopRef 类似, 启动有 3 个方法:
- run
- runUntilDate:
- runMode:beforeDate:
run 底层是不断(循环)调用 runMode:beforeDate:
来达到运行目的。runUntilDate:
底层也是调用 runMode:beforeDate:
来运行,和 run 不同的是,在指定的时间也就是 UntilDate 参数到后会停止调用。
退出
在系统提供的停止 RunLoop 方法只有 CFRunLoopStop()
,CFRunLoopStop()
方法只会结束当前的 RunLoop 调用,而不会结束后续的调用。也就意味着 如果你是用方法一也就是 run 的方式启动 RunLoop,那么这个 RunLoop 不会被退出,因为它会不断的启动,因为 run 底层是不断(循环)调用 runMode:beforeDate:
来达到运行目的。如果你是使用 runUntilDate:
启动的,那么超时结束后会自动终止 RunLoop,如果是 runMode:beforeDate:
那么你可以精确的控制 RunLoop 的停止。
RunLoop 的应用
RunLoop 用途广泛,如 AutoreleasePool,PerformSelecter,GCD,AsyncDisplayKit 等都有涉及到。
参考文章: