Graver源码阅读

前言

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的页面,或不想使用第三方库的项目来进行对页面的一次异步绘制的改造。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染Graver是一个App渲染框架,采用异步渲染的...
    Bel李玉阅读 1,099评论 0 1
  • 前言 我们经常在面试中,会被问及关于界面优化相关的问题,比如为什么界面会出现卡顿?如何监控卡顿?接着如何解决卡顿?...
    深圳_你要的昵称阅读 1,115评论 1 4
  • UI视图相关的知识 UITableview相关问题 重用机制重用机制主要用到了一个可变数组visiableCell...
    李白杜甫谈恋爱阅读 377评论 0 0
  • CALayer通过四个属性来确定大小和位置, 分别为:frame、bounds、position、anchorPo...
    一川烟草i蓑衣阅读 402评论 0 1
  • UItableView 重用机制 一般在iOS中,tableview的重用机制,我们在- (UITableView...
    叔简阅读 718评论 0 3