iOS界面渲染与优化(二) - UIView与渲染

3,131 阅读11分钟

UIView与CALayer的关系

在iOS UIKit中 UIView 持有一个CALayer成员, UIView继承自UIResponse, 可以响应事件, 而CALayer负责渲染, UIView只是作为一个容器, 用来精简封装layer并对外提供响应操作而已。

CALayer是UIView的基础, 所有实际的绘图工作都是Layer向其backing store里绘制bitmap完成的。而操作View的绝大多数图形属性,其实都是直接操作的其拥有的layer属性, 比如frame, bounds, backgroundColor等等。

关于UIView和CALayer的关系更加详细的信息

可以参考:WWDC 2011 session 121 understanding uikit rendering

tips:

frame属性实际是一个计算属性!!! 被 bounds 和 origin 影响

CALayer的显示基础 - content - CGImageRef - bitmap

简单理解,CALayer 就是屏幕显示的基础。那 CALayer 是如何完成的呢?让我们来从源码向下探索一下,在 CALayer.h 中,CALayer 有这样一个属性 contents:

/** Layer content properties and methods. **/

/* An object providing the contents of the layer, typically a CGImageRef,
 * but may be something else. (For example, NSImage objects are
 * supported on Mac OS X 10.6 and later.) Default value is nil.
 * Animatable. */

@property(nullable, strong) id contents;

contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。而我们进一步查到,Apple 对 CGImageRef 的定义是: A bitmap image or image mask.

实际上, CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store), 而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap, 进而呈现到屏幕上。也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示。

CALayer的图层树 Layer-tree

UIView和CALayer都有自己的树状结构,它们都可以有自己的SubView和SubLayer:

layer1.png

而iOS实际会有三种layer tree:

  1. layer tree(model tree): 一般我们称就是模型树, 也就是各个树的节点的model信息, 比如常见的frame, affineTransform, backgroundColor等等, 这些model数据都是我们在APP开发中可以配置设置的, 我们任何对于view/layer的修改都能反应在model-tree中
  2. presentation tree : 这是一个中间层. 我们APP无法主动操作, 这个层内容是iOS系统在Render Server中生成的!!! CAAnimation 的中间态就都在这一层上更改属性来完成动画的分动作.
  3. render tree:这是直接对应于提交到render server上进行显示的树

layer2.png

在结合第一篇文章中的 CA Commit 内容, 最后需要提交给Render-Server的的内容都是在 model-tree中, 包括Animation的相关参数

UIView display相关方法调用与过程

注意一下整个过程是发生在第一篇中的CA Transaction中的Display步骤!!!

下图是关于在CALayer在渲染之前的流程!!

view1.png

通过绘制过程图我们能归纳一下:

  1. 当调用[UIView setNeedsDisplay]时,实际上会直接调用底层layer的同名方法
  2. 调用[layer setNeedsDisplay]
  3. 然后会被Core Animation捕获到layer-tree的变化, 提交一个CATransaction , 然后触发Runloop的Observer回调,在回调中调用[CALayer display]进行当前视图的真正绘制流程. 这一步可以参考上面3 Runloop中触发渲染的过程
  4. [CALayer display]内部会先判断这个layer的delegate是否会响应displayLayer:方法,如果不响应就会进入系统绘制流程中。如果能够响应,实际上是提供了异步绘制的入口,也就是给我们进行异步绘制留有余地
  1. CoreGraphic的 API是线程安全的, 只要 CGBitmapContextCreate 和 endContext在同一个线程

  2. wwdc2012 session 211 building concurrent user interfaces on ios 内部有一个demo, 帮你理解UIkit的渲染, 并且使用异步渲染结合UIImageView去展示复杂渲染逻辑图的实例.

  3. 关于layout的更新与layoutSubViews的触发, 当我们调用[UIView setNeedsLayout] 时也会触发[CALayer setNeedsLayout]给layer上打上一个脏标记,runloop在下一次循环时, 会去调用[UIView layoutSubviews]/[CALayer layoutSublayers]. 然后触发CA Commit中的Layout处理

  4. 关于CALayer中渲染被触发的时机(不论是系统渲染 or drawRect渲染), 可以参考博主在 UIView/CALayer渲染的触发时机 (juejin.cn) 中实践的逻辑

系统绘制的流程

本质是创建一个 backing storage 的流程

view4.png

  1. [CALayer display]方法调用时, 判断是否有delegate去实现绘制方法, 如果没有就触发系统绘制
  2. 系统绘制时, 会先创建 backing storage(CGContextRef). 注意每个layer都会有一个context, 这个context指向一块缓存区被称为backing storeage
  3. 如果layer有delegate, 则调用delegate的- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx方法(默认会将创建的CGContextRef传入),否则调用-[CALayer drawInContext:]方法,进而调用[UIView drawRect:]方法, 此时已经在CGContextRef环境中, 如果在drawRect中通过UIGraphicsGetCurrentContext() 获取到的就是CALayer创建的CGContextRef.
  4. 注意drawRect方法是在CPU执行的, 在它执行完之后, 通过context将数据(通常情况下这里的最终结果会是一个bitmap, 类型是 CGImageRef)写入backing store, 通过rendserver交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上。

每一个UIView的Layer都有一个对应的Backing Store作为其存储Content的实际内容, 而这些内容其实就是一个CGImage数据, 确切的说,是bitmap数据,以供GPU读取展示。

再次梳理[UIView drawRect]的流程

在开发阶段, 与我们打交道更多的是[UIView drawRect]

如果我们实现了[UIView drawRect]方法, 它会按照如下情况进行工作:

  1. 当我们调用[UIView setNeedsLayout], 底层会调用[CALayer setNeedsLayout]. 然后会给图层增加一个dirty标记, 但还显示原来的内容。它实际上没做任何工作,所以多次调用 -setNeedsDisplay并不会造成性能损.

  2. 然后会触发[CALayer display]方法

  3. CALayer创建一个CGContextRef, 创建一个 backing store, 然后将CGContextRef推入Graphics context stack(因此 CGContextRef是可以嵌套的), 当我们调用UIKit的UIRectFill()等API, 会自动将绘制结果放在stack栈顶的CGContextRef中, 我们也可以直接调用UIGraphicsGetCurrent拿到当前的Grahics context栈顶的CGContextRef.

  4. 然后就是drawRect方法执行了. 绘制的内容在CGContextRef的backing storage中

  5. 这个back storage会保存在与 layer-model-tree关联的属性中, 一起在 commit 时, 提交给 render server

一个特殊场景 -- UIImageView

当我们使用UIImageView时, 这个View仍然有一个CALayer, 但是它会直接使用CGImageRef(UIImage), 我们传给UIImageView的UIImage中的图片可能是没有解码的, 在 CA Commit之前会有一个 prepare过程, 因此, 这样会在CA-Transaction的第三步prepare中能看到如下调用栈:

  1. CA::Layer::prepare_commit
  2. Render::prepare_image
  3. Render::copy_image
  4. Render::create_image
  5. ... decodeImage

UIImage其实是CGImage的一个轻量级封装, 于是很自然的, 在UIImageView中的UIImage对象直接将自己的CGImage图片数据作为CALayer的Content即可, 不再需要重新创建 CGContetRef.

再看Animation

前面我们提到Animation动画是在CA Transaction Commit到Render-Server时, RenderServer 根据 model-layer和动画参数自动完成的. 这里再看一下隐士动画和显示动画

1. 隐式动画

隐式动画是系统框架自动完成的!!!

Core Animation在每个runloop周期中自动开始一次新的事务, 即使你不显式的用[CATransaction begin]开始一次事务,任何在一次runloop循环中属性的改变都会被集中起来, 然后做一次0.25秒的动画。

在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations: , 这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。

CATransaction+begin+commit方法在+animateWithDuration:animations:内部自动调用, 这样block中所有属性的改变都会被事务所包含。

Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView是怎么把它关联的图层的这个特性关闭了呢?

每个UIView是它关联的Layer的delegate,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中, UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];
}
@end

$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

2. 显式动画

Core Animation提供的显式动画类型,既可以直接对退曾属性做动画,也可以覆盖默认的图层行为。 我们经常使用的CABasicAnimation,CAKeyframeAnimation,CATransitionAnimation,CAAnimationGroup等都是显式动画类型,这些CAAnimation类型可以直接提交到layer-model-tree上。

无论是隐式动画还是显式动画,提交到layer后,经过一系列处理,最后都经过render-server描述的绘制过程最终被渲染出来。

UIView的异步绘制优化

有很多异步绘制的框架, 也就是将很多系统需要在绘制的内容在异步子线程去用CPU提前绘制. 可以参考第三方开源库:

YYAsyncLayer ,AsyncDisplayKit

YYAsyncLayer原理

YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。

当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。

AsyncDisplayKit原理

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。

Tips

优化方案围绕着 使用多线程调用,合理利用CPU计算位置,布局,层次,解压等,再合理调度GPU进行渲染,GPU负担常常要比CPU大,合理调度CPU进行计算可以减轻GPU渲染负担,使应用更加流畅。

另外: iOS官方session也可以看一下: wwdc2012 session 211 building concurrent user interfaces on ios

参考文章:

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译文)

离屏渲染(Offscreen Render)

为iOS设计:图形和性能

iOS性能优化系列篇之“列表流畅度优化”

iOS图像显示原理和卡顿优化

iOS 性能优化总结

ios-rounded-corner

落影 - iOS性能优化——图片加载和处理 (iOS性能优化——图片加载和处理 - 云+社区 - 腾讯云 (tencent.com))

iOS离屏渲染优化(附DEMO)

[[转]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)

探讨iOS 中图片的解压缩到渲染过程 - 简书 (jianshu.com)

iOS 图形性能优化 (juejin.cn)