“砰砰砰、砰砰砰、砰砰砰”git
“大师,大师,江湖救急啊”github
“不知少侠,着急让老夫出关所为什么事?”性能优化
“大师以前授与个人iOS性能优化(初级)和iOS性能优化(中级),我已熟悉研读多日,且勤学苦练,至今已能解决大部分滑动卡顿问题。”bash
“少侠,果真聪慧过人”异步
“可是,最近依然遇到了问题,小师妹想作一个相似于微博主页的页面,有不少feed,每一个feed里面,有话题,连接、图片、表情、圆角头像等,这么多元素杂在一块儿,纵然我使出毕生所学,却依然会有卡顿,达不到小师妹对流畅性的要求,因此非常苦恼,恳求大师指点。”async
“原来是这样,老夫这就来助你突破瓶颈,更上一层楼。”oop
在iOS性能优化(初级)和iOS性能优化(中级)中,为了屏幕流畅咱们作了不少,也取得了不错的成果。但不管怎么作,最后的绘制是提交给系统的,系统默认是在主线程作这一切,当须要绘制的元素过多,过于频繁,那么依然会形成卡顿。post
那么咱们可不能够像处理复杂数据同样,把绘制过程放在后台线程执行呢?性能
很高兴,答案是能够的。学习
iOS里面的视图UIView
中有一个CALayer *layer
的属性,UIView
的内容,实际上是layer
显示的,layer
中有一个属性id contents
,contents
的内容就是要显示的具体内容,大多数状况下,contents
的值是一张图片。咱们经常使用的不管是 UILabel
仍是 UIImageView
里面显示的内容,其实都是绘制在一张画布上,绘制完成从画布中导出图片,再把图片赋值给layer.contents
就完成了显示。
异步绘制,就是异步在画布上绘制内容。
Talk is cheap. Show me the code
首先来新建一个AsyncLabel类,而后重写- (void)displayLayer:(CALayer *)layer
方法,在其中进行异步绘制。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AsyncLabel : UIView
//设置文字内容
@property(nonatomic, copy) NSString *text;
//设置字体
@property(nonatomic, strong) UIFont *font;
@end
NS_ASSUME_NONNULL_END
复制代码
#import "AsyncLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncLabel
- (void)displayLayer:(CALayer *)layer
{
NSLog(@"是否是主线程 %d", [[NSThread currentThread] isMainThread]);
//输出 1 表明是主线程
//异步绘制,因此咱们在使用了全局子队列,实际使用中,最好自创队列
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size = CGSizeZero;
__block CGFloat scale = 1.0;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
scale = [UIScreen mainScreen].scale;
});
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)(image.CGImage);
});
});
}
@end
复制代码
- (void)draw:(CGContextRef)context size:(CGSize)size
{
//将坐标系上下翻转。由于底层坐标系和UIKit的坐标系原点位置不一样。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1.0,-1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
//设置内容
NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:self.text];
//设置字体
[attString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
//把frame绘制到context里
CTFrameDraw(frame, context);
}
复制代码
这样就完成了一个简单的绘制。在- (void)displayLayer:(CALayer *)layer
方法中,在异步线程里,建立一个画布并把绘制的结果在主线程中传给layer.contents
。
绘制过程使用了CoreText
,这里只简单的把文字绘制上去,实际使用过程当中,根据须要可能会有不少的地方须要设置,还请少侠自行学习CoreText
。
调用一下看一下结果:
AsyncLabel *label = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 200, [UIScreen mainScreen].bounds.size.width - 2 * 50, 100)];
label.backgroundColor = [UIColor lightGrayColor];
label.text = @"今天是个好日子啊,心想的事儿都能成,今天是个好日子啊,啊,安心,太平";
label.font = [UIFont systemFontOfSize:20];
[self.view addSubview:label];
[label.layer setNeedsDisplay];
复制代码
显示效果达到。
“多谢大师指点,大师一番操做,让我茅塞顿开。”
上面的操做是很是常规的操做,在实际使用中还有几个问题须要解决:
下面老夫来给少侠介绍一种,全新的解决方式,刷新常规想法,且封装优秀。
它的主要处理流程以下:
observer
,它的优先级要比系统的CATransaction
要低,保证系统先作完必须的工做。runloop
会在observer
须要的时机通知统一处理。layer.contents
。大概了解了原理,咱们来使用一下YYAsyncLayer
删除以前在AsyncLabel.m
中使用原始方式异步绘制的代码加入下列代码
- (void)setText:(NSString *)text {
_text = text.copy;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)setFont:(UIFont *)font {
_font = font;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}
复制代码
这一些代码,执行了处理流程中的1、2,注册了observer,并收集了要统一处理的操做。
+ (Class)layerClass
{
return [YYAsyncLayer class];
}
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {
//...
};
task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
if (isCancelled()) {
return;
}
if (!self.text.length) {
return;
}
[self draw:context size:size];
};
task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
// finished
} else {
// cancelled
}
};
return task;
}
复制代码
这些代码实现了流程中的3,异步绘制,并提供给使用者willDisplay
、display
、didDisplay
几个block。
有一点须要注意,必须重写+ (Class)layerClass
,才会进入自定义的subLayer执行方法。至关于打UIView的layer,从默认layer指到subLayer。
上述招式,老夫只是简单演示,但少侠遇到的事要比老夫复杂的多。少侠天资聪慧,切不可傲娇,还需好生练习并配合runloop
、CoreText
使用,方能得心应手。快去答复小师妹去罢。