Graver·学习笔记·入门使用
1

前言

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];

效果图如下:

Graver-WMGCanvas Demo1

预备知识

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

学为好人。

讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!