结合 RunLoop 和 Instrument 定位卡顿

结合 RunLoop 和 Instrument 定位卡顿

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

RunLoop 的概念

在程序中,我们需要一种机制,可以让当前线程能够随时处理事件但不退出。这种模型通常被称为 Event Loop,在 iOS 中使用 RunLoop 来实现。
RunLoop 管理了所在线程需要处理的事件和消息。当有事情需要处理时,就唤醒当前线程,处理事件。当事情全部处理完毕时,线程处于休眠状态,以避免资源占用。这样子线程一直处于“接受消息->等待->处理”循环中。
结合 RunLoop 和 Instrument 定位卡顿
根据苹果文档,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,可以同时分析多个事件。如果某个事件发生了多次,那么就可以得到一个列表,以及这个事件耗时的统计信息。
结合 RunLoop 和 Instrument 定位卡顿
选择列表中的某个点以后,右键选择,可以过滤掉其他时间的信息。
结合 RunLoop 和 Instrument 定位卡顿
这样子,结合 Time Profile 功能,就可以看到某个事件耗时,以及究竟哪些代码耗时。

结合 RunLoop 与 Point of Instrument 功能

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

使用 Instrument,可以得到下面的图。
结合 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 和 Instrument 定位卡顿

利用 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  花老????  阅读(927)  评论(0)  编辑  收藏

上一篇:.NET 6 全新指标 System.Diagnostics.Metrics 介绍


下一篇:Pandas中OSError Traceback (most recent call last)的一中错误可能