在 UIView 中都有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出成位图赋值给 CALayer 的 contents 属性,完成显示。
这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。
那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?
答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。
一、UIView的绘制流程
下面我们来对这个流程进行说明:
- 当我们调用[UIView setNeedsDisplay]方法时,并没有执行立即执行绘制工作。
- 而是马上调用[view.layer setNeedsDisplay]方法,给当前layer打上脏标记, 表明需要绘制了。
- 在当前RunLoop快要结束的时候调用layer 的display方法,来进入到当前视图的真正绘制当中。
- 在layer的display方法内部,系统会判断layer的layer.delegate(默认是对应的UIView, layer上的子layer默认delegate为nil)是否实现了displayLayer:方法,
a.如果没有实现,则执行系统的绘制流程;
b.如果实现了则会进入异步绘制的入口。 - 最后把绘制完的backing store(可以理解为位图)提交给GPU,也就是将生成的 backing store 赋值给 layer.content 属性。。
二、系统绘制流程
对流程加以说明:
- 在layer内部会创建一个backing store,我们可以理解为CGContextRef上下文。
- 判断layer是否有delegate:
2.1 如果有delegate,则会执行[layer.delegate drawLayer:inContext](这个方法的执行是在系统内部执行的),然后在这个方法中会调用view的drawRect:方法,也就是我们重写view的drawRect:方法才会被调用到。
2.2 如果没有delegate,会调用layer的drawInContext方法,也就是我们可以重写的layer的该方法,此刻会被调用到。 - 最后都由CALayer把绘制完的backing store(可以理解为位图)提交给GPU。
三、异步绘制
//如果layer的delegate能respondsTo下面这个方法会调用异步绘制
- (void)displayLayer:(CALayer *)layer;
代理负责生成对应的bitmap,设置该bitmap作为layer.contents属性的值
当view调用setNeedsDisplay的时候,CALayer会调用display,如果代理实现了displayLayer:方法的话,就可以在子线程中去进行位图的绘制,完成后返回主线程把位图赋值给layer的contents属性,完成异步绘制。
下面是一个小demo, 在子线程绘制了一句话, 然后生成位图, 放到这个LoginView.layer.contents上,
#import "LoginView.h"
@import CoreText.CTFramesetter;
@implementation LoginView
- (void)displayLayer:(CALayer *)layer {
CGSize size = self.bounds.size;
// 此处还是主线程
NSLog(@"%s,%@",__func__,[NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSString * str = @"白日依山尽,黄河入海流";
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:str];
[attrStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:NSMakeRange(0, str.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 回到主线程设置layer.contents
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id _Nullable)(image.CGImage);
// layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Image"].CGImage);
});
});
}
@end