1、先来一张图看下runloop 的运行流程:
2、具体描述:RunLoop内部的基本流程
(1)通知观察者RunLoop启动
(2)通知观察者即将处理Timer
(3)通知观察者即将处理Source0
(4)触发Source0回调
(5)如果有Source1(基于port)处于ready状态,直接处理该Source1然后跳转到 第(9)步去处理消息
(6)如果没有待处理消息,则通知观察者RunLoop所在线程即将进入休眠。
(7)休眠前,RunLoop会添加一个dispatchPort,底层调用mach_ msg接收mach_ port的消息。线程进入休眠,直到下面某个事件触发唤醒线程:
- 基于port的Source1事件到达
- Timer时间到达
- RunLoop启动时设置的最大超时时间到了
- 手动唤醒
(8)唤醒后,将休眠前添加的dispatchPort移除,并通知观察者RunLoop已经被唤醒
(9)通过handle_ msg处理消息
(10)如果消息是Timer类型,则触发该Timer的回调
(11)如果消息是dispatch到main_ queue的block,执行block
(12)如果消息是Source1类型,则处理Source1回调
(13)以下条件中满足时候退出循环,否则从(2)继续循环
- 事件处理完毕而且启动RunLoop的时候参数设置为一次性执行
- 启动RunLoop时设置的最大运行时间到期
- RunLoop被外部调用强行停止
- 启动RunLoop的mode items为空
(14)上一步退出循环后退出RunLoop,通知观察者RunLoop退出
3、重点强调:
Runloop被唤醒后,一定是:先处理唤醒它的事件,处理完后再检查是否满足退出的条件,满足则退出Runloop,不满足才重新回到步骤(2)去循环。
可以断点验证:唤醒后先处理唤醒runloop的时间,之后才会走到步骤2的结论。
4、Runloop 的源
Source0 :只包含一个函数指针(回调方法),不能自动触发,只能手动触发,触发方式是先通过CFRunLoopSourceSignal(source)将这个Source标记为待处理,然后再调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop处理这个事件。
Source1 :基于port的Source源,包含一个port和一个函数指针(回调方法)。该Source源可通过内核和其他线程相互发送消息,而且可以主动唤醒RunLoop。
问题:
UI刷新是划分为Source1事件,而事件响应属于source0。笔者暂时这样理解:因为UI刷新是主动进行的,不需要外部手动触发。而事件响应(例如:touch事件的触发)等需要外部手动触发。鉴于是否可以手动触发以及来自消息来内核等知识点,故将UI刷新划分为Source1事件,而事件响应划分为source0。
如果理解有误,或者有更具说服力的理解,请及时联系笔者。
解决:笔者通过查阅core animation相关文档和资料,找到以下合理依据:
RunLoop任务分发
iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。