Graver·学习笔记·入门使用

作者: pengxuyuan | 来源:发表于2018-12-29 19:31 被阅读108次

    前言

    Graver 是美团最近开源的一款高效 UI 渲染框架,旨在用更低的资源来构建流畅的 UI 界面。对于复杂的视图层级场景,与传统用视图树来构建页面不同,Graver 使用 CoreGraphics 库将内容绘制成一张 Bitmap 位图,可以极大的减少视图层级。

    这里我就不重复叙述 Graver 的发展过程以及架构设计,可以参阅:美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染Graver GitHub

    主要是写入门 Graver 框架的学习笔记:通过使用 Graver 以及阅读源码,学习到的知识点以及一些疑问。对于知识点希望可以跟大家一同讨论;对于疑问,也还请大家指点迷津,在此先行谢过。

    简单使用

    这里先看看 Graver 的简单使用:使用 WMGCanvasView 类创建一个 View,然后设置一张背景图片,并设置一个圆角:

    //WMGCanvasView 是 Graver 提供的一个基础
    WMGCanvasView *canvasView = [[WMGCanvasView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    canvasView.center = self.view.center;
    canvasView.backgroundImage = [UIImage imageNamed:@"icon.jpeg"];
    canvasView.cornerRadius = 20;
    [self.view addSubview:canvasView];
    

    效果图如下:

    image

    预备知识

    Graver 是通过绘制 Bitmap 来实现 UI 渲染的,在了解 WMGCanvasView 实现原理之前,我们预先了解一些知识点,以便后面更加顺畅的阅读源码。

    UIView 跟 CALayer 的关系

    这里简单列举一下它们之间的区别联系:

    • UIView 继承自 UIResponder ,主要是提供事件响应的能力;CALayer 继承自 NSObject,属于 QuartzCore 框架,主要负责绘制方面的工作。
    • UIView 是 CALayer 的代理,在 CALayer 绘制时提供一些必要的数据信息(绘制,动画)

    对于 UIView 跟 CALayer 更加详尽的区别联系,可以参阅:27·iOS 面试题·UIView和CALayer是啥关系?

    CALayer 绘制的大概流程

    1. 当 CALayer 需要绘制 UI 的时候,会查看 layer 的代理是否实现了 - (void)displayLayer:(CALayer *)layer; 方法,如果实现了,就进入用户自定义绘制流程
    2. 如果没有实现则进入系统绘制流程
    3. 系统绘制流程,就会查看 layer 的代理 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; , 并且调用 UIView 的 - (void)drawRect:(CGRect)rect 方法。

    当然,一图胜千言:

    CALayer 绘制流程

    设计逻辑

    这里先列举上面示例代码涉及到的类:WMGCanvasViewWMGAsyncDrawViewWMGAsyncDrawLayer ,关系图如下:

    WMGCanvasView 关系图

    我们先来看 WMGAsyncDrawView 是如何实现异步绘制功能的:

    WMGAsyncDrawView 内部实现逻辑

    1、指定自定义 Layer

    通过重写方法,将 WMGAsyncDrawView 的 Layer 指定为 WMGAsyncDrawLayer ,通过自定义的 Layer 来抽象控制行为属性:是否需要异步绘制、是否需要渐变显示、当前绘制次数等。

    // 指定 WMGAsyncDrawView 的 Layer 指定为 WMGAsyncDrawLayer
    + (Class)layerClass
    {
        return [WMGAsyncDrawLayer class];
    }
    

    2、自定义异步绘制

    WMGAsyncDrawView 实现了 Layer 的代理方法:- (void)displayLayer:(CALayer *)layer; ,来实现自定义绘制。

    通过上面,我们知道 Layer 在绘制 UI 的时候,如果实现了 - (void)displayLayer:(CALayer *)layer; 方法就会进入用户自定义绘制流程,WMGAsyncDrawView 就是通过实现这个方法来进行绘制 UI 的。

    具体的调用流程如下:

    -[WMGAsyncDrawView displayLayer:]
    -[WMGAsyncDrawView _displayLayer:rect:drawingStarted:drawingFinished:drawingInterrupted:]
    -[WMGCanvasView drawInRect:withContext:asynchronously:userInfo:]
      
      
      // 下面这个就是子类重写绘制方法,拿到绘制的上下文 context,进行自定义绘制 UI
      /**
     * 子类可以重写,并在此方法中进行绘制,请勿直接调用此方法
     *
     * @param rect 进行绘制的区域,目前只可能是 self.bounds
     * @param context 绘制到的context,目前在调用时此context都会在系统context堆栈栈顶
     * @param asynchronously 当前是否是异步绘制
     * @param userInfo 由currentDrawingUserInfo传入的字典,供绘制传参使用
     *
     * @return 绘制是否已执行完成。若为 NO,绘制的内容不会被显示
     */
    - (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo;
    

    3、使用 dispatch_async 异步执行绘制视图

    对于绘制任务,这里使用 dispatch_async 来异步绘制视图

     if (drawInBackground) {
            dispatch_async([self drawQueue], drawBlock);
    } else {
            void (^block)(void) = ^{
                @autoreleasepool {
                    drawBlock();
                }
            };
            
            if ([NSThread isMainThread])
            {
                // 已经在主线程,直接执行绘制
                block();
            }
            else
            {
                // 不应当在其他线程,转到主线程绘制
                dispatch_async(dispatch_get_main_queue(), block);
            }
    }
    

    WMGCanvasView 内部实现逻辑

    WMGCanvasView 继承自 WMGAsyncDrawView,所以 WMGCanvasView 具有异步绘制 UI 的功能。WMGCanvasView 类主要是将 Layer 的属性提取出来,方便用户设置;拿到绘制上下文 context,将对应的 UI 绘制上去。

    1、将 Layer 一些属性封装出来

    我们知道 UIView 会封装 Layer 一些属性,例如 frame、backgroundColor等属性,但是对于 cornerRadius、borderWidth、borderColor 等属性,并没有封装,这里 WMGCanvasView 将这些属性封装,方便调用。

    @interface WMGCanvasView : WMGAsyncDrawView
    
    @property (nonatomic, assign) CGFloat cornerRadius;
    @property (nonatomic, assign) CGFloat borderWidth;
    @property (nonatomic, strong) UIColor *borderColor;
    @property (nonatomic, strong) UIColor *shadowColor;
    @property (nonatomic, assign) UIOffset shadowOffset;
    @property (nonatomic, assign) CGFloat shadowBlur;
    
    //额外提供一个 image 属性,方便为 View 设置一个背景图片
    @property (nonatomic, strong) UIImage *backgroundImage;
    
    @end
    

    2、将信息传递传递给绘制上下文

    通过上面了解了 WMGAsyncDrawView 内部实现逻辑,我们知道自定义绘制 UI,需要重写父类方法 - (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo; ,拿到上下文进行自定义绘制。

    但是在绘制上下文时,需要获取一些必要的信息来进行绘制,例如 borderWidth、cornerRadius、backgroundImage。

    这里是通过重写父类的 - (NSDictionary *)currentDrawingUserInfo 方法来提供。

    知识点

    1、+ (void)initialize 的使用

    + (void)initialize 这个方法是在类或它的子类收到第一条消息之前被调用的,我们可以通过在这个方法做一些初始化的工作。

    这里有两个特征:

    1. 是递归调用 +initialize 方法,父类比子类先初始化(故,不需要调用 super initialize )
    2. 利用 objc_msgSend 发消息模式调用 +initialize 方法的(这里与其它普通方法调用一致)

    故我们可以知道,如果子类没有实现 +initialize 方法,那么父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。

    对于 + (void)initialize 方法稍微消息的链接:24·iOS 面试题·+(void)load; +(void)initialize; 有什么用处?

    2、- (void)didMoveToWindow 的使用

    当 View 的父视图改变时,会调用 - (void)didMoveToWindow 这个方法,这个方法系统默认是一个空实现,子类可以通过重写来实现自己的业务场景。

    WMGAsyncDrawView 这个异步绘制类重写了这个方法,当父视图改变时,判断当前的 View 是否显示在界面上,存在才继续绘制,如果不存在则终止绘制。这里算是一个小优化吧。

    - (void)didMoveToWindow
    {
        NSLog(@"%s",__func__);
        [super didMoveToWindow];
        
        // 没有 Window 说明View已经没有显示在界面上,此时应该终止绘制
        if (!self.window){
            [self interruptDrawingWhenPossible];
        }
        else if (!self.layer.contents){
            [self setNeedsDisplay];
        }
    }
    

    疑问

    1、WMGAsyncDrawView 中 drawRect 方法是否还会被调用?

    首先我们知道 drawRect 方法不需要我们主动去调用,系统会在合适的时机帮我们调用,如果想强制调用的话,也是通过调用 setNeedsDisplay 方法来触发 drawRect 方法。

    WMGAsyncDrawView 类通过实现 - (void)displayLayer:(CALayer *)layer 方法来进行自定义绘制流程,那应该不会再走系统绘制流程了,那就不应该会再调用如下方法了:

    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    - (void)drawRect:(CGRect)rect
    

    WMGAsyncDrawView 的 drawRect 方法在什么场景下会被调用呢?还是说根本不会再被调用到?

    - (void)drawRect:(CGRect)rect
    {   NSLog(@"%s",__func__);
        [self drawingWillStartAsynchronously:NO];
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        if (!context) {
            WMGLog(@"may be memory warning");
        }
        
        [self drawInRect:self.bounds withContext:context asynchronously:NO userInfo:[self currentDrawingUserInfo]];
        [self drawingDidFinishAsynchronously:NO success:YES];
    }
    
    

    总结

    通过阅读 WMGCanvasViewWMGAsyncDrawViewWMGAsyncDrawLayer 的源码,简单了解了 Graver 中最简单的异步绘制流程。

    下一篇继续学习:Graver 使用 CoreGraphics 库来绘制一张 Bitmap 位图,但是对于事件是如何绑定的呢?

    参考文献

    美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染

    Graver GitHub

    相关文章

      网友评论

        本文标题:Graver·学习笔记·入门使用

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