前言
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 绘制的大概流程
- 当 CALayer 需要绘制 UI 的时候,会查看 layer 的代理是否实现了
- (void)displayLayer:(CALayer *)layer;
方法,如果实现了,就进入用户自定义绘制流程 - 如果没有实现则进入系统绘制流程
- 系统绘制流程,就会查看 layer 的代理
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
, 并且调用 UIView 的- (void)drawRect:(CGRect)rect
方法。
当然,一图胜千言:
CALayer 绘制流程设计逻辑
这里先列举上面示例代码涉及到的类:WMGCanvasView
、WMGAsyncDrawView
、WMGAsyncDrawLayer
,关系图如下:
我们先来看 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
这个方法是在类或它的子类收到第一条消息之前被调用的,我们可以通过在这个方法做一些初始化的工作。
这里有两个特征:
- 是递归调用 +initialize 方法,父类比子类先初始化(故,不需要调用 super initialize )
- 利用
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];
}
总结
通过阅读 WMGCanvasView
、WMGAsyncDrawView
、WMGAsyncDrawLayer
的源码,简单了解了 Graver 中最简单的异步绘制流程。
下一篇继续学习:Graver 使用 CoreGraphics 库来绘制一张 Bitmap 位图,但是对于事件是如何绑定的呢?
网友评论