美文网首页
iOS 底层原理39:Instruments系列(三)Anima

iOS 底层原理39:Instruments系列(三)Anima

作者: Style_月月 | 来源:发表于2022-08-10 18:03 被阅读0次

    iOS 底层原理 文章汇总

    引言

    从Xcode12开始,Instrument更新了UI,新增了一个模板 Animation Hitches 用来分析用户的 app 中的卡顿,并去除了 Core Animation 检测方式。在 iPhone13Pro 之前 iPhone 屏幕最高刷新频率仍为 60 HZ,而在支持 PromotionDisplay 的设备上帧率可调整至 120 帧,并且会根据当前用户手势和设备状态进行动态调整。此时再继续使用帧率来判断性能的好坏及流畅度将会是一个错误的选择。所以 Animation Hitches 主要用于代替帧率检测,并且提出 卡顿时间比(Hitch Time Ratio) 的概念用于替代 FPS。

    在 Hitch 提出之前,都是借助FPS (Frames Per Second 帧率),即每秒绘制帧的数量来衡量页面是否卡顿

    • 滑动屏幕时,帧率理想值为60FPS
    • 滑动屏幕时,帧率越高表示性能越好;帧率过低意味着屏幕可能出现卡顿,存在随机丢帧的可能。
    • 其中帧率>=57为优秀; >=55为良好; >=50为可接受;

    苹果于 20 年的 Session 中提出了 Hitch 的概念,用以衡量滑动时的卡顿情况。Hitch 指的是 卡顿时间(一帧延后出现的时间,ms)/ 总时间(一般是 1 秒),简单来说 卡顿时间比就是一个区间内的总卡顿时间除以它的持续时间。

    • 低于 5 ms/s 说明比较优秀,是最不易被用户察觉到的
    • 介于 5ms/s 和 10ms/s 之间,说明发生了中等卡顿,用户会察觉到一些中断,但并不严重
    • 高于 10 ms/s 说明发生了较严重的卡顿,已经影响了用户体验。
      Hitch

    卡顿

    • 概念:任何时候屏幕上出现晚于预计的帧都属于卡顿
    • 简单来说就是掉帧了,即没有在规定时间内渲染好一帧画面,这就是卡顿一次


      卡顿

    如上图所示,当手指在屏幕上滑动时,滚动视图会随着手势做出响应,如果一帧一帧来看,就是每一帧都对应手指位置的变化。当卡顿发生时,某一帧没有跟随手指变化,导致到下一帧时,产生跳跃,打破了用户和屏幕内容的视觉连接感。图中卡顿产生的原因就是第三帧重复了,主要是因为第四帧的延迟导致了第三帧占用了两帧的时间,给用户看到的就是卡顿掉帧的现象。

    RenderLoop

    • 概念:是一个连续的过程,通过用户手势等将事件传给 App,接着 App 向操作系统传递事件并最终响应事件,再将响应传递给用户的过程


      RenderLoop

      RenderLoop 的时间随着设备刷新频率,在 iPhone13 Pro(Max) 以下的 iPhone 设备最大均为 60 帧,而 iPhone13 Pro(Max) 及 iPadPro 则最高支持 120 帧,也就是最短仅需每 8.33 毫秒就可以显示一个新帧。


      RenderLoop刷新频率

    视图渲染流程

    在每一帧显示的过程中,大概可以分为3个阶段,如下所示


    渲染流程
    • App:进行用户事件处理
    • Render server:负责将图层树转换为可显示的图像(即用户界面绘制)
    • On the display:显示缓存的帧


    1、AppRender server 阶段需要在下一个 VSYNC 到来之前完成
    2、这里运用了 双缓存区 + 垂直同步机制,主要是用于解决 屏幕撕裂现象
    3、整个渲染阶段可以分为5个阶段:

    渲染阶段细分
    • 阶段 1 + 2:App(Event + Commit)
    • 阶段 3 + 4:Render server(Prepare + Excute)
    • 阶段 5:Display

    4、整体渲染流程如下

    • Event(事件阶段)通过 touch、timer 等事件决定用户界面是否需要改变
    • Commit(提交阶段),App会向 Render server(渲染服务器)提交渲染命令
    • Prepare (准备阶段)会为 GPU 的绘制组好准备
    • Excute(执行阶段)会由 GPU 将用户界面的图像绘制出来
    • Display(显示阶段)会将缓冲区的帧交换到屏幕上显示

    下面以一个带有阴影的渲染图形为例,通过观察 RenderLoop 中每一帧所做的工作,来分别介绍不同阶段

    App 阶段

    App 阶段包含 2 个阶段,分别是 Event 、Commit。其中Commit 又分为 4 个子阶段,分别是

    • Layout
    • Display
    • Prepare
    • Commit

    阶段 1:Event 事件阶段

    • 事件阶段 表示 App 接收到了事件(例如:touch、网络请求回调、键盘、timer等)。


      事件阶段-Event
    • 在 App 中可以通过改变其层级结构,或者使用其他方式响应事件。例如图层颜色/大小/位置变化。当 App 更新了图层时, CoreAnimation 会同时调用 setNeedsLayout 方法,该方法能够找出哪些图层需要重新计算布局,且系统会合并这些重新计算的请求,并在 Commit 阶段按需执行,以此来减少重复工作

      层级变化

    阶段 2: Commit 提交阶段

    提交阶段还可以细分,主要分为4个子阶段


    提交阶段-Commit
    • layout (布局阶段):layoutSubviews 会被所有需要布局的 View 调用
    • display(显示阶段):drawRect 会被每个需要被更新的 View 调用
    • prpare(准备阶段):未解码图像进一步解码(即需要优化的常见的图片主线程解码操作)
    • commit(提交阶段):视图树将会被递归打包并发送到 RenderServer 中
    Layout - 布局阶段

    在布局阶段,layoutSubviews 会被所有需要布局的 View 调用。例如视图布局(frame、bounds、tranform等)、增加/移除视图、直接调用 setNeedsLayout/layoutIfNeesed 等

    注:这些操作并不是立即执行,系统会合并布局请求,在 Runloop 休眠前进行统一处理

    Display - 显示阶段
    • 在显示阶段,drawRect 会被每个需要被更新的 View 调用,例如 UILabel 等空间类或者 任何重写 drawRect 方法 的类,必须调用 调用 setNeedsDisplay 用以支持 View 的更新。
    • 非必须不要重写 drawRect 方法,因为在绘制时,每个自定义图层都会接收到带纹理的 CoreGraphics 的背景,会利用 CoreAnimation 进行绘制,这些图层就变成了图片
      • 导致内存额外的开销以及bitmap的存储,对整体内存压力较大
      • 由于是在 CPU上进行绘制,还增加了整体主线程的占用
    drawRect
    Prepare - 准备阶段

    在准备阶段,主要是将还未解码的图像进行进一步解码,这也是我们需要优化的点(即优化图片主线程解码操作)。

    因为对于每个解码的图像,App可能会持续存在大量的内存分配(与图像大小成正比),当App占用内存越来越多时,操作系统就会开始压缩物理内存(physical memory),这整个过程都需要CPU参与,所以除了App会使用CPU,还增加了无法控制的全局 CPU 使用率,导致App消耗更多的物理内存,此时操作系统会终止低优先级的后台进程,从而释放更多的物理内存。但设备的物理内存始终是有限的,当App对内存的消耗达到了临界值时,该App进程就会被操作系统终止,这就是常说的大图导致的OOM

    若某个图像的颜色格式 GPU 无法直接使用,也会在这一步进行格式转换。这就要求对该图像进行 copy 操作,而不是直接使用指针,这样会耗时更长及占用更多的内存。

    Commit - 提交阶段

    在提交阶段,视图树会被递归打包,并发送到 Render Server中,所以当视图图层较复杂时,这个过程的耗时也会相对较长,这也是我们经常提及的优化点(即尽量减轻视图层级结构,不要跟套娃似的,无穷无尽)。

    Render Server 阶段

    Render Server(渲染服务器)主要负责将图层树转换为真正显示的图像,分为两个子阶段


    Render Server 阶段
    • prepare:图层树被编译成一系列简单的指令,供 GPU 执行,帧动画也在此处进行处理
    • excute:GPU 将 App 的图层绘制成最终图像

    阶段 3:Prepare 准备阶段

    • 在 准备阶段,RenderServer 会广度优先遍历 App 的图层树,准备一个线性管线,这样 GPU 就能按照顺序执行命令进行绘制。


      遍历图层树
    • 从根图层开始逐层遍历,最终才有了 GPU 可以在下一个执行阶段执行的整个管线。


      逐层遍历

    阶段 4:Excute 执行阶段

    在执行阶段,主要是由 GPU 根据前面 prepare 阶段准备好的图层树进行顶点着色、形状装配、几何着色、光栅化、片段着色与图层混合。一旦 GPU 执行完会将渲染好的图像放入帧缓存区中等待下一个 VSYNC 的到来并交换到屏幕上进行显示。

    执行阶段-Excute

    Display 阶段

    阶段 5:Display 显示阶段

    在显示阶段,主要是将帧缓存区中的内容交换到显示器上进行最终显示

    视图渲染流程总结

    • App:进行用户事件的处理
      • Event:App接收到事件(touch、网络请求、键盘、timer等)
      • Commit
        • layout (布局阶段):layoutSubviews 会被所有需要布局的 View 调用
        • display(显示阶段):drawRect 会被每个需要被更新的 View 调用
        • prpare(准备阶段):未解码图像进一步解码(即需要优化的常见的图片主线程解码操作)
        • commit(提交阶段):视图树将会被递归打包并发送到 RenderServer 中
    • RenderServer:负责将图层树转换为可显示的图像(即用户界面绘制)
      • prepare:图层树被编译成一系列简单的指令,供 GPU 执行,帧动画也在此处进行处理
      • excute:GPU 将 App 的图层绘制成最终图像
    • Display:将缓冲的帧显示出来

    想了解离屏渲染的同学请阅读# 屏幕卡顿 及 iOS中的渲染流程解析

    卡顿类型

    通过了解了视图渲染的工作流程,其主要工作是在App 和 Render Server 中进行的,所以总共涉及两种卡顿类型

    • 提交卡顿(App 阶段)
    • 渲染卡顿(Render Server 阶段)
      卡顿类型

    提交卡顿

    • 提交卡顿:是指 App 话费过长的时间来处理/提交事件
      提交卡顿
      如上图所示,在提交阶段耗时过长,从而导致错过了截止时间,所以在下一个 VSYNC 中 Render Server 没有需要处理的事情,必须要等待下一个 VSYNC 到了后才开始渲染。简单来说就是把帧传送的时间延迟了一帧(即 16.67ms),这个延迟时间 即为 卡顿时间(Hitch time)

    如何避免提交卡顿?

    主要有以下几种方式

    • 保持视图轻量
    • 避免复杂布局
    • 合理运用多线程能力

    下面进行详细说明

    保持视图的轻量
    • 尽可能利用 CALayer 上GPU 加速的可用属性,如非必要避免使用CPU进行自定义绘制
    • 非必要情况下,避免重写 drawRect 方法,因为会导致额外的内存开销。

    针对于文本、图片等原本就在 CPU 上进行绘制的系统控件,我们可以尝试使用其更底层线程安全的 CoreGraphics 能力,比如 TextKit、CoreText 等搭配多线程异步绘制减轻主线程压力。

    • 尽量复用视图,避免重复的添加/移除视图
    • 如果需要将某一个视图从某一个动画中移除,尽量使用 hidden 属性
    • 对于 Prepare 阶段,当我们的 UIImage 容器视图的大小小于图片本身时,我们通常可以使用 下采样技术(downsampling) 来进行缩略图的创建以节省部分内存空间。
    避免复杂布局
    • 减少代价过高且重复的布局,在需要更新布局时尽量只使用 setNeedsLayout

    layoutIfNeeded 会消耗当前事务的生命周期也会造成卡顿,大多数时候你可以等到下一次 Runloop 执行时再更新你的布局。

    • 避免复杂布局约束,尝试使用最少的约束来完成布局
    • 避免递归布局,即视图应该只能使自己或自己的子视图无效,而不能使其同级视图或父视图无效
    • 避免非必要的视图层级,复杂的视图层级会增加提交阶段的整体耗时
    合理运用多线程能力
    • 利用GCD的多线程能力,充分利用 CPU 多核优势,提前在子线程进行布局等 UI 无关操作,避免主线程挂起(hang)。
    • 避免主线程 IO 等磁盘相关操作
    • 针对于常见的主线程解码操作,
      • 在 iOS15 之前,我们通常都是自己封装或是利用最常见的第三方库 SDWebImage 替我们在子线程进行解码操作。
      • 在 iOS15 中,Apple 终于提供了官方的解决方案以解决该问题:UIImage 的 prepareThumbnailOfSize:completionHandler: 等新接口。
    • 针对于必须在 CPU 上进行绘制的组件,尝试结合多线程使用异步绘制能力减轻主线程压力。

    渲染卡顿

    • 渲染卡顿:是指 Render Server 无法按时准备/执行图层树的出现,即 Excute 阶段耗时超过了 VSYNC 的界限,导致本来应该渲染的帧为准备好。


      渲染卡顿

      如上图所示,绿色的画面比预期的晚了一帧于是有了 16 毫秒的卡顿。

    如何避免渲染卡顿?

    Prepare 阶段对卡顿的影响较少,主要还是在 Excute 阶段的离屏渲染。针对离屏渲染的优化,请阅读
    # iOS 常见触发离屏渲染场景及优化方案总结

    • 对于阴影来说,在设置阴影时确保设置 shadowPath 以减少大量离屏通道
    • 在圆化矩形时,使用 cornerRadiuscornerCurve 属性避免用蒙版或角内容来构成圆角矩形。
    • 优化整个 App 的 Mask。

    使用 masksToBounds 遮蔽为矩形圆角矩形或椭圆形的性能比自定义蒙版图层好得多

    • 合理并谨慎的使用 shouldRasterize 属性,

    它会对一块图层进行光栅化操作并进行缓存。若针对于需要频繁刷新的图层使用该属性反而对性能有着负面影响。

    • 尽量使用非透明的图层
    • 尽量减少图层混合
    • 重要的是用 Instruments 来对 App 进行分析并检查图层树以获得重要的技巧从而降低整体离屏计数。

    下面就主要介绍 Instrument 中 Animation Hitches 的使用

    使用

    • 选中 Instrument 中的 Animation Hitches

      Animation Hitches
    • 启动程序,会显示recording 此时操作界面卡顿的位置工具会记录


      记录
    • 然后再次点击关闭等待Analyze分析完成后显示如下界面,找出耗时的函数


      耗时函数
    • 最后分析,并根据实际情况解决问题

    参考文章

    Tech Talk - Hitches 与 渲染循环
    iOS 性能检测新方式——AnimationHitches
    Animation Hitches in iOS Development
    Explore UI animation hitches and the render loop - Tech Talks - Videos - Apple Developer
    Find and fix hitches in the commit phase - Tech Talks - Videos
    iOS 性能分析-阿里
    iOS 高刷屏监控 + 优化:从理论到实践全面解析 -字节
    WWDC20 10077 - 使用 XCTest 消除动画卡顿
    # APP 性能优化终极求生指南
    # iOS性能优化之界面卡顿监测
    # iOS 高刷屏监控 + 优化:从理论到实践全面解析
    ## 精确定位页面滑动帧率瓶颈及优化参考

    相关文章

      网友评论

          本文标题:iOS 底层原理39:Instruments系列(三)Anima

          本文链接:https://www.haomeiwen.com/subject/reonwrtx.html