Runloop干了什么
RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方。
下图描述了runloop将一个触摸事件从操作系统层传送到应用内的main runloop中的简单过程
Runloop处理哪些事件
- Observer事件,runloop中状态变化时进行通知。(微信卡顿监控就是利用这个事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程)。这里还需要特别注意,CAAnimation是由RunloopObserver触发回调来重绘,接下来会讲到。
- Block事件,非延迟的NSObject PerformSelector立即调用,dispatch_after立即调用,block回调。
- Main_Dispatch_Queue事件:GCD中dispatch到main queue的block会被dispatch到main loop执行。
- Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。
- Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。
- Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)。
为什么操作UI只能在主线程中?
因为UIKit不是线程安全的。试想下面这几种情况:
- 两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。
- 两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑树上的背景颜色属性为B。
- 两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。
iOS4之后苹果将大部分绘图的方法和诸如 UIColor 和 UIFont 这样的类改写为了线程安全可用,但是仍然强烈建议讲UI操作保证在主线程中执行。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
系统的哪些操作用到了Runloop
- 页面渲染
- NSTimer
- CADisplayLink
Runloop解释页面渲染
当我们调用 [UIView setNeedsDisplay]
时,这时会调用当前 View.layer
的 [view.layer setNeedsDisplay]
方法。
这等于给当前的 layer 打上了一个脏标记,而此时并没有直接进行绘制工作。而是会到当前的 Runloop 即将休眠,也就是 beforeWaiting 时才会进行绘制工作。
紧接着会调用 [CALayer display]
,进入到真正绘制的工作。CALayer 层会判断自己的 delegate 有没有实现异步绘制的代理方法 displayer:
,这个代理方法是异步绘制的入口,如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束。
CALayer 内部会创建一个 Backing Store,用来获取图形上下文。接下来会判断这个 layer 是否有 delegate。
如果有的话,会调用 [layer.delegate drawLayer:inContext:],并且会返回给我们 [UIView DrawRect:]
的回调,让我们在系统绘制的基础之上再做一些事情。
如果没有 delegate,那么会调用 [CALayer drawInContext:]
。
以上两个分支,最终 CALayer 都会将位图提交到 Backing Store,最后提交给 GPU。
至此绘制的过程结束。
当UI被添加到界面后,我们改变Frame,或更新
UIView
/CALayer
层次,或调用setNeedsLayout
/setNeedsDisplay
方法,均会添加重新绘制任务。这个时候系统会注册一个Observer
监听BeforeWaiting
(即将进入休眠)和Exit
(即将退出Loop)事件,并回调执行当前绘制任务(setNeedsDisplay
->display
->displayLayer
),最终更新界面