iOS Graver

165 阅读8分钟

Graver: github.com/lxqioscoder… Graver (1.0.0)

   Graver for apps within waimai C

   pod 'Graver', '~> 1.0.0'

   - Homepage: github.com/meituan-dia…

   - Source:   github.com/meituan-dia…

   - Versions: 1.0.0 [master repo]

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

Graver是一个App渲染框架,采用异步渲染的方式,很好的解决了App渲染时的性能损耗。

Graver 探究

Graver 是美团 18 年底开源的 iOS 异步渲染框架,因为一些争议最近在 GitHub 上取消开源,不过有 fork 过的仓库我们还是可以看下其实现细节。

Graver 开源的介绍文章可以参考 美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染, 从中可以看到其主推的几大特点:

  • CPU 占用低, 性能表现优异
  • 异步化
  • 性能消耗的边界成本低
  • 渲染速度快

模块关系

Graver 源码中主要有四个部分:

  • AsyncDraw 异步渲染的核心模块,包括渲染分工的类继承关系

  • AttributedItem 负责串联文本,图片等展示信息的对象,将会绑定在 AsyncDraw 模块中的 View 上来进行渲染

  • CoreText 文字渲染,布局,响应事件的核心实现,基于 CoreText Framework

  • PreLayout 使用场景的一些定义,笔墨较少

AysncDraw

异步渲染的核心模块,其中视图类从父到子主要为 WMGAsynceDrawView,WMGCanvasView,WMGCanvaseControl,WMGMixedView 。

WMGAsyncDrawView

WMGAsynceDrawView 顶层类,继承自 UIView, 定义了一些基础属性和行为,比如 layerClass 使用自定义的 WMGAsyncDrawLayer,异步绘制的队列,绘制的策略 (同步或者异步) 等。

核心的绘制则是由 drawRect: 以及 _displayLayer:rect:drawingStarted:drawingFinished:drawingInterrupted: 完成。

- (void)drawRect:(CGRect)rect{    
    [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];
    
}

drawRect: 只是调用了一个等待子类实现的 drawInRectXXX 方法,同时调用了 will 和 did 渲染完成的回调,注意这里是非异步渲染时绘制的流程,如果异步渲染需要显式调用 setNeedsDisplayAsync,然后其会调用 [self.layer setNeedsDisplay] 方法来触发 CALayerDelegate 的 displayLayer: 方法。

而实际进行 layer 绘制的 pipeline 较长,可以分为几大步:

  • 比较 layer 的哨兵 drawingCount 来防止异步导致的绘制上下文异常
[layer increaseDrawingCount];
NSUInteger targetDrawingCount = layer.drawingCount;
if (layer.drawingCount != targetDrawingCount){    
    failedBlock();    
    return;
}
  • 检查渲染尺寸并开始调用上面非异步渲染也调用的 drawInRect:withContext:asynchronously:userInfo: 方法,交给子类渲染
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();    
    CGContextSaveGState(context);    
    // ...    
   drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];  CGContextRestoreGState(context);
    }
  • 渲染完成生成位图, 并在主线程设置为 layer 的 backingImage
// 所有耗时的操作都已完成,但仅在绘制过程中未发生重绘时,将结果显示出来
if (drawingFinished && targetDrawingCount == layer.drawingCount){    
    CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;    
    {        
        // 让 UIImage 进行内存管理        
        UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;        void (^finishBlock)(void) = ^{            // 由于block可能在下一runloop执行,再进行一次检查            if (targetDrawingCount != layer.drawingCount)            {                failedBlock();                return;            }                        layer.contents = (id)image.CGImage;            // ...        }        if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);        else finishBlock();    }        // 一些清理工作: release CGImageRef, Image context ending}
  • 线程的处理上,绘制可以指定在外部传进来的队列,否则就使用 global queue
- (dispatch_queue_t)drawQueue{    if (self.dispatchDrawQueue)    {        return self.dispatchDrawQueue;    }    return dispatch_get_global_queue(self.dispatchPriority, 0);}
其他视图类

WMGCanvasView 继承自 WMGAsyncDrawView, 主要负责圆角,边框,阴影和背景图片的绘制,绘制通过 CoreGraphics API 。

WMGCanvasControl 继承自 WMGCanvasView,在这层处理事件响应,自实现了一套 Target-Action 模式,重写了 touchesBegin/Moved/Cancelled/Moved 一系列方法,来进行响应状态决议,然后将事件发给缓存的 targets 对象看能否响应指定的 control events 。

- (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event{    for(__WMGCanvasControlTargetAction *t in [self _targetActions])    {        if(t.controlEvents == controlEvents)        {            if(t.target && t.action)            {                [self sendAction:t.action to:t.target forEvent:nil];            }        }    }}

WMGMixedView 则是上层视图,属性仅有水平/垂直对齐方式,行数和绘制内容 attributedItem 。drawInRect 中则根据对齐方式来决定绘制文字位置, 然后调用 textDrawer 来进行文字渲染,如果其中有图片则会读取后直接通过 drawInRect: 方法来渲染图片(通过 TextDrawer 的 delegate)。

Graver 通过将所有子视图/图层压扁的形式来减少图层的层级,比较适用于静态内容渲染的场景,但失去了视图/图层树,也相应就失去了树形结构的灵活性,这个 Demo 中如果手动点击 cell,会导致整个 cell content 重绘,出现图片闪屏的情况。而在不使用 Graver 情况下,点击 cell 只需要 selectionView 或其他点击区域去做出相关响应反馈即可,所以视图层级的划分可以帮助我们更细粒度的去进行布局,绘制和点击事件的处理。

另外在未开启异步渲染时,更多的依赖 drawRect: 方法也会带来一定的内存消耗,尤其是较大区域的绘制。

所以总的来看, Graver 在作为第三方库接入时,比较适用于部分静态区域图文组合的绘制,不适用于大规模的使用。

YYAsyncLayer

YYAsyncLayer 比较老,属于 YYKit 其中一部分,其核心就是同名类,该类继承自 CALayer,只专注于异步渲染的 layer 实现。

来看其 _displayAsync: 方法

  1. 异步绘制情况下,获取渲染相关属性,做哨兵,尺寸检查,决定是否要释放资源
// Sentinel 实际为一个可以原子自增的 int32_tYYSentinel *sentinel = _sentinel;int32_t value = sentinel.value;BOOL (^isCancelled)(void) = ^BOOL() {    return value != sentinel.value;};CGSize size = self.bounds.size;BOOL opaque = self.opaque;CGFloat scale = self.contentsScale;CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;if (size.width < 1 || size.height < 1) {    CGImageRef image = (__bridge_retained CGImageRef)(self.contents);    self.contents = nil;    if (image) {        dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{            CFRelease(image);        });    }    if (task.didDisplay) task.didDisplay(self, YES);    CGColorRelease(backgroundColor);    return;}

2. 异步开始绘制图片, 主要绘制颜色,不断的对哨兵进行检查

UIGraphicsBeginImageContextWithOptions(size, opaque, scale);CGContextRef context = UIGraphicsGetCurrentContext();if (opaque) {//  ... 背景颜色绘制}task.display(context, size, isCancelled);// ... sentinel checkUIImage *image = UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();// 切换到主线程渲染dispatch_async(dispatch_get_main_queue(), ^{// ... sentinel check    self.contents = (__bridge id)(image.CGImage);if (task.didDisplay) task.didDisplay(self, YES);});

非异步渲染实现与其类似,这里省略。

可以看出 Graver 应该参考了 YY 的异步实现,同时在上层抽象出继承链来分摊不同职责。YYAsyncLayer 在 YYKit 的位置相对底层,依赖其的 YYText 则会实现协议,完成上层渲染的实现。

同时,YYAsyncLayer 中还抽象了 Transaction 的概念,在第一次调用时向主线程 RunLoop 注册优先级低于 CoreAnimation 的 Observer ,

static void YYTransactionSetup() {    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        transactionSet = [NSMutableSet new];        CFRunLoopRef runloop = CFRunLoopGetMain();        CFRunLoopObserverRef observer;                observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,                                           true,      // repeat                                           0xFFFFFF,  // after CATransaction(2000000)                                           YYRunLoopObserverCallBack, NULL);        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);        CFRelease(observer);    });}

回调时遍历当前所有未执行的 transaction 来触发执行。

线程方面,其默认会读取 YYDispatchQueue 线程池的队列,如果没有该模块则根据硬件情况来简单的实现一个线程池,通过 number % capacity 的方式来分配负载。

Graver --> UIView的绘制流程

UIView的绘制离不开CALayer,UIView的layer是CALayer,CALayer的delegate(CALayerDelegate)是UIView,CALayer主要负责内容,当内容改变时通过CALayerDelegate代理方法来询问UIView的渲染实现。

image.png

我们看一下UIView的drawRect的函数调用栈:

image.png

首先会调用 display方法,该方法会默认询问CALayerDelegate的 - (void)displayLayer:(CALayer *)layer;方法,先后在调用 drawInContext:,该方法会默认询问 CALayerDelegate的- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;方法,最后调用drawRect:方法。

在 Graver中,是通过在UIView中重写CALayerDelegate的- (void)displayLayer:(CALayer *)layer;方法来接管绘制过程。

如何实现异步绘制成图片的

具体的绘制过程是通过CoreGraphics和 CoreText进行绘制的,UIView的 背景色,背景图片,边框,圆角 使用 CoreGraphics实现,文字图片信息主要使用CoreText进行绘制。 通过Graver我们看下主要的绘制过程,新建LYDrawView继承于UIView

-(void)displayLayer:(LYDrawLayer *)layer {
    if (!layer) {
        return;
    }
    [self _displayLayer:layer rect:self.bounds];

}

- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo {

    // backgroundColor
    CGContextSetFillColorWithColor(context, [UIColor yellowColor].CGColor);
    CGContextFillRect(context, rect);

    // borderWidth
    CGContextAddPath(context, [UIBezierPath bezierPathWithRect:rect].CGPath);
    CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextSetLineWidth(context, 0.5);
    CGContextDrawPath(context, kCGPathFillStroke);

    UIGraphicsPushContext(context);
    UIImage *image = [UIImage imageNamed:@"a5a61ab8c196836fe1efbcd9d33edc44"];
    [image drawInRect:CGRectInset(rect, 8, 8)];
    UIGraphicsPopContext();

    return YES;
}

- (void) _displayLayer:(LYDrawLayer *)layer rect:(CGRect) rectToDraw {
    BOOL drawInBackground = layer.isAsyncDrawsCurrentContent ;

    // 绘制Block
    void(^drawBlock)(void) = ^{
        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();

            CGContextSaveGState(context);

            if (rectToDraw.origin.x || rectToDraw.origin.y) {
                CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
            }

            drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:@{}];

            CGContextRestoreGState(context);
        }

        if (drawingFinished) {
            CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
            {
                UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;

                void (^finishBlock)(void) = ^{
                    layer.contents = (id)image.CGImage;
                };

                if (drawInBackground) {
                    dispatch_async(dispatch_get_main_queue(), finishBlock);
                }else {
                    finishBlock();
                }
            }
            if (CGImage) {
                CGImageRelease(CGImage);
            }
        }
        UIGraphicsEndImageContext();
    };


    if (drawInBackground) {

        layer.contents = nil;

        dispatch_async(dispatch_get_global_queue(0, 0),drawBlock);
    }
}

新建 LYDrawTextView继承于 LYDrawView,重写- (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];

    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    // 创建 绘制的区域
    CGMutablePathRef path = CGPathCreateMutable();
       CGRect bounds = CGRectInset(rect, 8, 8);
       CGPathAddRect(path, NULL, bounds);

    // 创建NSMutableString
       CFStringRef textString = CFSTR("Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine.");

       CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
       // 将 textString 复制到 attrString中
       CFAttributedStringReplaceString(attrString, CFRangeMake(0, 0), textString);

       // 创建Color
       CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
       CGFloat components[] = {1.0,0.0,0.0,0.8};
       CGColorRef red = CGColorCreate(rgbColorSpace, components);
       CGColorSpaceRelease(rgbColorSpace);

       // 设置前12位的颜色
       CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12), kCTForegroundColorAttributeName, red);

       // 使用attrString 创建 framesetter
       CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
       CFRelease(attrString);

       // 创建ctframe
       CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
       // 在当前context 绘制ctframe
       CTFrameDraw(ctframe, context);

       CFRelease(ctframe);
       CFRelease(path);
       CFRelease(framesetter);

    return YES;
}

在 Graver中 通过-(BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo方法向context绘制元素,在所有子类的绘制任务完成时,将context合成一张图片,赋值到layer.contents中。

参考:github.com/DevaLee/LYD…