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: 方法
- 异步绘制情况下,获取渲染相关属性,做哨兵,尺寸检查,决定是否要释放资源
// 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的渲染实现。
我们看一下UIView的drawRect的函数调用栈:
首先会调用 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中。