底层初窥——Runloop
Runloop 介绍
关于 Runloop 的文章在网上一搜一大片,这里也就不费心去写一篇了,这里推荐一篇写的比较全面的,有兴趣的可以去看。
这篇文章,主要就像记录一些关于 Runloop 的面试题目,在解题的过程来加深对 Runloop 的了解。
Runloop 相关问题
Runloop 对于一个标准的 iOS 开发来说都不陌生,下面就随便列几个典型问题吧.
app如何接收到触摸事件的?
- 系统响应阶段
手指触碰屏幕,屏幕感应到触碰后,将事件交由 IOKit 处理。
IOKit 将触摸事件封装成一个 IOHIDEvent 对象,并通过 mach port 传递给 SpringBoad 进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系> 统接收到的触摸事件。
SpringBoard 进程因接收到触摸事件,触发了主线程 runloop 的 source1 事件源的回调。
- 此时 SpringBoard 会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无 APP 运行),则触发 SpringBoard 本身主线程 runloop 的 source0 事件源的回调,将事件交由桌面系统去消耗;若是后者(即有 App 正在前台运行),则将触摸事件通过 IPC 传递给前台 APP 进程,接下来的事情便是 APP 内部对于触摸事件的响应了。
- App 响应阶段
- APP 进程的 mach port 接受到 SpringBoard 进程传递来的触摸事件,主线程的 runloop 被唤醒,触发了 source1 回调。
- source1 回调又触发了一个 source0 回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象,此时 APP 将正式开始对于触摸事件的响应。
- source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication 开始一个寻找最佳响应者的过程,这个过程又称 hit-testing。另外,此处开始便是与我们平时开发相关的工作了。
- 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。事实上,事件除了被响应者消耗,还能被手势识别器或是 target-action 模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级。
- 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么最后也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop 若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
步骤3,就是寻找最佳响应者。
步骤4,就是寻找事件响应者。
为什么只有主线程的runloop是开启的?
- 目的:
保持程序持续运行,如果没有 Runloop。 在运行,当主线程执行完当前任务后,线程自动销毁了。
- 原因:
我们的 iOS 程序能保持持续运行的原因就是在 main() 函数中调用了 UIApplicationMain 函数,这个函数内部会启动主线程的 RunLoop。
CFRunLoopRun
1 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
在 CFRunLoopGetCurrent() 方法中:
- RunLoop和线程的一一对应的,对应的方式是以 key-value 的方式保存在一个全局字典中。
- 主线程的 RunLoop 会在初始化全局字典时创建。
PerformSelector和 Runloop 的关系?
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
Runloop 和 AutoreleasePool?
AutoreleasePool 是另一个与 RunLoop 相关讨论较多的话题。其实 从RunLoop 源代码分析,AutoreleasePool 与 RunLoop 并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在 iOS 应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。可以应用程序刚刚启动时打印 currentRunLoop 可以看到系统默认注册了很多个 Observer,其中有两个 Observer 的 callout 都是 ** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。
- 第一个 Observer 会监听 RunLoop 的进入:
- 会回调 objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。
- 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态
- 在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用 objc_autoreleasePoolPop() 释放自动自动释放池内对象。
实现一个常驻线程?
- 在子线程中启动 Runloop,注意添加的那个空 port。并使用 runMode:beforeDate: 启动。
1 | - (void)run { |
- 退出时,使用 CFRunLoopStop()
1 | - (void)stopThread { |
使用 performSelector:withObject:afterDelay 验证 Runloop 是否停止。
我们知道 performSelector:withObject:afterDelay 依赖于线程的 runloop,因为它本质上是由一个定时器负责定期加入到 runloop 中执行。所以如果这个方法可以成功执行,说明当前线程的 runloop 已经开启,否则则说明没有启动。