前言
Graver 是一款高效的 UI 渲染框架,它以更低的资源消耗来构建十分流畅的 UI 界面。Graver 独创性的采用了基于绘制的视觉元素分解方式来构建界面,得益于此,该框架能让 UI 渲染过程变得更加简单、灵活。目前,该框架已经在美团 App 的外卖频道、独立外卖 App 核心业务场景的大多数业务中进行了应用,同时感谢美团团队开源了该优秀的框架。
背景
项目中虽然卡片列表FPS、CPU、Memory 等方面的各项指标还算过的去,但是在涉及大量图表的页面中还是存在滑动不流畅的情况,正好美团技术团队开源了Graver项目,在阅读了Graver源码后,虽然没法拿来直接在项目中使用去替换掉页面的元素,但是根据思路修改了自定义一些图表的绘制(大量的折线图,饼图,及复杂元素层级),在经过一段时间将项目中部分模块修改为异步绘制后,大幅度提高了前端用户的体验感觉。
开始
该片文章只要对WMGCanvasView、WMGAsyncDrawView两个基础类的源码进行每一步的详解来总结Graver异步绘制的思想,在开始之前先说两个地方
Graver 渲染原理
绘制原理.png这也是一个最典型的卡片列表页的一个流程大部分是没有绘制队列这一个队列也就是将预排版好的数据直接在主队列中完成绘制,而Graver在绘制队列中将预排版的数据直接将位图画好传回给主队列直接进行展示,不需要在有层级的一层层叠加,也就是大幅度减少了层级过多有带来的卡顿。
CALayer 绘制的大概流程
当 CALayer 需要绘制 UI 的时候,会查看 layer 的代理是否实现了 - (void)displayLayer:(CALayer *)layer; 方法,如果实现了,就进入用户自定义绘制流程
如果没有实现则进入系统绘制流程
系统绘制流程,就会查看 layer 的代理 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; , 并且调用 UIView 的 - (void)drawRect:(CGRect)rect 方法。
Graver实现了displayLayer的代理直接进行自定义的绘制
WMGCanvasView
WMGCanvasView是Graver 提供的一个做基础的画布View类作用与UIImageView基本相同,因为功能比较单一所以可以先通过该类看下基本的异步绘制流程。
在WMGCanvasView.h中直接将操作layer的属性暴露出来方便使用
@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;
@property (nonatomic, strong) UIImage *backgroundImage;
WMGCanvasView.m中的方法只有6个
//初始化
- (id)initWithFrame:(CGRect)frame
//背景颜色相关
- (void)setBackgroundColor:(UIColor *)backgroundColor
- (UIColor *)backgroundColor
//背景图片
- (void)setBackgroundImage:(UIImage *)backgroundImage
//配置数据
- (NSDictionary *)currentDrawingUserInfo
//绘制方法
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
初始化方法中有个drawingPolicy
self.drawingPolicy = WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged;
drawingPolicy是一个关于是否异步绘制的枚举
typedef NS_ENUM(NSInteger, WMGViewDrawingPolicy)
{
// 当 contentsChangedAfterLastAsyncDrawing 为 YES 时异步绘制
WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged,
// 同步绘制
WMGViewDrawingPolicySynchronouslyDraw,
// 异步绘制
WMGViewDrawingPolicyAsynchronouslyDraw,
};
看下绘制- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo方法的具体实现
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
{
[super drawInRect:rect withContext:context asynchronously:asynchronously userInfo:userInfo];
//获取在currentDrawingUserInfo方法中配置的绘制数据
UIColor *backgroundColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewBackgroundColorKey];
CGFloat borderWidth = [[userInfo valueForKey:WMGCanvasViewBorderWidthKey] floatValue];
CGFloat cornerRadius = [[userInfo valueForKey:WMGCanvasViewCornerRadiusKey] floatValue];
UIColor *borderColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewBorderColorKey];
borderWidth *= [[UIScreen mainScreen] scale];
if(cornerRadius == 0){
if (backgroundColor && backgroundColor != [UIColor clearColor]) {
//给上下文填充颜色
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
//填充框
CGContextFillRect(context, rect);
}
if(borderWidth > 0){
//将路径对象加入上下文对象中
CGContextAddPath(context, [UIBezierPath bezierPathWithRect:rect].CGPath);
}
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
if(borderWidth > 0){
//设置线框的颜色
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
//设置线的宽度
CGContextSetLineWidth(context, borderWidth);
//如果borderwidth大于0绘制路径
CGContextDrawPath(context, kCGPathFillStroke);
}else{
//如果borderwidth等于0绘制填充
CGContextDrawPath(context, kCGPathFill);
}
/*
kCGPathStroke:划线(空心)
kCGPathFill: 填充(实心)
kCGPathFillStroke:即划线又填充
*/
}
else{
CGRect targetRect = CGRectMake(0, 0, rect.size.width , rect.size.height);
//带圆角矩形的bezier路径
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:targetRect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
//使用偶数奇数填充规则
[path setUsesEvenOddFillRule:YES];
//就相当于剪裁路径以外的看不见
[path addClip];
CGContextAddPath(context, path.CGPath);
if (backgroundColor && backgroundColor != [UIColor clearColor]) {
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGContextFillRect(context, rect);
CGContextAddPath(context, path.CGPath);
}
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
if(borderWidth > 0){
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextSetLineWidth(context, borderWidth);
CGContextDrawPath(context, kCGPathFillStroke);
}else{
CGContextDrawPath(context, kCGPathFill);
}
}
// 阴影设置
UIColor *shadowColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewShadowColorKey];
CGFloat shadowBlur = [[userInfo valueForKey:WMGCanvasViewShadowBlurKey] floatValue];
UIOffset shadowOffset = [[userInfo valueForKey:WMGCanvasViewShadowOffsetKey] UIOffsetValue];
if (shadowColor) {
//设置阴影颜色
CGContextSetShadowWithColor(context, CGSizeMake(shadowOffset.horizontal, shadowOffset.vertical), shadowBlur, shadowColor.CGColor);
}
//更改当前上下文
UIGraphicsPushContext(context);
UIImage *image = [userInfo valueForKey:WMGCanvasViewBackgroundImageKey];
[image drawInRect:rect];
UIGraphicsPopContext();
return YES;
}
该子类重写的方法在displayLayer中调用
- -(NSDictionary *)currentDrawingUserInfo
- -(BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
这两个方法均是重写父类方法,下面看下在WMGAsyncDrawView如何进行异步绘制的
WMGAsyncDrawView
在WMGAsyncDrawView当中先将layer指定为自己自定义的layer
+ (Class)layerClass
{
return [WMGAsyncDrawLayer class];
}
/**
* 子类可以重写,并在此方法中进行绘制,请勿直接调用此方法
*
* @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;
在前文说过当 CALayer 需要绘制 UI 的时候,会查看 layer 的代理是否实现了 - (void)displayLayer:(CALayer *)layer; 方法,如果实现了,就进入用户自定义绘制流程,在WMGAsyncDrawView中实现了该方法:
- (void)displayLayer:(CALayer *)layer
{
if (!layer) return;
NSAssert([layer isKindOfClass:[WMGAsyncDrawLayer class]], @"WMGAsyncDrawingView can only display WMGAsyncDrawLayer");
if (layer != self.layer) return;
[self _displayLayer:(WMGAsyncDrawLayer *)layer rect:self.bounds drawingStarted:^(BOOL drawInBackground) {
//异步绘制即将启动
[self drawingWillStartAsynchronously:drawInBackground];
} drawingFinished:^(BOOL drawInBackground) {
//异步绘制完成
[self drawingDidFinishAsynchronously:drawInBackground success:YES];
} drawingInterrupted:^(BOOL drawInBackground) {
//异步绘制失败
[self drawingDidFinishAsynchronously:drawInBackground success:NO];
}];
}
- (void)_displayLayer:(WMGAsyncDrawLayer *)layer
rect:(CGRect)rectToDraw
drawingStarted:(WMGAsyncDrawCallback)startCallback
drawingFinished:(WMGAsyncDrawCallback)finishCallback
drawingInterrupted:(WMGAsyncDrawCallback)interruptCallback
{
//是否允许异步绘制
BOOL drawInBackground = layer.isAsyncDrawsCurrentContent && ![[self class] globalAsyncDrawingDisabled];
//增加异步绘制次数
[layer increaseDrawingCount];
//得到绘制次数
NSUInteger targetDrawingCount = layer.drawingCount;
//子类可以重写,用于在主线程生成并传入绘制所需参数
NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
//这部分blcok用的很多看起来很乱
//定义一个drawBlock
void (^drawBlock)(void) = ^{
//定义一个failedBlock用来回掉失败
void (^failedBlock)(void) = ^{
if (interruptCallback)
{
interruptCallback(drawInBackground);
}
};
//如果绘制次数不等于得到的绘制次数中断回掉
if (layer.drawingCount != targetDrawingCount)
{
failedBlock();
return;
}
//获得绘制区域
CGSize contextSize = layer.bounds.size;
//上下文大小有效性
BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
CGContextRef context = NULL;
//绘制是否完成
BOOL drawingFinished = YES;
if (contextSizeValid) {
//开启上下文
UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
//获取当前上下文
context = UIGraphicsGetCurrentContext();
if (!context) {
WMGLog(@"may be memory warning");
}
//保存上下文
CGContextSaveGState(context);
if (rectToDraw.origin.x || rectToDraw.origin.y)
{
//该方法相当于把原来位于 (0, 0) 位置的坐标原点平移到 (tx, ty) 点。在平移后的坐标系统上绘制图形时,所有坐标点的 X 坐标都相当于增加了 tx,所有点的 Y 坐标都相当于增加了 ty。
CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
}
if (layer.drawingCount != targetDrawingCount)
{
drawingFinished = NO;
}
else
{
//绘制方法
drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
}
CGContextRestoreGState(context);
}
// 所有耗时的操作都已完成,但仅在绘制过程中未发生重绘时,将结果显示出来
if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
//生成图片
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 让 UIImage 进行内存管理
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
//定义finishBlock用来完成回掉
void (^finishBlock)(void) = ^{
// 由于block可能在下一runloop执行,再进行一次检查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
layer.contents = (id)image.CGImage;
// 在drawingPolicy 为 WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged 时使用
// 需要异步绘制时设置一次 YES,默认为NO
[layer setContentsChangedAfterLastAsyncDrawing:NO];
//下次AsyncDrawing完成前是否保留当前的contents
[layer setReserveContentsBeforeNextDrawingComplete:NO];
//完成绘制
if (finishCallback)
{
finishCallback(drawInBackground);
}
// 如果当前是异步绘制,且设置了有效fadeDuration,则执行动画
if (drawInBackground && layer.fadeDuration > 0.0001)
{
layer.opacity = 0.0;
[UIView animateWithDuration:layer.fadeDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
layer.opacity = 1.0;
} completion:NULL];
}
};
//是否异步完成完成绘制
if (drawInBackground)
{
dispatch_async(dispatch_get_main_queue(), finishBlock);
}
else
{
finishBlock();
}
}
if (CGImage) {
CGImageRelease(CGImage);
}
}
else
{
failedBlock();
}
UIGraphicsEndImageContext();
};
if (startCallback)
{
//异步绘制即将启动
startCallback(drawInBackground);
}
//是否允许异步绘制
if (drawInBackground)
{
// 清空 layer 的显示
if (!layer.reserveContentsBeforeNextDrawingComplete)
{
layer.contents = nil;
}
//异步全局队列做任务
dispatch_async([self drawQueue], drawBlock);
}
else
{
void (^block)(void) = ^{
@autoreleasepool {
drawBlock();
}
};
if ([NSThread isMainThread])
{
// 已经在主线程,直接执行绘制
block();
}
else
{
// 不应当在其他线程,转到主线程绘制
dispatch_async(dispatch_get_main_queue(), block);
}
}
}
也就是说WMGAsyncDrawView类中开辟了异步队列来进行绘制,但是绘制的具体过程交由继承自WMGAsyncDrawView的子类来实现具体是什么类型的绘制,然后在WMGAsyncDrawView中拿到子类绘制好的context在将其绘制成位图在交付出去。
总结
通过这两个类的思路即可以对自己项目中的一些无法直接使用Graver的页面,或不想使用第三方库的项目来进行对页面的一次异步绘制的改造。
网友评论