runloop事件、UI更新、observer与coranimation
一、触摸事件派发与视图绘制打包
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__dispatchPreprocessedEventFromEventQueue
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
CA::Transaction::commit()
CA::Context::commit_transaction(CA::Transaction*)
二、UI更新
如果打印App启动之后的主线程RunLoop可以发现另外一个callout为**_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv**的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。
三、runloop6类事件与调度策略
简单的说,RunLoop是事件驱动的一个大循环,如下代码所示
int main(int argc, char * argv[]) {
//程序一直运行状态
while (AppIsRunning) {
//睡眠状态,等待唤醒事件
id whoWakesMe = SleepForWakingU p();
//得到唤醒事件
id event = GetEvent(whoWakesMe);
//开始处理事件
HandleEvent(event);
}
return 0;
}
RunLoop主要处理以下6类事件:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
- 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也是这里触发)。
RunLoop执行顺序的伪代码
SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
//通知即将进入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do {
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(); //一个循环中会调用两次,确保非延迟的NSObject PerformSelector调用和非延迟的dispatch_after调用在当前runloop执行。还有回调block
__CFRunLoopDoSource0(); //例如UIKit处理的UIEvent事件
CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap,陷入内核等待匹配的内核mach_msg事件
// Zzz...
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// Handle msgs
if (wakeUpPort == timerPort) {
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
//GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
__CFRunLoopDoSource1(); //CADisplayLink是source1的mach_msg触发?
}
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
//通知observers,即将退出runloop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);
结合上面的Runloop事件执行顺序,思考下面代码逻辑中为什么可以标识tableview是否reload完成
dispatch_async(dispatch_get_main_queue(), ^{
_isReloadDone = NO;
[tableView reload]; //会自动设置tableView layoutIfNeeded为YES,意味着将会在runloop结束时重绘table
dispatch_async(dispatch_get_main_queue(),^{
_isReloadDone = YES;
});
});
提示:这里在GCD dispatch main queue中插入了两个任务,一次RunLoop有两个机会执行GCD dispatch main queue中的任务,分别在休眠前和被唤醒后。
iOS 为什么必须在主线程中操作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 事件都是在这个回调中完成的。
CALayer
在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来,UIView可以处理触摸事件,可以支持基于Core Graphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。
CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。CALayer并不清楚具体的响应链。
UIView和CALayer是一个平行的层级关系,每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。实际上这些背后关联的Layer图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。
UIView 的 Layer 在系统内部,被维护着三份同样的树形数据结构,分别是:
- 图层树(这里是代码可以操纵的,设置属性的最终值会立刻在这里更新);
- 呈现树(是一个中间层,系统就在这一层上更改属性,进行各种渲染操作。比如一个动画是更改alpha值从0到1,那么在逻辑树上此属性会被立刻更新为最终属性1,而在动画树上会根据设置的动画时间从0逐步变化到1);
- 渲染树(其属性值就是当前正被显示在屏幕上的属性值);
http://www.cnblogs.com/Twisted-Fate/p/4892192.html
四、几个主要的observer
NSRunLoop 三个主要的observer:
1、autoreleasepool;
2、手势识别;
3、动画;
observers = (
"<CFRunLoopObserver 0x600000e145a0 [0x10389db68]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10648a1b1), context = <CFArray 0x600003158a20 [0x10389db68]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa317002058>\n)}}",
"<CFRunLoopObserver 0x600000e10140 [0x10389db68]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10605c473), context = <CFRunLoopObserver context 0x6000014102a0>}",
"<CFRunLoopObserver 0x600000e14460 [0x10389db68]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x1064b9dfc), context = <CFRunLoopObserver context 0x7fa315502e10>}",
"<CFRunLoopObserver 0x600000e14b40 [0x10389db68]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x107eb86ae), context = <CFRunLoopObserver context 0x0>}",
"<CFRunLoopObserver 0x600000e143c0 [0x10389db68]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x1064b9e75), context = <CFRunLoopObserver context 0x7fa315502e10>}",
"<CFRunLoopObserver 0x600000e146e0 [0x10389db68]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10648a1b1), context = <CFArray 0x600003158a20 [0x10389db68]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa317002058>\n)}}"
),