iOS 需要大量的去绘制UI,怎样去平衡GPU和CPU?

9 阅读5分钟

简单来说:CPU 擅长处理逻辑、布局和解码,GPU 擅长渲染纹理、混合和像素填充。  平衡的目标是让两者都不成为瓶颈,且合理分工。

以下是针对大量图层绘制场景的平衡策略,按优先级排序:

1. 识别瓶颈(最重要)

不要盲目优化。使用 Xcode 工具定位瓶颈:

  • Instruments - Time Profiler:看 CPU 是否爆满。如果主线程在 layoutSubviewsdrawRect、图片解码上耗时过多,瓶颈在 CPU。

  • Instruments - Core Animation:看 FPS 和 GPU 利用率

    • Debug Options 中的 Color Offscreen-Rendered(离屏渲染):黄色图层过多会增加 GPU 负担。
    • Color Blended Layers(图层混合):红色区域过多会增加 GPU 像素填充率压力。
    • Color Hits Green and Misses Red:检查 shouldRasterize 的光栅化缓存是否有效。

2. CPU 侧优化(减轻主线程压力)

大量绘制通常导致 CPU 忙于创建视图、计算布局、解码图片。

减少视图层级

  • 使用 CALayer 替代 UIView:如果图层不需要响应交互(点击、手势),直接用 CALayerUIView 是对 CALayer 的封装,包含事件处理逻辑,开销更大。
  • 减少子视图数量:在列表或滚动视图中,尽量复用视图(UITableView/UICollectionView 复用机制)。对于极其复杂的静态界面,考虑使用 drawRect 直接绘制 或使用 TextKit / Core Text 将多个控件合并为一个视图绘制。

异步化非 UI 操作

  • 异步绘制:如果必须实现 drawRect,确保绘图逻辑放在子线程。可以封装一个 UIView,其 drawRect 只是将数据传递给后台,后台生成图片后设置 contents

    • YYTextAsyncDisplayKit (Texture)  等框架的核心原理就是异步绘制。
  • 异步图片解码UIImage 加载后通常是未解码的。系统在渲染到 GPU 前会在主线程解码。务必使用第三方库(如 SDWebImage、Kingfisher)并在后台强制解码(将其绘制到图形上下文中)。

  • 栅格化(Rasterization)

    • 对于复杂、静态、且频繁重用的图层(如列表的圆角头像阴影),设置 layer.shouldRasterize = YES
    • 注意:这会将图层渲染成位图缓存到 GPU 内存。如果图层经常变化(滚动中的 Cell),栅格化会变成性能杀手(反复创建缓存,导致 GPU 抖动)。仅用于静态且不变的视图。

3. GPU 侧优化(减轻渲染压力)

GPU 主要负责纹理上传、顶点转换、像素填充(光栅化)。大量图层容易导致 Overdraw(过度绘制)  和 带宽瓶颈

避免离屏渲染

离屏渲染会迫使 GPU 开辟额外缓冲区进行上下文切换,开销极大。

  • 圆角:不要使用 layer.masksToBounds + cornerRadius 组合(会导致离屏渲染)。

    • 方案:使用 带圆角的图片;或者使用 CAShapeLayer 作为遮罩(虽然也是离屏,但通常比 masksToBounds 效率高且可控);性能要求极高时,直接让设计师提供圆角素材。
  • 阴影

    • 避免使用 shadowOffset + shadowOpacity 组合且无 shadowPath(会导致离屏渲染)。
    • 方案:必须指定 layer.shadowPath,告诉 GPU 阴影的形状,避免 GPU 去计算视图的 Alpha 通道来生成阴影。
  • 组透明度:少用 layer.allowsGroupOpacity

避免图层混合

GPU 混合像素时,需要将多层图层计算叠加。

  • 背景色不透明:确保 UIView 的 opaque = YES,并且 backgroundColor 设置为非透明色(通常是白色)。如果视图完全覆盖其父视图,务必标记为不透明。
  • 避免透明重叠:在滚动视图中,尽量减少透明图层(Alpha < 1)的重叠层数。

控制纹理大小

  • 图片尺寸:不要直接显示 4096x4096 的原图然后缩放到 50x50。GPU 需要加载整个纹理到内存。必须根据显示尺寸裁剪图片。
  • 格式:使用 MTLPixelFormatBGRA8Unorm 或 kCVPixelFormatType_32BGRA。在 iOS 上,BGRA 格式是 GPU 原生支持的,速度最快。

4. 架构层面的选择:UIKit 与 Metal

如果绘制量极大(例如绘图类 App、复杂图表、游戏):

  • 普通场景(几百个图层内) :优化后的 UIKit / Core Animation 足够。

  • 极端场景(数万个图层或实时粒子效果)

    • Texture (AsyncDisplayKit) :将 UIView 替换为 ASDisplayNode。它将视图的创建、布局、绘制全部放到后台线程,只将最终的 contents 提交给 GPU。这是目前 iOS 复杂界面性能优化的天花板。
    • Metal / OpenGL ES:直接接管渲染管线。如果需要在同一时间绘制几千个独立的元素(如地图瓦片层、手写笔迹),使用 Metal 手动管理顶点缓冲区和片段着色器,可以绕过 UIKit 的隐式开销。

5. 实践总结:平衡法则

场景策略平衡点
列表滚动卡顿1. 复用机制 2. 异步绘制/解码 3. 设置 shadowPathCPU 处理复用逻辑和布局;GPU 只负责简单纹理填充。
界面启动/切换慢减少首屏视图层级;懒加载;使用 CALayer 替代 UIViewCPU 减少布局计算时间;GPU 减少首帧渲染指令数量。
动画掉帧开启 shouldRasterize(仅对不变图层);避免离屏渲染;尽量使用 CATransform3D 而非修改 frame让 GPU 处理矩阵变换(硬件加速),CPU 不参与每一帧的坐标计算。
耗电发热降低刷新率(CADisplayLink 降频);减少混合;缩小图片纹理GPU 负载过高是发热主因。减少像素填充率比降低 CPU 频率更有效。

核心原则

将 UIKit 的隐式开销显式化,把非 UI 线程的活移出主线程,把离屏渲染和混合留给 GPU 的带宽极限。