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:
而iOS实际会有三种layer tree:
- layer tree(model tree): 一般我们称就是模型树, 也就是各个树的节点的model信息, 比如常见的frame, affineTransform, backgroundColor等等, 这些model数据都是我们在APP开发中可以配置设置的, 我们任何对于view/layer的修改都能反应在model-tree中
- presentation tree : 这是一个中间层. 我们APP无法主动操作, 这个层内容是iOS系统在Render Server中生成的!!! CAAnimation 的中间态就都在这一层上更改属性来完成动画的分动作.
- render tree:这是直接对应于提交到render server上进行显示的树
在结合第一篇文章中的 CA Commit 内容, 最后需要提交给Render-Server的的内容都是在 model-tree中, 包括Animation的相关参数
UIView display相关方法调用与过程
注意一下整个过程是发生在第一篇中的CA Transaction
中的Display
步骤!!!
下图是关于在CALayer在渲染之前的流程!!
通过绘制过程图我们能归纳一下:
- 当调用
[UIView setNeedsDisplay]
时,实际上会直接调用底层layer的同名方法 - 调用
[layer setNeedsDisplay]
- 然后会被
Core Animation
捕获到layer-tree
的变化, 提交一个CATransaction
, 然后触发Runloop
的Observer回调,在回调中调用[CALayer display]
进行当前视图的真正绘制流程. 这一步可以参考上面3 Runloop中触发渲染的过程
[CALayer display]
内部会先判断这个layer的delegate是否会响应displayLayer:
方法,如果不响应就会进入系统绘制流程中。如果能够响应,实际上是提供了异步绘制的入口,也就是给我们进行异步绘制留有余地
CoreGraphic的 API是线程安全的, 只要 CGBitmapContextCreate 和 endContext在同一个线程
wwdc2012 session 211 building concurrent user interfaces on ios
内部有一个demo, 帮你理解UIkit的渲染, 并且使用异步渲染
结合UIImageView
去展示复杂渲染逻辑图的实例.关于layout的更新与layoutSubViews的触发, 当我们调用
[UIView setNeedsLayout]
时也会触发[CALayer setNeedsLayout]
给layer上打上一个脏标记,runloop在下一次循环时, 会去调用[UIView layoutSubviews]/[CALayer layoutSublayers]
. 然后触发CA Commit
中的Layout处理关于CALayer中渲染被触发的时机(不论是系统渲染 or drawRect渲染), 可以参考博主在 UIView/CALayer渲染的触发时机 (juejin.cn) 中实践的逻辑
系统绘制的流程
本质是创建一个 backing storage
的流程
- 当
[CALayer display]
方法调用时, 判断是否有delegate去实现绘制方法, 如果没有就触发系统绘制 - 系统绘制时, 会先创建
backing storage(CGContextRef)
. 注意每个layer都会有一个context, 这个context指向一块缓存区被称为backing storeage - 如果layer有delegate, 则调用delegate的
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
方法(默认会将创建的CGContextRef传入),否则调用-[CALayer drawInContext:]
方法,进而调用[UIView drawRect:]
方法, 此时已经在CGContextRef环境中, 如果在drawRect
中通过UIGraphicsGetCurrentContext()
获取到的就是CALayer创建的CGContextRef. - 注意
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]
方法, 它会按照如下情况进行工作:
-
当我们调用
[UIView setNeedsLayout]
, 底层会调用[CALayer setNeedsLayout]. 然后会给图层增加一个dirty标记, 但还显示原来的内容。它实际上没做任何工作,所以多次调用-setNeedsDisplay
并不会造成性能损. -
然后会触发[CALayer display]方法
-
CALayer创建一个CGContextRef, 创建一个 backing store, 然后将CGContextRef推入
Graphics context stack
(因此 CGContextRef是可以嵌套的), 当我们调用UIKit的UIRectFill()
等API, 会自动将绘制结果放在stack
栈顶的CGContextRef中, 我们也可以直接调用UIGraphicsGetCurrent
拿到当前的Grahics context栈顶
的CGContextRef. -
然后就是
drawRect
方法执行了. 绘制的内容在CGContextRef
的backing storage中 -
这个back storage会保存在与 layer-model-tree关联的属性中, 一起在 commit 时, 提交给 render server
一个特殊场景 -- UIImageView
当我们使用UIImageView时, 这个View仍然有一个CALayer, 但是它会直接使用CGImageRef(UIImage), 我们传给UIImageView的UIImage中的图片可能是没有解码的, 在 CA Commit之前会有一个 prepare过程, 因此, 这样会在CA-Transaction的第三步prepare中能看到如下调用栈:
- CA::Layer::prepare_commit
- Render::prepare_image
- Render::copy_image
- Render::create_image
- ... 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译文)
落影 - 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)