iOS 界面渲染与优化(一) - CPU与GPU干了啥事儿
最近在研究界面优化, 看了很多关于iOS界面渲染相关的内容, 这里做一个简单的小结.
1. 计算机的渲染原理
可以参考OpenGL的渲染即可, 或者参考以下文章:
iOS Rendering 渲染全解析(长文干货) (juejin.cn)
ObjC 中国 - 绘制像素到屏幕上 (objccn.io)
2. 成像原理与屏幕卡顿
因此渲染完成以后的渲染的内容在帧缓冲区(framebuffer)
中, 需要视频控制将framebuffer中的内容展示到屏幕上. 在每一帧没展示到界面前需要经过如下两个过程:
- 在CPU中进行视图创建,删除(layer-tree的调整), layout frame计算, 图像解码, 文本绘制等内容, 本质是操作图层树, 将运行结果存储在CommandBuffer
- 将CPU处理结果CommandBuffer提交给GPU, GPU进行渲染
如下图所示, CPU或者GPU任何一个耗时较长就有可能导致丢帧从而导致卡顿.
具体渲染的原理可以参考
iOS 保持界面流畅的技巧 | Garan no dou (ibireme.com)
3. Runloop中触发渲染的过程
以下是在参考文章中的摘抄:
iOS 的显示系统是由 VSync
信号驱动的,VSync
信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到VSync
信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource
通过 mach_port
接收传过来的时钟信号通知,随后Source
的回调会驱动整个 App 的动画与显示。
在iOS中是通过CoreAnimation
, 框架完成Runloop
中的通知监听和后续处理的. CoreAnimation
名称很容易让人误导, 在MAC上被称为LayerKit
.
CoreAnimation
框架会在RunLoop
中注册一个 Observer
,监听了 BeforeWaiting
和 Exit
事件。这个Observer
的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop
被唤醒,App 中的代码会执行一些操作, 可以称为handleEvent
, 常见的操作就是我们开发中实现的那些, 例如:
- 创建和调整视图层级, 例如 addSubView, removeSubView等
- 设置 UIView 的 frame, 调制autolayout约束等
- 修改 CALayer 的透明度
- 为视图添加一个动画
- 其他可能导致 CALayer - Tree 变化的操作
上面的操作实际是会有一个隐士的CATransaction
生成:
隐士[CATransaction begin];
CALayer 图层变化
隐士[CATransaction commit];
上面这些操作最终都会被 CALayer 捕获,并通过 CATransaction
保存到一个中间状态, 在RunLoop
即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知(这个Observer的优先级非常低, 会在最后执行)。这时 CoreAnimation 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。
隐式动画是系统框架自动完成的。
Core Animation在每个runloop周期中自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务,任何在一次runloop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。 这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。 CATransaction的+begin和+commit方法在+animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。
上面的过程在CPU的调用栈, 可能如下:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
4. CPU在界面渲染前干了啥
内容可以参考[WWDC 2014 -Advanced Graphics and Animations for iOS Apps]
这个session已经被apple删了. b站上有人发了这个视频
用另外一个图展示从CPU -> GPU的总流程如下:
-
CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;(这一步是在APP中完成的, 通过IPC框架将CA信息提交给CA)
-
RenderServer解析提交的子树状态, 生成绘制指令; (这一步是在Render Server中, 非APP内部)
-
GPU执行绘制指令
-
显示渲染后的数据
其中CPU中会经历如下步骤, CA在Commit Transaction
时候实际经过了如下4个步骤:
-
Layout - 构建视图
-
调用layoutSubviews方法
- 调用addSubview:方法
- 文本计算(size)等
- AutoLayout根据 Layout Constraint 计算各个view的frame
-
-
Display - 绘制视图
-
在这个阶段程序会创建 layer 的
backing image
,无论是通过 setContents 将一个 image 传給 layer,还是通过drawRect:
或drawLayer: inContext:
来画出来的。所以
drawRect:
等函数是在这个阶段被调用的。注意不要混淆这里的Display和最终的display, 并且这个过程在CPU中完成
-
-
Prepare - 提交前准备
- 图像拷贝和解码(image copy + image decode) -- 这里后面会详细讲
- 尽量使用GPU支持的格式, Apple推荐JPG和PNG
-
Commit - 打包layers 通过IPC提交给Render-Server(另外一个进程)
- 打包layers并发送到render-server
- 递归提交子树的layers (如果view层级过多, 会能看到调用栈中的大量的方法:
CA::Layer::commit_if_needed
)
在CPU完成需要准备的layers并提交给Render-Server以后, 真正的内容就交给GPU来完成了!
这里有一个附加内容, 关于动画 Animation:
Apple的做法很简单直接, 对于每一个Animation, 前期的准备阶段和单独的图像提交基本相同(Layout/Display/Prepare/Commit),但是Render Server在渲染时, 将根据Core Animation的动画参数自动计算出动画所需要的每一帧图像, 然后一帧一帧的渲染显示, 最终呈现的就是动画效果。也就是说App只需要告诉Render Server
动画的起始和终止状态, 它自动将中间过程计算出来并自动渲染这个动画。
小tips:
因此从这里能看出针对CPU中处理的内容优化点如下(后面专门开小结归纳常见的优化方法):
- handleEvent过程中的操作尽可能少, 不做耗时操作
- 布局计算, 文本计算的内容竟可能少, 并且autolayout比frame更加消耗性能
- 图像需要内存对齐, 并且图像解码过程提前在子线程完成!!并且UIImageView.size与图片size保持一致!
小tips2:
关于Animation的动画
我们会在后面介绍三种类型的layer-tree的概念, 它们会帮助我们更加深刻的理解动画
简单来说就是: 你只需要操作起始和终止状态的modal Layer属性, Render Server会在presentation Layer中即完成所有中间过程的计算
5. GPU的渲染过程
目前移动设备都是用的Tiled-Based
渲染, 这里列举了常规的渲染流程和一个离屏渲染的流程:
5.1 普通的Tile-Based渲染流程如下(正常渲染):
- CommandBuffer,接受OpenGL ES处理完毕的渲染指令;
Tiler
,调用顶点着色器,把顶点数据进行分块(Tiling);- ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
- Renderer,调用片元着色器,进行像素渲染;
- RenderBuffer,存储渲染完毕的像素;
5.2 离屏渲染 -- MASK
使用Mask遮罩时, pass1过程执行完成以后, 也需要保存pass1结果, 保存pass2结果, 然后合并成pass3 结果. (普通方式只会有一个storage结果)
- 渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
- 渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
- Compositing操作,合并1、2的纹理;
其他的效果, 比如UIVisiualEffectView
效果的性能消耗更加巨大, 被称为昂贵特效!!! 具体离屏渲染过程可以参考: WWDC 2014 -Advanced Graphics and Animations for iOS Apps
6. 性能优化问答
1、帧率一般在多少? 2、是否存在CPU和GPU瓶颈? (查看占有率) 3、额外的使用CPU来进行渲染? 4、是否存在过多离屏渲染? 5、是否渲染过多视图? 6、使用奇怪的图片格式和大小? 7、使用昂贵的特效? 8、视图树上不必要的元素?
参考文章:
WWDC2011 121: understanding uikit rendering
WWDC2012 211: building concurrent user interfaces on ios
WWDC2012 235: iOS App Performance: Responsiveness
WWDC2012 242: iOS App Performance: Memory
WWDC 2012: iOS App Performance: Graphics and Animations
WWDC 2014 -Advanced Graphics and Animations for iOS Apps
WWDC2018 Image and Graphics Best Practices
WWDC2018 iOS Memory Deep Dive
iOS 视图---动画渲染机制探究 - CocoaChina_一站式开发者成长社区
iOS Rendering 渲染全解析(长文干货) (juejin.cn)
bang神强文-iOS图片加载速度极限优化—FastImageCache解析
绘制像素到屏幕上(Getting Pixels onto the Screen译文)
落影 - iOS性能优化——图片加载和处理 (iOS性能优化——图片加载和处理 - 云+社区 - 腾讯云 (tencent.com))
[[转]iOS 事件处理机制与图像渲染过程](www.cnblogs.com/linganxiong…)
iOS 2D Graphic(1)—— Concept 基本概念和原理
iOS中的图片使用方式、内存对比和最佳实践(juejin.cn/post/684490…)
iOS界面渲染流程分析 - 云+社区 - 腾讯云 (tencent.com)
iOS高效图片 IO 框架是如何炼成的iOS开发-CSDN博客
iOS图片内存管理和性能优化 - 简书 (jianshu.com)
iOS的5种图片缩略技术以及性能探讨 - 简书 (jianshu.com)
JHBlog/加载大图的优化算法.md at master · SunshineBrother/JHBlog (github.com)