前言
Graver 是一款高效的 UI 渲染框架稳吮,它以更低的資源消耗來構(gòu)建十分流暢的 UI 界面殉摔。Graver 獨(dú)創(chuàng)性的采用了基于繪制的視覺元素分解方式來構(gòu)建界面凄贩,得益于此淑玫,該框架能讓 UI 渲染過程變得更加簡(jiǎn)單、靈活捆毫。目前,該框架已經(jīng)在美團(tuán) App 的外賣頻道冲甘、獨(dú)立外賣 App 核心業(yè)務(wù)場(chǎng)景的大多數(shù)業(yè)務(wù)中進(jìn)行了應(yīng)用绩卤,同時(shí)感謝美團(tuán)團(tuán)隊(duì)開源了該優(yōu)秀的框架。
背景
項(xiàng)目中雖然卡片列表FPS江醇、CPU濒憋、Memory 等方面的各項(xiàng)指標(biāo)還算過的去,但是在涉及大量圖表的頁面中還是存在滑動(dòng)不流暢的情況陶夜,正好美團(tuán)技術(shù)團(tuán)隊(duì)開源了Graver項(xiàng)目凛驮,在閱讀了Graver源碼后,雖然沒法拿來直接在項(xiàng)目中使用去替換掉頁面的元素条辟,但是根據(jù)思路修改了自定義一些圖表的繪制(大量的折線圖黔夭,餅圖,及復(fù)雜元素層級(jí))羽嫡,在經(jīng)過一段時(shí)間將項(xiàng)目中部分模塊修改為異步繪制后本姥,大幅度提高了前端用戶的體驗(yàn)感覺。
開始
該片文章只要對(duì)WMGCanvasView杭棵、WMGAsyncDrawView兩個(gè)基礎(chǔ)類的源碼進(jìn)行每一步的詳解來總結(jié)Graver異步繪制的思想婚惫,在開始之前先說兩個(gè)地方
Graver 渲染原理
這也是一個(gè)最典型的卡片列表頁的一個(gè)流程大部分是沒有繪制隊(duì)列這一個(gè)隊(duì)列也就是將預(yù)排版好的數(shù)據(jù)直接在主隊(duì)列中完成繪制,而Graver在繪制隊(duì)列中將預(yù)排版的數(shù)據(jù)直接將位圖畫好傳回給主隊(duì)列直接進(jìn)行展示,不需要在有層級(jí)的一層層疊加先舷,也就是大幅度減少了層級(jí)過多有帶來的卡頓艰管。
CALayer 繪制的大概流程
當(dāng) CALayer 需要繪制 UI 的時(shí)候,會(huì)查看 layer 的代理是否實(shí)現(xiàn)了 - (void)displayLayer:(CALayer *)layer; 方法蒋川,如果實(shí)現(xiàn)了蛙婴,就進(jìn)入用戶自定義繪制流程
如果沒有實(shí)現(xiàn)則進(jìn)入系統(tǒng)繪制流程
系統(tǒng)繪制流程,就會(huì)查看 layer 的代理 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; 尔破, 并且調(diào)用 UIView 的 - (void)drawRect:(CGRect)rect 方法街图。
Graver實(shí)現(xiàn)了displayLayer的代理直接進(jìn)行自定義的繪制
WMGCanvasView
WMGCanvasView是Graver 提供的一個(gè)做基礎(chǔ)的畫布View類作用與UIImageView基本相同,因?yàn)楣δ鼙容^單一所以可以先通過該類看下基本的異步繪制流程懒构。
在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個(gè)
//初始化
- (id)initWithFrame:(CGRect)frame
//背景顏色相關(guān)
- (void)setBackgroundColor:(UIColor *)backgroundColor
- (UIColor *)backgroundColor
//背景圖片
- (void)setBackgroundImage:(UIImage *)backgroundImage
//配置數(shù)據(jù)
- (NSDictionary *)currentDrawingUserInfo
//繪制方法
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
初始化方法中有個(gè)drawingPolicy
self.drawingPolicy = WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged;
drawingPolicy是一個(gè)關(guān)于是否異步繪制的枚舉
typedef NS_ENUM(NSInteger, WMGViewDrawingPolicy)
{
// 當(dāng) contentsChangedAfterLastAsyncDrawing 為 YES 時(shí)異步繪制
WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged,
// 同步繪制
WMGViewDrawingPolicySynchronouslyDraw,
// 異步繪制
WMGViewDrawingPolicyAsynchronouslyDraw,
};
看下繪制- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo方法的具體實(shí)現(xiàn)
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
{
[super drawInRect:rect withContext:context asynchronously:asynchronously userInfo:userInfo];
//獲取在currentDrawingUserInfo方法中配置的繪制數(shù)據(jù)
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){
//將路徑對(duì)象加入上下文對(duì)象中
CGContextAddPath(context, [UIBezierPath bezierPathWithRect:rect].CGPath);
}
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
if(borderWidth > 0){
//設(shè)置線框的顏色
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
//設(shè)置線的寬度
CGContextSetLineWidth(context, borderWidth);
//如果borderwidth大于0繪制路徑
CGContextDrawPath(context, kCGPathFillStroke);
}else{
//如果borderwidth等于0繪制填充
CGContextDrawPath(context, kCGPathFill);
}
/*
kCGPathStroke:劃線(空心)
kCGPathFill: 填充(實(shí)心)
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)];
//使用偶數(shù)奇數(shù)填充規(guī)則
[path setUsesEvenOddFillRule:YES];
//就相當(dāng)于剪裁路徑以外的看不見
[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);
}
}
// 陰影設(shè)置
UIColor *shadowColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewShadowColorKey];
CGFloat shadowBlur = [[userInfo valueForKey:WMGCanvasViewShadowBlurKey] floatValue];
UIOffset shadowOffset = [[userInfo valueForKey:WMGCanvasViewShadowOffsetKey] UIOffsetValue];
if (shadowColor) {
//設(shè)置陰影顏色
CGContextSetShadowWithColor(context, CGSizeMake(shadowOffset.horizontal, shadowOffset.vertical), shadowBlur, shadowColor.CGColor);
}
//更改當(dāng)前上下文
UIGraphicsPushContext(context);
UIImage *image = [userInfo valueForKey:WMGCanvasViewBackgroundImageKey];
[image drawInRect:rect];
UIGraphicsPopContext();
return YES;
}
該子類重寫的方法在displayLayer中調(diào)用
- -(NSDictionary *)currentDrawingUserInfo
- -(BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
這兩個(gè)方法均是重寫父類方法餐济,下面看下在WMGAsyncDrawView如何進(jìn)行異步繪制的
WMGAsyncDrawView
在WMGAsyncDrawView當(dāng)中先將layer指定為自己自定義的layer
+ (Class)layerClass
{
return [WMGAsyncDrawLayer class];
}
/**
* 子類可以重寫,并在此方法中進(jìn)行繪制胆剧,請(qǐng)勿直接調(diào)用此方法
*
* @param rect 進(jìn)行繪制的區(qū)域絮姆,目前只可能是 self.bounds
* @param context 繪制到的context,目前在調(diào)用時(shí)此context都會(huì)在系統(tǒng)context堆棧棧頂
* @param asynchronously 當(dāng)前是否是異步繪制
* @param userInfo 由currentDrawingUserInfo傳入的字典秩霍,供繪制傳參使用
*
* @return 繪制是否已執(zhí)行完成篙悯。若為 NO,繪制的內(nèi)容不會(huì)被顯示
*/
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo;
在前文說過當(dāng) CALayer 需要繪制 UI 的時(shí)候铃绒,會(huì)查看 layer 的代理是否實(shí)現(xiàn)了 - (void)displayLayer:(CALayer *)layer; 方法鸽照,如果實(shí)現(xiàn)了,就進(jìn)入用戶自定義繪制流程颠悬,在WMGAsyncDrawView中實(shí)現(xiàn)了該方法:
- (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) {
//異步繪制即將啟動(dòng)
[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];
//增加異步繪制次數(shù)
[layer increaseDrawingCount];
//得到繪制次數(shù)
NSUInteger targetDrawingCount = layer.drawingCount;
//子類可以重寫矮燎,用于在主線程生成并傳入繪制所需參數(shù)
NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
//這部分blcok用的很多看起來很亂
//定義一個(gè)drawBlock
void (^drawBlock)(void) = ^{
//定義一個(gè)failedBlock用來回掉失敗
void (^failedBlock)(void) = ^{
if (interruptCallback)
{
interruptCallback(drawInBackground);
}
};
//如果繪制次數(shù)不等于得到的繪制次數(shù)中斷回掉
if (layer.drawingCount != targetDrawingCount)
{
failedBlock();
return;
}
//獲得繪制區(qū)域
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);
//獲取當(dāng)前上下文
context = UIGraphicsGetCurrentContext();
if (!context) {
WMGLog(@"may be memory warning");
}
//保存上下文
CGContextSaveGState(context);
if (rectToDraw.origin.x || rectToDraw.origin.y)
{
//該方法相當(dāng)于把原來位于 (0, 0) 位置的坐標(biāo)原點(diǎn)平移到 (tx, ty) 點(diǎn)。在平移后的坐標(biāo)系統(tǒng)上繪制圖形時(shí)赔癌,所有坐標(biāo)點(diǎn)的 X 坐標(biāo)都相當(dāng)于增加了 tx诞外,所有點(diǎn)的 Y 坐標(biāo)都相當(dāng)于增加了 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);
}
// 所有耗時(shí)的操作都已完成灾票,但僅在繪制過程中未發(fā)生重繪時(shí)峡谊,將結(jié)果顯示出來
if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
//生成圖片
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 讓 UIImage 進(jìn)行內(nèi)存管理
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
//定義finishBlock用來完成回掉
void (^finishBlock)(void) = ^{
// 由于block可能在下一runloop執(zhí)行,再進(jìn)行一次檢查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
layer.contents = (id)image.CGImage;
// 在drawingPolicy 為 WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged 時(shí)使用
// 需要異步繪制時(shí)設(shè)置一次 YES刊苍,默認(rèn)為NO
[layer setContentsChangedAfterLastAsyncDrawing:NO];
//下次AsyncDrawing完成前是否保留當(dāng)前的contents
[layer setReserveContentsBeforeNextDrawingComplete:NO];
//完成繪制
if (finishCallback)
{
finishCallback(drawInBackground);
}
// 如果當(dāng)前是異步繪制既们,且設(shè)置了有效fadeDuration,則執(zhí)行動(dòng)畫
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)
{
//異步繪制即將啟動(dòng)
startCallback(drawInBackground);
}
//是否允許異步繪制
if (drawInBackground)
{
// 清空 layer 的顯示
if (!layer.reserveContentsBeforeNextDrawingComplete)
{
layer.contents = nil;
}
//異步全局隊(duì)列做任務(wù)
dispatch_async([self drawQueue], drawBlock);
}
else
{
void (^block)(void) = ^{
@autoreleasepool {
drawBlock();
}
};
if ([NSThread isMainThread])
{
// 已經(jīng)在主線程班缰,直接執(zhí)行繪制
block();
}
else
{
// 不應(yīng)當(dāng)在其他線程贤壁,轉(zhuǎn)到主線程繪制
dispatch_async(dispatch_get_main_queue(), block);
}
}
}
也就是說WMGAsyncDrawView類中開辟了異步隊(duì)列來進(jìn)行繪制,但是繪制的具體過程交由繼承自WMGAsyncDrawView的子類來實(shí)現(xiàn)具體是什么類型的繪制埠忘,然后在WMGAsyncDrawView中拿到子類繪制好的context在將其繪制成位圖在交付出去脾拆。
總結(jié)
通過這兩個(gè)類的思路即可以對(duì)自己項(xiàng)目中的一些無法直接使用Graver的頁面馒索,或不想使用第三方庫的項(xiàng)目來進(jìn)行對(duì)頁面的一次異步繪制的改造。