结合 RunLoop 和 Instrument 定位卡顿

iOS 应用,丝般顺滑的理想情况就是 60FPS (对于 iPad Pro 是 240FPS),即在 16ms 之内完成一次渲染。如果找到在每次渲染花费了多久,究竟做了什么事情,那么就可以进行针对性的优化。

RunLoop 的概念

在程序中,我们需要一种机制,可以让当前线程能够随时处理事件但不退出。这种模型通常被称为 Event Loop,在 iOS 中使用 RunLoop 来实现。
RunLoop 管理了所在线程需要处理的事件和消息。当有事情需要处理时,就唤醒当前线程,处理事件。当事情全部处理完毕时,线程处于休眠状态,以避免资源占用。这样子线程一直处于“接受消息->等待->处理”循环中。

根据苹果文档,RunLoop 的内部逻辑如上。RunLoop 有很多种状态,比如 beforeWaitingafterWaiting,当状态发生改变时,就会通知 observer。而触摸事件、定时器、网络请求返回等都是作为 Source0、Source1、Timer 被加到需要处理的队列中,都可以唤醒当前线程的 RunLoop。

RunLoop 与界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

即界面更新一定发生在主线程的 RunLoop BeforeWaiting 状态发生时。那么我们可以得出一个结论,如果主线程的 RunLoop 当前事件循环占用的时间过多,超过了 32 ms,那么一定会发生一次掉帧。

Point of Interest

iOS 10 之后,引入了一个新的 API,kdebug_signpost_startkdebug_signpost_end。可以结合 Instrument 的 Point of Interest 功能,用来分析起始到结束的耗时。

//事件段的起始和结束
// Timing an activity (code 7 - "Start Up")
kdebug_signpost_start(7, 0, 0, 0, 0);
[self loadAssets];
kdebug_signpost_end(7, 0, 0, 0, 0);

具体使用方法如上,其中第一个参数用来标识事件的 ID,可以同时分析多个事件。如果某个事件发生了多次,那么就可以得到一个列表,以及这个事件耗时的统计信息。

选择列表中的某个点以后,右键选择,可以过滤掉其他时间的信息。

这样子,结合 Time Profile 功能,就可以看到某个事件耗时,以及究竟哪些代码耗时。

结合 RunLoop 与 Point of Instrument 功能

为主线程的 RunLoop 增加一个 observer,并监听特定的状态变化,就找出每一个 RunLoop 循环究竟花费了多长事件。

使用 Instrument,可以得到下面的图。

这样子可以看到每一个 RunLoop 耗时多少,耗时在哪里。找出 Top 问题,针对性优化。

System Trace

time profile 只是查看 CPU 的执行情况,如果一个线程长时间得不到调度,在 time profile 里得不到相应的信息。这时需要用到 System Trace 这个工具。
使用 system trace 时,会记录最近 5s 的 kernel trace,然后分析 Scheduling activity、System calls、Virtual memory operations 等信息。如果可以卡顿可以复现,那么就可以找出来锁等待、死锁、系统调用造成的卡顿问题。
如下图就是由于线程调度造成的卡顿问题。可以看到主线程被 block 了 1 秒多,原因是调用了 AudioSession 相关的函数。

利用 RunLoop 进行优化

找到每一个 RunLoop 中耗时之后,就可以针对性优化,比如主线程读写、懒加载、异步布局之类。也可以把比较复杂的任务分解到不同的 RunLoop 中,这样子 RunLoop 循环的时间不会太长,可以更快响应事件。
具体做法可以参考 texture 这个组件。下面是 copy 过来的代码。

  1. 为主线程的 RunLoop 增加一个 Source

    _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
    CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes);
    
  2. 如果需要,把事件加入一个队列中,等待下一个 RunLoop 处理

      [renderQueue enqueue:node];
    
  3. Runloop 进入 kCFRunLoopBeforeWaiting 状态时,判断队列中是否有待处理的事情。如果有,唤醒 Source,使得 RunLoop 马上进入下一个时间循环

    if (!isQueueDrained) {
        CFRunLoopSourceSignal(_runLoopSource);
        CFRunLoopWakeUp(_runLoop);
     }
    

参考

posted on 2018-01-27 11:22  花老🐯  阅读(1431)  评论(0编辑  收藏  举报

导航