iOS列表性能优化之异步绘制

一、背景

iOS所提供的UIKit框架,其工作基本是在主线程上进行,界面绘制、用户输入响应交互等等。当大量且频繁的绘制任务,以及各种业务逻辑同时放在主线程上完成时,便有可能造成界面卡顿,丢帧现象,即在16.7ms内未能完成1帧的绘制,帧率低于60fps黄金标准。目前常用的UITableView或UICollectionView,在大量复杂文本及图片内容填充后,如果没有优化处理,快速滑动的情况下易出现卡顿,流畅性差问题。

二、原理和思路

  • UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。
  • 具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。
    这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。

解决方案使用异步绘制就是:

  • 把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制生成的 bitmap 在子线程完成。
  • 然后在回到主线程把 bitmap 赋值给 view.layer.content 属性。

目前比较好的异步绘制框架,比如YYTextAsyncLayer

三、具体实现

那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?

可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础:

image.png
  1. 首先 UIView 调用 setNeedsDisplay 方法
  2. 其次是调用其 layer 属性的同名方法(view.layer setNeedsDisplay)
  3. 这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。
  4. 在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。

所以去实现 displayLayer 方式,实现开启异步绘制入口

在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。

系统绘制流程
image.png
  1. 首先 CALayer 会在内部创建 一个上下文环境 (CGContextRef)
  2. 然后判断 layer 是否有代理:
    • 没有代理的话,就调用 layer 的 drawInContext: 方法
    • 有代理的话,调用 delegate 的 drawLayer : inContext方法,这个方法实现是系统完成。
    • 然后在合适的时机回调代理,调用 drawRect 默认操作是什么都不做(而之所以有这个接口,就是为了让我们在系统绘制之后,还可以做些自定义的绘制工作)。
  3. 最后无论是哪个分支都把 backing store (上下文环境) 的 bitmap 位图提交到 GPU
  4. 也就是将生成的 bitmap 位图赋值给 layer.content 属性。
好了,下面是重点,如何实现异步绘制的过程

下面看一下异步绘制的时序图能更好的理解异步绘制流程:


image.png
  1. 首先在主线程调用 setNeedsdispay 方法
  2. 系统会在 runloop 将要结束的时候调用 [CAlayer display] 方法
  3. 如果我们的代理实现了dispayLayer 这个方法,会调用 dispayLayer 这个方法。我们可以去子线程里面进行异步绘制。子线程主要做的工作:
  • 创建上下文
  • UI控件的绘制工作
  • 生成对应的图片(bitmap)

4.主线程可以做其他工作

  1. 异步绘制完事之后,回到主线程,把绘制的 bitmap 赋值 view.layer.contents 属性中
面试考点

我们调用 [UIView setNeedsDisplay] 方法的时候,不会立马发送对应视图的绘制工作,为什么?

调用 [UIView setNeedsDisplay] 后,
然后会调用系统的同名方法 [view.layer setNeedsDisplay] 方法并在当前 view 上面打上一个脏标记
当前 Runloop 将要结束的时候才会调用 [CALyer display] 方法,然后进入到视图真正的绘制工作当中。

是否知道异步绘制?如何进行异步绘制?

基于系统开的口子 [layer.delegate dispayLayer:] 方法。

并且实现/遵从了 dispayLayer 这个方法,我们就可以进行异步绘制:
1)代理负责生产对应的 bitmap
2)设置 bitmap 作为 layer.contents 属性的值

代码:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface AsyncDrawLabel : UIView

@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;

@end

NS_ASSUME_NONNULL_END


#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>

@implementation AsyncDrawLabel

- (void)setText:(NSString *)text {
    _text = text;
}

- (void)setFont:(UIFont *)font {
    _font = font;
}


// 除了在drawRect方法中, 其他地方获取context需要自己创建[https://www.jianshu.com/p/86f025f06d62] coreText用法简介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html]
 
- (void)displayLayer:(CALayer *)layer {
    CGSize size = self.bounds.size;
    CGFloat scale = [UIScreen mainScreen].scale;
    // 异步绘制,切换至子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        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;
        });
    });
}

- (void)draw:(CGContextRef)context size:(CGSize)size {
    // 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    // 文本沿着Y轴移动
    CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
    // 文本反转成context坐标系
    CGContextScaleCTM(context, 1, -1);
    // 创建绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
    // 创建需要绘制的文字
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
    [attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
    // 根据attStr生成CTFramesetterRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    // 将frame的内容绘制到content中
    CTFrameDraw(frame, context);
}

一次runloop回调,经常会执行多个绘制任务,这里考虑开辟多个线程去异步执行。首选并行队列可以满足,但为了满足性能效率的同时确保不过多的占用资源和避免线程间竞争等待,更好的方案应该是开辟多个串行队列单线程处理并发任务。
接下来的问题是,异步绘制创建几个串行队列合适?最大并发数参考SDWebImage图片下载并发数的限制数:6。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容