Core Animation在渲染中扮演的角色
概述
Core Animation是苹果一个提供渲染和动画的框架,从苹果官方的文档可以知道,Core Animation可以在iOS和OS X上使用,而平常使用的UIKit框架底层使用的就是CoreAnimation实现控件的渲染。
Core Animation摘自苹果开发者中心:
Core Animation is a graphics rendering and animation infrastructure available on both iOS and OS X that you use to animate the views and other visual elements of your app. With Core Animation, most of the work required to draw each frame of an animation is done for you. All you have to do is configure a few animation parameters (such as the start and end points) and tell Core Animation to start. Core Animation does the rest, handing most of the actual drawing work off to the onboard graphics hardware to accelerate the rendering. This automatic graphics acceleration results in high frame rates and smooth animations without burdening the CPU and slowing down your app.
本文将通过断点调试的方式探究Core Anmation在渲染中扮演的角色。
CALayer和UIView
CALayer时Core Animation框架下的一个类,而UIView是UIKit的类,我们从苹果官方的介绍,可以知道,UIView有一个layer属性,view本身并不负责渲染显示工作,他负责给layer提供内容,以及处理事件如点击,而layer只负责显示控件,不参与到响应链当中。这体现了单一职责这一设计原则,这也是为什么CALayer可以在OS X系统中使用的原因。
代码准备
首先,自定义一个Layer和View,并给方法打上断点。
Layer.h文件:
#import <QuartzCore/QuartzCore.h>
@interface LJLayer : CALayer
@end
Layer.m文件:
#import "LJLayer.h"
@implementation LJLayer
- (void)setNeedsDisplay {
[super setNeedsDisplay];// 打上断点
}
- (void)display {
[super display]; // 打上断点
}
- (void)drawInContext:(CGContextRef)ctx {
[super drawInContext:ctx]; // 打上断点
}
@end
View.h文件
#import <UIKit/UIKit.h>
@interface LJView : UIView
@end
View.m文件
#import "LJView.h"
#import "LJLayer.h"
@implementation LJView
- (void)drawRect:(CGRect)rect {
// 打上断点
}
+ (Class)layerClass {
return LJLayer.class;
}
@end
然后,在控制器的ViewDidLoad方法中,添加自定义的View
LJView *_testView = [[LJView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
_testView.backgroundColor = UIColor.blueColor;
[self.view addSubview:_testView]; // 打上断点
...
运行程序。
断点调试
进入断点后,我们使用lldb打印堆栈信息,看看方法的调用流程。
setNeedsDisplay
首先进来的的layer的setNeedsDisplay方法,用bt
命令打印调用信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x000000010f3c7c90 test`-[LJLayer setNeedsDisplay](self=0x00006000009ca800, _cmd="setNeedsDisplay") at LJLayer.m:14:5
frame #1: 0x00007fff490b66d6 UIKitCore`-[UIView(Rendering) setNeedsDisplay] + 82
frame #2: 0x000000010f3c96b3 test`-[LJView setNeedsDisplay](self=0x00007feefd4189e0, _cmd="setNeedsDisplay") at LJView.m:30:5
frame #3: 0x00007fff490950bb UIKitCore`-[UIView _createLayerWithFrame:] + 523
frame #4: 0x00007fff49095a6d UIKitCore`UIViewCommonInitWithFrame + 1020
frame #5: 0x00007fff49095633 UIKitCore`-[UIView initWithFrame:] + 98
frame #6: 0x000000010f3c95b1 test`-[LJView initWithFrame:](self=0x0000000000000000, _cmd="initWithFrame:", frame=(origin = (x = 0, y = 0), size = (width = 100, height = 100))) at LJView.m:19:16
可以看到当View定义frame时,会先执行了_createLayerWithFrame
方法创建layer,之后在执行方法setNeedsDisplay
,而view的setNeedsDisplay方法,最终还是调用layer的setNeedsDisplay
。
走掉当前断点,会发现setNeedsDisplay会在进来一次,此时的调用信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x000000010049ed00 test`-[LJLayer setNeedsDisplay](self=0x0000600001453160, _cmd="setNeedsDisplay") at LJLayer.m:14:5
frame #1: 0x00007fff490b66d6 UIKitCore`-[UIView(Rendering) setNeedsDisplay] + 82
frame #2: 0x00000001004a0703 test`-[LJView setNeedsDisplay](self=0x00007f7fe7c107d0, _cmd="setNeedsDisplay") at LJView.m:30:5
frame #3: 0x00007fff49095650 UIKitCore`-[UIView initWithFrame:] + 127
frame #4: 0x00000001004a0601 test`-[LJView initWithFrame:](self=0x0000000000000000, _cmd="initWithFrame:", frame=(origin = (x = 0, y = 0), size = (width = 100, height = 100))) at LJView.m:19:16
虽然不知道为何会再进来一次,但是可以看到相比第一次,少了_createLayerWithFrame
这个方法,
而之后再执行到_testView.backgroundColor = UIColor.blueColor;
代码时,layer会在执行一次
setNeedsDisplay
,此时的堆栈信息:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x000000010049ed00 test`-[LJLayer setNeedsDisplay](self=0x0000600001453160, _cmd="setNeedsDisplay") at LJLayer.m:14:5
frame #1: 0x00007fff490b66d6 UIKitCore`-[UIView(Rendering) setNeedsDisplay] + 82
frame #2: 0x00000001004a0703 test`-[LJView setNeedsDisplay](self=0x00007f7fe7c107d0, _cmd="setNeedsDisplay") at LJView.m:30:5
frame #3: 0x00007fff490c461c UIKitCore`-[UIView(Internal) _setBackgroundCGColor:withSystemColorName:] + 800
frame #4: 0x00007fff490b1ddf UIKitCore`-[UIView(Hierarchy) _setBackgroundColor:] + 484
所以可以得出结论,但我们给View设置位置、颜色等属性时,UiView先判断当前layer是否创建,如果layer为nil,那么会先执行_createLayerWithFrame
创建layer,之后调用自身的setNeedsDisplay
方法,setNeedsDisplay
方法内部实际调用的是layer.setNeedsDisplay
。
display
断点走完setNeedsDisplay
后进入的是layer的setNeedsDisplay
方法。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x000000010049ee00 test`-[LJLayer display](self=0x0000600001453160, _cmd="display") at LJLayer.m:30:5
frame #1: 0x00007fff2b4bfdca QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 520
frame #2: 0x00007fff2b408c84 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double) + 324
frame #3: 0x00007fff2b43c65f QuartzCore`CA::Transaction::commit() + 649
frame #4: 0x00007fff48bdfc2b UIKitCore`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 81
frame #5: 0x00007fff23d9dcdc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
frame #6: 0x00007fff23d9d3d3 CoreFoundation`__CFRunLoopDoBlocks + 195
frame #7: 0x00007fff23d981c3 CoreFoundation`__CFRunLoopRun + 995
frame #8: 0x00007fff23d97ac4 CoreFoundation`CFRunLoopRunSpecific + 404
frame #9: 0x00007fff38b2fc1a GraphicsServices`GSEventRunModal + 139
frame #10: 0x00007fff48bc7f80 UIKitCore`UIApplicationMain + 1605
frame #11: 0x00000001004a07e2 test`main(argc=1, argv=0x00007ffeef760d50) at main.m:18:12
frame #12: 0x00007fff519521fd libdyld.dylib`start + 1
首先值得注意的是__CFRunLoopRun
和__CFRunLoopDoBlocks
这两个方法,我们在调用栈里并没有发现setNeedsDisplay
方法的调用,而是在新的一轮runloop循环中,触发了__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
这个方法执行了一次回调,另外我们在调用栈中,发现了CoreAnimation的CATransaction的身影,查阅资料得知,在setNeedsDisplay方法执行后,在主线程的下一个runloop到来时,CoreAnimation提交了一个隐式的CATransaction(从调用栈信息上来看应该是一个block),runloop则会在唤醒时commit这个CATransaction,从而执行到display
方法,重新渲染界面。
drawInContext和drawRect
继续走断点,可以看到之后执行的是layer.drawInContext,而后才执行view.drawRect。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x00000001004a059c test`-[LJView drawRect:](self=0x00007f7fe7c107d0, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 100, height = 100))) at LJView.m:16:1
frame #1: 0x00007fff490c8a7e UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 632
frame #2: 0x0000000102e10dd5 UIKit`-[UIViewAccessibility drawLayer:inContext:] + 74
frame #3: 0x00007fff2b4adcfc QuartzCore`-[CALayer drawInContext:] + 286
frame #4: 0x000000010049ee6b test`-[LJLayer drawInContext:](self=0x0000600001453160, _cmd="drawInContext:", ctx=0x0000600002170900) at LJLayer.m:34:5
frame #5: 0x00007fff2b379e48 QuartzCore`CABackingStoreUpdate_ + 196
frame #6: 0x00007fff2b4b66bd QuartzCore`___ZN2CA5Layer8display_Ev_block_invoke + 53
frame #7: 0x00007fff2b4ad66e QuartzCore`-[CALayer _display] + 2026
frame #8: 0x000000010049ee23 test`-[LJLayer display](self=0x0000600001453160, _cmd="display") at LJLayer.m:30:5
总结
至此,我们可总结出CoreAnimation在渲染中做了什么事,当UIView发生位置、大小、内容等变化时,会触发UIView的setNeedsDisplay
,setNeedsDisplay
中会判断当前layer是否已创建,如果layer未创建则调用_createLayerWithFrame
,之后再调用layer.setNeedsDisplay
,但此时CoreAnimation并没有执行渲染,而是通过CATransaction提交这个变化到Runloop,在下一次runloop循环时,才开始触发layer的display、drawInContext一系列方法开始渲染,而通过官方文档我们知道,CoreAnimation底层使用了Metal或OpenGL ES进行渲染,而CoreAnimation则是将UIView数据转为位图数据传递下去。