iOS性能优化(中级+): 异步绘制

山雨欲来

“砰砰砰、砰砰砰、砰砰砰”

“大师,大师,江湖救急啊”

“不知少侠,着急让老夫出关所为何事?”

“大师之前授与我的iOS性能优化(初级)iOS性能优化(中级),我已熟悉研读多日,且勤学苦练,至今已能解决大部分滑动卡顿问题。”

“少侠,果然聪慧过人”

“但是,最近依然遇到了问题,小师妹想做一个类似于微博主页的页面,有很多feed,每个feed里面,有话题,链接、图片、表情、圆角头像等,这么多元素杂在一起,纵然我使出毕生所学,却依然会有卡顿,达不到小师妹对流畅性的要求,所以很是苦恼,恳求大师指点。”

“原来是这样,老夫这就来助你突破瓶颈,更上一层楼。”

异步绘制

iOS性能优化(初级)iOS性能优化(中级)中,为了屏幕流畅我们做了很多,也取得了不错的成果。但无论怎么做,最后的绘制是提交给系统的,系统默认是在主线程做这一切,当需要绘制的元素过多,过于频繁,那么依然会造成卡顿。

那么我们可不可以像处理复杂数据一样,把绘制过程放在后台线程执行呢?

很高兴,答案是可以的。

iOS里面的视图UIView中有一个CALayer *layer的属性,UIView的内容,其实是layer显示的,layer中有一个属性id contentscontents的内容就是要显示的具体内容,大多数情况下,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];
绘制结果

显示效果达到。

“多谢大师指点,大师一番操作,让我茅塞顿开。”

耳目一新

上面的操作是非常常规的操作,在实际使用中还有几个问题需要解决:

  1. 当AsyncLabel使用在cell中,数量较多,不断重绘时,要处理好子线程问题,不能放在全局队列(因为全局队列中可能有系统提交的任务)。
  2. 对不同类型如文字、图片的封装性问题。

下面老夫来给少侠介绍一种,全新的解决方式,刷新常规想法,且封装优秀。

YYAsyncLayer

它的主要处理流程如下:

  1. 在主线程的runLoop中注册一个observer,它的优先级要比系统的CATransaction要低,保证系统先做完必须的工作。
  2. 把需要异步绘制的操作集中起来。比如设置字体、颜色、背景这些,不是设置一个就绘制一个,把他们都收集起来,runloop会在observer需要的时机通知统一处理。
  3. 处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给layer.contents
YYAsyncLayer主要流程

大概了解了原理,我们来使用一下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];
}

这一些代码,执行了处理流程中的12,注册了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,异步绘制,并提供给使用者willDisplaydisplaydidDisplay几个block。

有一点需要注意,必须重写+ (Class)layerClass,才会进入自定义的subLayer执行方法。相当于打UIView的layer,从默认layer指到subLayer。

绘制结果
驾轻就熟

上述招式,老夫只是简单演示,但少侠遇到的事要比老夫复杂的多。少侠天资聪慧,切不可傲娇,还需好生练习并配合runloopCoreText使用,方能驾轻就熟。快去答复小师妹去罢。

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