美文网首页OpenGL
Core Animation在渲染中扮演的角色

Core Animation在渲染中扮演的角色

作者: iOSer_jia | 来源:发表于2020-07-06 09:44 被阅读0次

    概述

    Core Animation是苹果一个提供渲染和动画的框架,从苹果官方的文档可以知道,Core Animation可以在iOS和OS X上使用,而平常使用的UIKit框架底层使用的就是CoreAnimation实现控件的渲染。

    摘自苹果开发者中心:
    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 Animation

    本文将通过断点调试的方式探究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的setNeedsDisplaysetNeedsDisplay中会判断当前layer是否已创建,如果layer未创建则调用_createLayerWithFrame,之后再调用layer.setNeedsDisplay,但此时CoreAnimation并没有执行渲染,而是通过CATransaction提交这个变化到Runloop,在下一次runloop循环时,才开始触发layer的display、drawInContext一系列方法开始渲染,而通过官方文档我们知道,CoreAnimation底层使用了Metal或OpenGL ES进行渲染,而CoreAnimation则是将UIView数据转为位图数据传递下去。

    执行流程

    相关文章

      网友评论

        本文标题:Core Animation在渲染中扮演的角色

        本文链接:https://www.haomeiwen.com/subject/ajsiqktx.html