RunLoop普遍存在于各个系统中, 是比较底层、基础同时又很重要的一个机制.
所以对RunLoop的学习也是对技术的提高, 也能进一步帮助我们理解用户操作、界面刷新等.
RunLoop的基本概念
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出.
RunLoop就是一个机制,让线程能随时处理事件但并不退出.
用伪代码来写就是:
func runloop() {
init();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
类似于这样, 永远处于一个循环中, 直到收到了quit
的消息.
这样的循环, 称为Event Loop. Event Loop存在于很多操作系统中或者框架中.
关键点在于: 如何管理事件/消息, 如何让线程在没有处理消息时休眠(wait)以避免资源占用、在有消息到来时立刻被唤醒.
所以, RunLoop 实际上就是一个对象, 这个对象管理了其需要处理的事件和消息, 并提供了一个入口函数来执行上面 Event Loop 的逻辑. 线程执行了这个函数后, 就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中, 直到这个循环结束(比如传入 quit 的消息), 函数返回.
OSX/iOS 系统中, 提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef. CFRunLoopRef 是在 CoreFoundation 框架内的, 它提供了纯 C 函数的 API, 所有这些 API 都是线程安全的. NSRunLoop 是对 CFRunLoopRef 的封装, 提供了面向对象的 API, 但是这些 API 不是线程安全的.
RunLoop与线程的关系
线程和 RunLoop 之间是一一对应的, 其关系是保存在一个全局的 Dictionary 里.线程刚创建时并没有 RunLoop, 如果你不主动获取, 那它一直都不会有. RunLoop 的创建是发生在第一次获取时, RunLoop 的销毁是发生在线程结束时. 你只能在一个线程的内部获取其 RunLoop(主线程除外).
RunLoop对外的接口
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef 类并没有对外暴露, 只是通过 CFRunLoopRef 的接口进行了封装.
一个RunLoop包含的元素, 以及存储所用数据结构, 以及它们之间的关系, 如下图所示:
一个 RunLoop 包含若干个 Mode, 每个 Mode 又包含若干个 Source/Timer/Observer. 每次调用 RunLoop 的主函数时, 只能指定其中一个 Mode, 这个Mode被称作 CurrentMode. 如果需要切换 Mode, 只能退出 Loop, 再重新指定一个 Mode 进入. 这样做主要是为了分隔开不同组的 Source/Timer/Observer, 让其互不影响.
CFRunLoopSourceRef 是事件产生的地方. Source有两个版本: Source0 和 Source1.
- Source0 只包含了一个回调(函数指针), 它并不能主动触发事件. 使用时, 你需要先调用 CFRunLoopSourceSignal(source), 将这个 Source 标记为待处理, 然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop, 让其处理这个事件。
- Source1 包含了一个 mach_port 和一个回调(函数指针), 被用于通过内核和其他线程相互发送消息. 这种 Source 能主动唤醒 RunLoop 的线程.
CFRunLoopTimerRef 是基于时间的触发器. 它和 NSTimer 是toll-free brideged 的, 可以混用. 其中包含一个时间长度和一个回调(函数指针). 当其加入到RunLoop时, RunLoop会注册对应的时间点, 当到达那个时间点时, RunLoop就会被唤醒, 来执行那个回调.
有一些数据类型是能够在 Core Foundation Framework 和 Foundation Framework 之间交换使用的. 这意味着, 对于同一个数据类型, 你既可以将其作为参数传入 Core Foundation 函数, 也可以将其作为接收者对其发送 Objective-C 消息(即调用ObjC类方法), 这种在 Core Foundation 和 Foundation 之间交换使用数据类型的技术就叫 Toll-Free Bridging.
CFRunLoopObserverRef 是观察者, 每个 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
};
上面的 Source/Timer/Observer 被统称为 mode item, 一个 item 可以被同时加入多个mode. 但一个 item 被重复加入同一个 mode 时是不会有效果的. 如果一个 mode 中一个 item 都没有, 这个 RunLoop 就会直接退出, 不会进入到循环中.
RunLoop 的 Mode
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
commonModes
: 一个 Mode 可以将其标记为 common
(通过将其 ModeName 添加到 RunLoop 的 commonModes 中). 每当RunLoop 的内容发生变化时, RunLoop 都会自动将 _commonMOdeItems 里的 Source/Timer/Observer 同步到具有 common
标记的所有 Mode 里.
应用场景举例: 主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode. 这两个 Mode 都已经被标记为
Common
属性. DefaultMode 是 App 平时所处的状态, TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态, 当你创建一个 Timer 并加到 DefaultMode 时, Timer 会得到重复回调, 但此时滑动一个TableView时, RunLoop 会将 mode 切换为 TrackingRunLoopMode, 这时 Timer 就不会被回调, 并且也不会影响到滑动操作.
有时你需要一个 Timer, 在两个 Mode 中都能得到回调, 一种办法就是将这个 Timer 分别加入这两个 Mode. 还有一种方式, 就是将 Timer 加入到顶层的 RunLoop 的 commonModeItems 中. commonModeItems 被 RunLoop 自动更新到所有具有
Common
属性的 Mode 里去.
只能通过 mode name 来操作内部的 mode, 当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时, RunLoop会自动帮你创建对应的 CFRunLoopModeRef
. 对于一个 RunLoop 来说, 其内部的 mode 只能增加不能删除.
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
) 和 UITrackingRunLoopMode
, 可以用这两个 Mode Name 来操作其对应的 Mode.
同时苹果还提供了一个操作 Common 标记的字符串: kCFRunLoopCommonModes
(NSRunLoopCommonModes
), 你可以用这个字符串来操作 Common Items, 或标记一个 Mode 为 "Common". 使用时注意区分这个字符串和其他 mode name.