美文网首页iOS 功能类ios基础iOS 性能优化
iOS性能优化(中级+): 异步绘制

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

作者: 王大妈啊 | 来源:发表于2018-11-29 11:39 被阅读109次
    山雨欲来

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

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

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

    “大师之前授与我的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使用,方能驾轻就熟。快去答复小师妹去罢。

    相关文章

      网友评论

        本文标题:iOS性能优化(中级+): 异步绘制

        本文链接:https://www.haomeiwen.com/subject/ggbxcqtx.html