界面优化解析

作者: 深圳_你要的昵称 | 来源:发表于2020-12-07 15:10 被阅读0次

    前言

    我们经常在面试中,会被问及关于界面优化相关的问题,比如为什么界面会出现卡顿?如何监控卡顿?接着如何解决卡顿?那么本篇文章将重点分析一下卡顿的原理解决的措施

    一、卡顿的原理

    我们先看看界面显示出来的一个完整流程,如下图👇

    CPU负责事务的计算,GPU负责渲染,将渲染的结果交给FrameBuffer帧缓冲区,帧缓冲区将处理的数据传递给UI层(Video 或 Conrtroller)UI层交给Monitor显示出来。

    问题1:GPU效率

    那么问题来了,帧缓冲区需要处理数据,那么就会有耗时,耗时长就会影响效率,如何解决这个效率问题呢?人们就提出了一个双帧缓冲区的概念,如下图👇

    双帧缓冲区,我们可以理解分为一个前帧区和一个后帧区,CPU首先在前帧区处理数据,处理完成后紧接着去后帧区处理数据,然后GPU先去前帧区取CPU第1次处理的数据,然后去后帧区取CPU第2次数据,CPU和GPU二者交替进行写和读,这样就腾出了一个缓冲的时间。

    问题2:变化因子影响

    但是又有问题,缓冲区内数据处理会受到外界环境因素的影响,例如当前手机在充电时,cpu发烫严重,就会导致数据处理变慢,这些外界因素可称之为变化因子变化因子的影响就会导致一个现象-->丢帧

    因为cpu这边的变化因子太多太多,即受外部环境的因素影响的可变性太多,所以一般不会将CPU作为优化的对象,转而我们一般将GPU这边规定一个固定的标准,是什么标准呢,例如:每秒渲染60帧图片进行显示,那么问题来了,GPU这边如何知道这个渲染的时间点呢?就是谁来通知GPU渲染去显示?答案就是显示器了,这时就又引入一个概念垂直同步信号VSync,显示器发送一个垂直同步信号VSync给monitor,monitor拿到该信号后,就去拿帧缓冲区的数据,开始渲染屏幕,显示画面,要是没有接收到该信号,就不显示。

    问题3:丢帧

    那么还是有问题,要是monitor接收到垂直同步信号后,去帧缓冲区拿数据,数据没有的话,如下图👇

    借用上图解释一下掉帧卡顿的情况(假设显示器在接收VSync信号时,只显示1帧画面)👇

    1. 正常不掉帧的情况下,在两个VSync信号之间CPU能处理完数据,GPU也能处理完数据,这样在下一个VSync信号来的时候,显示器(Display)能完整的拿到帧数据进行显示,如上图第1个VSync和第2个VSync之间,第1帧的画面显示正常。
    2. CPUGPU处理数据的时长超过了两个VSync信号之间的时长,如上图第2个VSync和第3个VSync之间,GPU处理时长(红色条)已经超过了第3个VSync的时间点,那么显示器(Display)在第三个VSync的时间点去取第2帧数据的时候,发现GPU那边没有数据,因为此时GPU还在处理数据,这时显示器(Display)仍然在显示第1帧的画面,然后接着继续往下走。
    3. 在第3个VSync和第4个VSync之间,CPUGPU在第四个VSync时间点之前,又都处理完了数据,和第1点的情况一样,那么显示器(Display)能拿到第3帧的数据显示了,那么此时显示器(Display)只显示了第1帧和第3帧,没有显示第2帧,于是第2帧就丢了,这就是丢帧

    VSync间隔时长

    再解释VSync之间的时间间隔,大约是16.67ms,怎么来的,之前我们说过,最完美的显示是每秒60帧图片,那么每一帧耗时就是1000/60 ≈ 16.67ms

    二、卡顿的监测

    2.1 CADisplayLink

    查看文档,搜索CADisplayLink,找到相关的VSync,说明CADisplayLink就是和显示器刷新频率一样的。我们再来看看YYKit中的YYFPSLabel,它就是封装了CADisplayLink,显示FPS。(每秒传输帧数(Frames Per Second))👇

    @implementation YYFPSLabel {
        CADisplayLink *_link;
        NSUInteger _count;
        NSTimeInterval _lastTime;
        UIFont *_font;
        UIFont *_subFont;
        
        NSTimeInterval _llll;
    }
    
    - (instancetype)initWithFrame:(CGRect)frame {
        if (frame.size.width == 0 && frame.size.height == 0) {
            frame.size = kSize;
        }
        self = [super initWithFrame:frame];
        
        self.layer.cornerRadius = 5;
        self.clipsToBounds = YES;
        self.textAlignment = NSTextAlignmentCenter;
        self.userInteractionEnabled = NO;
        self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
        
        _font = [UIFont fontWithName:@"Menlo" size:14];
        if (_font) {
            _subFont = [UIFont fontWithName:@"Menlo" size:4];
        } else {
            _font = [UIFont fontWithName:@"Courier" size:14];
            _subFont = [UIFont fontWithName:@"Courier" size:4];
        }
        
        _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
        [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        return self;
    }
    
    - (void)dealloc {
        [_link invalidate];
    }
    
    - (CGSize)sizeThatFits:(CGSize)size {
        return kSize;
    }
    
    - (void)tick:(CADisplayLink *)link {
        if (_lastTime == 0) {
            _lastTime = link.timestamp;
            return;
        }
        
        _count++;
        NSTimeInterval delta = link.timestamp - _lastTime;
        if (delta < 1) return;
        _lastTime = link.timestamp;
        float fps = _count / delta;
        _count = 0;
        
        CGFloat progress = fps / 60.0;
        UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
        
        NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
        [text setColor:color range:NSMakeRange(0, text.length - 3)];
        [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
        text.font = _font;
        [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
        
        self.attributedText = text;
    }
    
    @end
    

    2.2 RunLoop监控

    事务的卡顿,事务是交给RunLoop处理的,于是我们可以通过监控RunLoop,监控RunLoop的运行状态kCFRunLoopBeforeSources & kCFRunLoopAfterWaiting,判断事务是否处理完成。源码如下👇

    @interface LGBlockMonitor (){
        CFRunLoopActivity activity;
    }
    
    @property (nonatomic, strong) dispatch_semaphore_t semaphore;
    @property (nonatomic, assign) NSUInteger timeoutCount;
    
    @end
    
    @implementation LGBlockMonitor
    
    + (instancetype)sharedInstance {
        static id instance = nil;
        static dispatch_once_t onceToken;
        
        dispatch_once(&onceToken, ^{
            instance = [[self alloc] init];
        });
        return instance;
    }
    
    - (void)start{
        [self registerObserver];
        [self startMonitor];
    }
    
    static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
        monitor->activity = activity;
        // 发送信号
        dispatch_semaphore_t semaphore = monitor->_semaphore;
        dispatch_semaphore_signal(semaphore);
    }
    
    - (void)registerObserver{
        CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
        //NSIntegerMax : 优先级最小
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                kCFRunLoopAllActivities,
                                                                YES,
                                                                NSIntegerMax,
                                                                &CallBack,
                                                                &context);
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    }
    
    - (void)startMonitor{
        // 创建信号
        _semaphore = dispatch_semaphore_create(0);
        // 在子线程监控时长
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (YES)
            {
                // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
                long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
                if (st != 0)
                {
                    if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                    {
                        if (++self->_timeoutCount < 2){
                            NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                            continue;
                        }
                        // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
                        NSLog(@"检测到超过两次连续卡顿");
                    }
                }
                self->_timeoutCount = 0;
            }
        });
    }
    

    2.3 其它卡顿监测方案

    微信matrix滴滴DoraemonKit,感兴趣的童鞋,可以去官方文档上下载源码,自行查看具体的实现流程。这里大致说一下其中的实现原理

    • 微信的方案:和上面讲的RunLoop监控的原理一样的,不多做说明。
    • 滴滴的方案:是一种轮询的机制,每隔一段时间主动去查询主线程的状态,如果在你的设置的阈值时间内,没有返回信息,那么就能判定卡顿,如下图👇

    worker工作线程每隔一小段时间(delta) ping一下主线程,如果主线程此时有空,必然能接收到这个通知,并pong一下(发送另一个NSNotification),如果worker线程超过delta时间没有收到pong的回复,那么可以推测UI线程必然在处理其他任务了,此时我们暂停UI线程,并打印出当前UI线程的函数调用栈

    三、卡顿的解决措施

    我们在面试过程中,经常会被问此类的问题,我们一般的回答就是UITableView的复用啊,CellRowHeight的计算啊,切圆角的处理优化啊,这些都是表明的应用,很难回答到点上,今天我们将在以下几点分析下卡顿的解决方案:

    1. 预排版
    2. 预解码 & 预渲染
    3. 按需加载
    4. 异步渲染

    3.1 预排版

    以网络请求为案例,一般的网络请求,我们获取到response后,解析完数据后直接回调给主线程刷新TableView,那么主线程上就得计算cell子控件的布局,cell的高低等,这样比较损耗GPU的性能

    如何避免这些损耗呢?

    基于这些损耗问题,我们可以单独在一个预排版的子线程去做一些事情:

    • frame的计算
    • 控件层级的部署
    • 渲染所需数据的处理
    • Model模型的数据解析等

    这些都可以专门自定义一个layout类去处理,然后主线程上直接使用这些已经处理好layout的数据,这样可以减少很多不必要的数据处理(麻烦),这就是预排版的作用

    案例演示

    例如我们要实现这样一个UITableView,首先我们先分析cell的布局,cell包含的子控件有:

    1. 头像
    2. 昵称Label
    3. 内容Label
    4. 收起/展开 Button
    5. 图片(注意:也可以是多张图)
    6. 分割view(底部灰色部分)

    然后,根据对这个cell子控件的分析,可以定义一个layout类👇

    接着看看cell类的定义👇

    然后扩展类里的子控件的定义👇

    继续看看configureLayout方法具体做了什么👇

    很简单,一些frame的赋值,图片的赋值,圆角的处理,文案的赋值。根本没有任何数据的计算,全部是赋值。那所有的数据的预处理交给谁去做呢?--> 当然是layout类👇

    - (instancetype)initWithModel:(LGTimeLineModel *)timeLineModel{
        if (!timeLineModel) return nil;
        self = [super init];
        if (self) {
            _timeLineModel = timeLineModel;
            [self layout];
        }
        return self;
    }
    
    - (void)setTimeLineModel:(LGTimeLineModel *)timeLineModel{
        _timeLineModel = timeLineModel;
        [self layout];
    }
    
    - (void)layout{
    
        CGFloat sWidth = [UIScreen mainScreen].bounds.size.width;
    
        self.iconRect = CGRectMake(10, 10, 45, 45);
        CGFloat nameWidth = [self calcWidthWithTitle:_timeLineModel.name font:titleFont];
        CGFloat nameHeight = [self calcLabelHeight:_timeLineModel.name fontSize:titleFont width:nameWidth];
        self.nameRect = CGRectMake(CGRectGetMaxX(self.iconRect) + nameLeftSpaceToHeadIcon, 17, nameWidth, nameHeight);
    
        CGFloat msgWidth = sWidth - 10 - 16;
        CGFloat msgHeight = 0;
    
        //文本信息高度计算
        NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        [paragraphStyle setLineSpacing:5];
        NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:msgFont],
                                     NSForegroundColorAttributeName: [UIColor colorWithRed:26/255.0 green:26/255.0 blue:26/255.0 alpha:1]
                                     ,NSParagraphStyleAttributeName: paragraphStyle
                                     ,NSKernAttributeName:@0
                                     };
        NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:_timeLineModel.msgContent attributes:attributes];
        msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
    
    
        if (attrStr.length > msgExpandLimitHeight) {
            if (_timeLineModel.isExpand) {
                self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
            } else {
                attrStr = [[NSMutableAttributedString alloc] initWithString:[_timeLineModel.msgContent substringToIndex:msgExpandLimitHeight] attributes:attributes];
                msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
                self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
            }
        } else {
            self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
        }
    
        if (attrStr.length < msgExpandLimitHeight) {
            self.expandHidden = YES;
            self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) - 20, 30, 20);
        } else {
            self.expandHidden = NO;
            self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) + 10, 30, 20);
        }
        
        
        CGFloat timeWidth = [self calcWidthWithTitle:_timeLineModel.time font:timeAndLocationFont];
        CGFloat timeHeight = [self calcLabelHeight:_timeLineModel.time fontSize:timeAndLocationFont width:timeWidth];
        self.imageRects = [NSMutableArray array];
        if (_timeLineModel.contentImages.count == 0) {
    //        self.timeRect = CGRectMake(10, CGRectGetMaxY(self.expandRect) + 10, timeWidth, timeHeight);
        } else {
            if (_timeLineModel.contentImages.count == 1) {
                CGRect imageRect = CGRectMake(11, CGRectGetMaxY(self.expandRect) + 10, 250, 150);
                [self.imageRects addObject:@(imageRect)];
            } else if (_timeLineModel.contentImages.count == 2 || _timeLineModel.contentImages.count == 3) {
                for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
                    CGRect imageRect = CGRectMake(11 + i * (10 + 90), CGRectGetMaxY(self.expandRect) + 10, 90, 90);
                    [self.imageRects addObject:@(imageRect)];
                }
            } else if (_timeLineModel.contentImages.count == 4) {
                for (int i = 0; i < 2; i++) {
                    for (int j = 0; j < 2; j++) {
                        CGRect imageRect = CGRectMake(11 + j * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + i * (10 + 90), 90, 90);
                        [self.imageRects addObject:@(imageRect)];
                    }
                }
            } else if (_timeLineModel.contentImages.count == 5 || _timeLineModel.contentImages.count == 6 || _timeLineModel.contentImages.count == 7 || _timeLineModel.contentImages.count == 8 || _timeLineModel.contentImages.count == 9) {
                for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
                    CGRect imageRect = CGRectMake(11 + (i % 3) * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + (i / 3) * (10 + 90), 90, 90);
                    [self.imageRects addObject:@(imageRect)];
                }
            }
        }
    
        if (self.imageRects.count > 0) {
            CGRect lastRect = [self.imageRects[self.imageRects.count - 1] CGRectValue];
            self.seperatorViewRect = CGRectMake(0, CGRectGetMaxY(lastRect) + 10, sWidth, 15);
        }
        
        self.height = CGRectGetMaxY(self.seperatorViewRect);
    }
    
    #pragma mark -- Caculate Method
    
    - (CGFloat)calcWidthWithTitle:(NSString *)title font:(CGFloat)font {
        NSStringDrawingOptions options =  NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
        CGRect rect = [title boundingRectWithSize:CGSizeMake(MAXFLOAT,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:font]} context:nil];
        
        CGFloat realWidth = ceilf(rect.size.width);
        return realWidth;
        
    }
    
    - (CGFloat)calcLabelHeight:(NSString *)str fontSize:(CGFloat)fontSize width:(CGFloat)width {
        NSStringDrawingOptions options =  NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
        
        CGRect rect = [str boundingRectWithSize:CGSizeMake(width,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]} context:nil];
        
        CGFloat realHeight = ceilf(rect.size.height);
        return realHeight;
    }
    
    - (int)caculateAttributeLabelHeightWithString:(NSAttributedString *)string  width:(int)width {
        int total_height = 0;
        
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);    //string 为要计算高度的NSAttributedString
        CGRect drawingRect = CGRectMake(0, 0, width, 100000);  //这里的高要设置足够大
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, drawingRect);
        CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL);
        CGPathRelease(path);
        CFRelease(framesetter);
        
        NSArray *linesArray = (NSArray *) CTFrameGetLines(textFrame);
        
        CGPoint origins[[linesArray count]];
        CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
        
        int line_y = (int) origins[[linesArray count] -1].y;  //最后一行line的原点y坐标
        
        CGFloat ascent;
        CGFloat descent;
        CGFloat leading;
        
        CTLineRef line = (__bridge CTLineRef) [linesArray objectAtIndex:[linesArray count]-1];
        CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        
        total_height = 100000 - line_y + (int) descent +1;    //+1为了纠正descent转换成int小数点后舍去的值
        
        CFRelease(textFrame);
        
        return total_height;
    }
    

    设置数据模型LGTimeLineModel的同时,就去计算布局[self layout]

    最后看看调用方,一般是ViewController,是如何使用layout的?👇

    上图可见,tableView是直接拿着layout数据直接使用的,很简单。

    3.2 预解码 & 预渲染

    除了上面讲的预排版可以解决部分性能问题外,当我们碰到一些需要解码处理的数据时,该怎么办呢?例如:读取二进制流数据,生成一张图片,再渲染显示出来。

    • DataBuffer就是二进制流数据,必须通过decode解码生成像素data。
    • 像素data存储在ImageBuffer像素缓冲区中
    • 然后帧缓冲区FrameBuffer拿到数据后,就去渲染显示

    那么,其中最重要的一步就是decode解码,这个解码非常消耗性能

    iOS中视图展示的完整流程
    1. Layout:Layout 阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。
    2. Display:Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。
    3. Prepare:Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。
    4. Commit:Commit 阶段主要将图层进行打包,并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在。

    那么decode解码就在prepare预备这一阶段,这个解码依赖苹果底层的一个插件库:图形编解码插件

    案例演示

    接下来我们以SDWebImage为例,看看其核心的图像从download到加载的一个大致完整过程。
    首先来到SDWebImageDownloaderOperation.m中,我们知道,SDWebImage中的下载是依赖网络底层URLSession的,其Delegate方法URLSession:dataTask:didReceiveData:是接收二进制流数据👇

    接下来,我们看看解码的流程SDImageLoaderDecodeProgressiveImageData👇

    最终我们来到decodedImageWithImage,看看SDWebImage中如何进行渲染的同时再做内存缓存的👇

    渲染的核心代码👇(这个就不详细说明了,可以直接拿来使用的)

    + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
        return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
    }
    
    + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
        if (!cgImage) {
            return NULL;
        }
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        if (width == 0 || height == 0) return NULL;
        size_t newWidth;
        size_t newHeight;
        switch (orientation) {
            case kCGImagePropertyOrientationLeft:
            case kCGImagePropertyOrientationLeftMirrored:
            case kCGImagePropertyOrientationRight:
            case kCGImagePropertyOrientationRightMirrored: {
                // These orientation should swap width & height
                newWidth = height;
                newHeight = width;
            }
                break;
            default: {
                newWidth = width;
                newHeight = height;
            }
                break;
        }
        
        BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
        // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
        // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
        // But since our build-in coders use this bitmapInfo, this can have a little performance benefit
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
        if (!context) {
            return NULL;
        }
        
        // Apply transform
        CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
        CGContextConcatCTM(context, transform);
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
        CGImageRef newImageRef = CGBitmapContextCreateImage(context);
        CGContextRelease(context);
        
        return newImageRef;
    }
    

    从SDWebImage处理下载和解码图片的流程中,我们知道了在下载图片的数据接收delegate方法中,新开辟了子线程去处理解码图片,在子线程上做解码做渲染,不会影响主线程的交互,可以降低主线程runloop执行任务的复杂度,提高性能,这就是预解码预渲染!解码的同时又以关联属性的方式进行内存缓存,这种手法十分巧妙,值得借鉴!

    setImage为何影响性能

    我们可以通过案例演示给大家看:给UIImageView设置一张本地的图片文件(图片尽可能大些,可超过20M),测试代码如下👇

    - (void)testImageView {
        NSData *data = [NSData dataWithContentsOfFile:@"/Users/Aron/Desktop/RunLoop.png"];
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // 压缩过后的二进制数据
            UIImage *image = [UIImage imageWithData:data];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                // 解码还是在主线程上
                self.imageView.image = image;
            });
        });
    }
    

    代码很简单:首先我们读取一张本地的图片,然后开启子线程 将图片文件的二进制数据转换成image,再切回主线程,赋值给imageView.image显示出来。

    接着,我们运行代码,然后打开Instrument里的Time Profile工具👇

    再选择你的项目,选择模拟器运行,点击开始👇

    接着查看主线程👇

    上图可知,我们平时的imageView.image赋值操作中,系统对图形的解码及渲染操作,全是在主线程执行的,这就是影响性能的根本原因。

    3.3 按需加载

    除了上面说的预排版预解码和预渲染以外,我们比较常用的还有按需加载,这个实现起来也不难。现在以UITableView为例,实现一个按需加载的流程。UITableView在滚动过程中会不断的加载cell,虽然系统会对cell进行复用,但是假如你的cell布局很复杂,有很多图片得显示,那么势必会出现卡顿,此时我们可以利用按需加载,其核心的思路就是👇

    在UITableView的滚动过程中,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行进行预先的加载,且在用户触摸时第一时间加载内容。

    示例演示

    接下来,我们看看是如何实现的👇
    首先,看看自定义的UITableView源码,先看.h👇

    再看.m👇


    接着看看监控TableView的滚动,是如何处理按需加载的

    完整.m代码

    @implementation VVeboTableView{
        NSMutableArray *datas;
        NSMutableArray *needLoadArr;
        BOOL scrollToToping;
    }
    
    - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style{
        self = [super initWithFrame:frame style:style];
        if (self) {
            self.separatorStyle = UITableViewCellSeparatorStyleNone;
            self.dataSource = self;
            self.delegate = self;
            datas = [[NSMutableArray alloc] init];
            needLoadArr = [[NSMutableArray alloc] init];
            
            [self loadData];
            [self reloadData];
        }
        return self;
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
        return datas.count;
    }
    
    - (NSInteger)numberOfSections{
        return 1;
    }
    
    - (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
        NSDictionary *data = [datas objectAtIndex:indexPath.row];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        [cell clear];
        cell.data = data;
        if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
            [cell clear];
            return;
        }
        if (scrollToToping) {
            return;
        }
        [cell draw];
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
        VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell==nil) {
            cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                             reuseIdentifier:@"cell"];
        }
        [self drawCell:cell withIndexPath:indexPath];
        return cell;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
        NSDictionary *dict = datas[indexPath.row];
        float height = [dict[@"frame"] CGRectValue].size.height;
        return height;
    }
    
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
        [needLoadArr removeAllObjects];
    }
    
    //按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
        NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
        NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
        NSInteger skipCount = 8;
        if (labs(cip.row-ip.row)>skipCount) {
            NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
            NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
            if (velocity.y<0) {
                NSIndexPath *indexPath = [temp lastObject];
                if (indexPath.row+3<datas.count) {
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
                }
            } else {
                NSIndexPath *indexPath = [temp firstObject];
                if (indexPath.row>3) {
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                    [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
                }
            }
            [needLoadArr addObjectsFromArray:arr];
        }
    }
    
    - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
        scrollToToping = YES;
        return YES;
    }
    
    - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
        scrollToToping = NO;
        [self loadContent];
    }
    
    - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
        scrollToToping = NO;
        [self loadContent];
    }
    
    //用户触摸时第一时间加载内容
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        if (!scrollToToping) {
            [needLoadArr removeAllObjects];
            [self loadContent];
        }
        return [super hitTest:point withEvent:event];
    }
    
    - (void)loadContent{
        if (scrollToToping) {
            return;
        }
        if (self.indexPathsForVisibleRows.count<=0) {
            return;
        }
        if (self.visibleCells&&self.visibleCells.count>0) {
            for (id temp in [self.visibleCells copy]) {
                VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
                [cell draw];
            }
        }
    }
    
    //读取信息
    - (void)loadData{
        NSArray *temp = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"data" ofType:@"plist"]];
        for (NSDictionary *dict in temp) {
            NSDictionary *user = dict[@"user"];
            NSMutableDictionary *data = [NSMutableDictionary dictionary];
            data[@"avatarUrl"] = user[@"avatar_large"];
            data[@"name"] = user[@"screen_name"];
            NSString *from = [dict valueForKey:@"source"];
            if (from.length>6) {
                NSInteger start = [from indexOf:@"\">"]+2;
                NSInteger end = [from indexOf:@"</a>"];
                from = [from substringFromIndex:start toIndex:end];
            } else {
                from = @"未知";
            }
            data[@"time"] = @"2015-05-25";
            data[@"from"] = from;
            [self setCommentsFrom:dict toData:data];
            [self setRepostsFrom:dict toData:data];
            data[@"text"] = dict[@"text"];
            
            NSDictionary *retweet = [dict valueForKey:@"retweeted_status"];
            if (retweet) {
                NSMutableDictionary *subData = [NSMutableDictionary dictionary];
                NSDictionary *user = retweet[@"user"];
                subData[@"avatarUrl"] = user[@"avatar_large"];
                subData[@"name"] = user[@"screen_name"];
                subData[@"text"] = [NSString stringWithFormat:@"@%@: %@", subData[@"name"], retweet[@"text"]];
                [self setPicUrlsFrom:retweet toData:subData];
                
                {
                    float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
                    CGSize size = [subData[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_SUBCONTENT) lineSpace:5];
                    NSInteger sizeHeight = (size.height+.5);
                    subData[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_BIG, width, sizeHeight)];
                    sizeHeight += SIZE_GAP_BIG;
                    if (subData[@"pic_urls"] && [subData[@"pic_urls"] count]>0) {
                        sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
                    }
                    sizeHeight += SIZE_GAP_BIG;
                    subData[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
                }
                data[@"subData"] = subData;
            } else {
                [self setPicUrlsFrom:dict toData:data];
            }
            
            {
                float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
                CGSize size = [data[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_CONTENT) lineSpace:5];
                NSInteger sizeHeight = (size.height+.5);
                data[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG, width, sizeHeight)];
                sizeHeight += SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG;
                if (data[@"pic_urls"] && [data[@"pic_urls"] count]>0) {
                    sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
                }
                
                NSMutableDictionary *subData = [data valueForKey:@"subData"];
                if (subData) {
                    sizeHeight += SIZE_GAP_BIG;
                    CGRect frame = [subData[@"frame"] CGRectValue];
                    CGRect textRect = [subData[@"textRect"] CGRectValue];
                    frame.origin.y = sizeHeight;
                    subData[@"frame"] = [NSValue valueWithCGRect:frame];
                    textRect.origin.y = frame.origin.y+SIZE_GAP_BIG;
                    subData[@"textRect"] = [NSValue valueWithCGRect:textRect];
                    sizeHeight += frame.size.height;
                    data[@"subData"] = subData;
                }
                
                sizeHeight += 30;
                data[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
            }
            
            [datas addObject:data];
        }
    }
    
    - (void)setCommentsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
        NSInteger comments = [dict[@"reposts_count"] integerValue];
        if (comments>=10000) {
            data[@"reposts"] = [NSString stringWithFormat:@"  %.1fw", comments/10000.0];
        } else {
            if (comments>0) {
                data[@"reposts"] = [NSString stringWithFormat:@"  %ld", (long)comments];
            } else {
                data[@"reposts"] = @"";
            }
        }
    }
    
    - (void)setRepostsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
        NSInteger comments = [dict[@"comments_count"] integerValue];
        if (comments>=10000) {
            data[@"comments"] = [NSString stringWithFormat:@"  %.1fw", comments/10000.0];
        } else {
            if (comments>0) {
                data[@"comments"] = [NSString stringWithFormat:@"  %ld", (long)comments];
            } else {
                data[@"comments"] = @"";
            }
        }
    }
    
    - (void)setPicUrlsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
        NSArray *pic_urls = [dict valueForKey:@"pic_urls"];
        NSString *url = [dict valueForKey:@"thumbnail_pic"];
        NSArray *pic_ids = [dict valueForKey:@"pic_ids"];
        if (pic_ids && pic_ids.count>1) {
            NSString *typeStr = @"jpg";
            if (pic_ids.count>0||url.length>0) {
                typeStr = [url substringFromIndex:url.length-3];
            }
            NSMutableArray *temp = [NSMutableArray array];
            for (NSString *pic_url in pic_ids) {
                [temp addObject:@{@"thumbnail_pic": [NSString stringWithFormat:@"http://ww2.sinaimg.cn/thumbnail/%@.%@", pic_url, typeStr]}];
            }
            data[@"pic_urls"] = temp;
        } else {
            data[@"pic_urls"] = pic_urls;
        }
    }
    
    - (void)removeFromSuperview{
        for (UIView *temp in self.subviews) {
            for (VVeboTableViewCell *cell in temp.subviews) {
                if ([cell isKindOfClass:[VVeboTableViewCell class]]) {
                    [cell releaseMemory];
                }
            }
        }
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        [datas removeAllObjects];
        datas = nil;
        [self reloadData];
        self.delegate = nil;
        [needLoadArr removeAllObjects];
        needLoadArr = nil;
        [super removeFromSuperview];
    }
    
    @end
    

    3.4 异步渲染

    如果上面的所有UI优化的方式都没法解决你的卡顿问题,那么接下来就讲一个终极大招:异步渲染

    3.4.1异步渲染的原理

    这个异步渲染方式,可能在平时的开发中极少会用到,虽然使用率低,但是我们也要掌握异步渲染的原理。我们通过一个面试题来切入👇

    UIView 和CALayer之间是什么关系?

    答案相信大家都知道,大致有以下几点:

    1. view通过layer驱动来完成显示
    2. view可以交互,而layer不可以
    3. view负责内容的管理,而layer负责内容的绘制
    4. view是layer的代理delegate

    接着我们自定义一个View:LGView,在drawRect:方法中打断点,bt查看其调用栈是什么样的?👇

    调用栈信息里从下至上,依次调用了
    [CALayer _display] --> [CALayer drawInContext:] --> [UIView(CALayerDelegate) drawLayer:inContext:] --> [LGView drawRect:]

    伪代码模拟CALayer 和 UIView的交互过程

    根据上面的调用栈信息,我们伪代码模拟一下这个过程👇

    1. 首先定义一个CALayer子类:LGLayer,主要是覆写display方法
    @interface LGLayer : CALayer
    
    @end
    
    

    我们从调用栈信息中,查看display中调用的是[CALayer drawInContext:]方法,我们去到帮助文档搜索该方法👇

    • 方法的入参是CGContextRef类型
    • layer的delegate(即UIView,也就是我们的LGView)需要实现drawLayer:inContext方法

    所以,我们在LGLayer的display方法的代码大致是这样👇

    #import "LGLayer.h"
    
    @implementation LGLayer
    
    //前面断点调用写下的代码
    - (void)layoutSublayers{
        if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
            //UIView
            [self.delegate layoutSublayersOfLayer:self];
        }else{
            [super layoutSublayers];
        }
    }
    
    
    //绘制流程的发起函数
    - (void)display{
        
        // 创建CGContextRef上下文对象,给drawInContext:使用
        CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
        
        // 告知UIView即将展示Layer
        [self.delegate layerWillDraw:self];
        
        // Layer开始绘制内容
        [self drawInContext:context];
        
        // 告知UIView展示Layer
        [self.delegate displayLayer:self];
    
        // 关闭CGContextRef上下文对象
        [self.delegate performSelector:@selector(closeContext)];
    }
    
    @end
    
    1. 再来看看LGView里需要做的事情
    • 因为LGView需要Layer调用关于上下文对象CGContextRef创建关闭,所以在头文件中定义一下2个方法👇
    @interface LGView : UIView
    
    - (CGContextRef)createContext;
    
    - (void)closeContext;
    
    @end
    

    然后方法的实现👇(我们只是简单的做一个图像的展示,所以采用ImageContext)

    - (CGContextRef)createContext{
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        return context;
    }
    
    - (void)closeContext{
        UIGraphicsEndImageContext();
    }
    
    • LGView指定自定义的CALayer👇
    + (Class)layerClass{
        return [LGLayer class];
    }
    
    • 因为需要将UIView和CALayer关联起来,所有需实现一下layoutSublayersOfLayer方法
    - (void)layoutSublayersOfLayer:(CALayer *)layer{
        [super layoutSublayersOfLayer:layer];
        [self layoutSubviews];
    }
    
    • UIView中还需实现关于Layer的绘制相关的方法:
    //绘制的准备工作,do nontihing
    - (void)layerWillDraw:(CALayer *)layer{
        
    }
    
    // 绘制的操作
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
        [super drawLayer:layer inContext:ctx];
        [[UIColor redColor] set];
        // Core Graphics
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)];
        CGContextAddPath(ctx, path.CGPath);
        CGContextFillPath(ctx);
    }
    
    // layer.contents = (位图)
    - (void)displayLayer:(CALayer *)layer{
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)(image.CGImage);
        });
    }
    

    那么,LGView.m的完整版代码👇

    #import "LGView.h"
    #import "LGLayer.h"
    
    @implementation LGView
    
    
    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    - (void)drawRect:(CGRect)rect {
        // Drawing code, 绘制的操作, BackingStore(额外的存储区域产于的) -- GPU
    }
    
    
    + (Class)layerClass{
        return [LGLayer class];
    }
    
    - (void)layoutSublayersOfLayer:(CALayer *)layer{
        [super layoutSublayersOfLayer:layer];
        [self layoutSubviews];
    }
    
    - (CGContextRef)createContext{
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        return context;
    }
    
    //绘制的准备工作,do nontihing
    - (void)layerWillDraw:(CALayer *)layer{
        
    }
    
    //绘制的操作
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
        [super drawLayer:layer inContext:ctx];
        [[UIColor redColor] set];
        // Core Graphics
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)];
        CGContextAddPath(ctx, path.CGPath);
        CGContextFillPath(ctx);
    }
    
    // layer.contents = (位图)
    - (void)displayLayer:(CALayer *)layer{
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)(image.CGImage);
        });
    }
    
    - (void)closeContext{
        UIGraphicsEndImageContext();
    }
    
    @end
    
    1. run👇

    从以上的案例可以知道,我们完全能实现CALayer和UIView之间的剥离LGView就好比消费者LGLayer生产者LGView收集我们需要绘制的内容(包含CGContextRef的创建与关闭),而LGLayer负责渲染(也就是display),渲染又很耗时,这样我们就有了一个思路 --> 实现一个异步渲染的功能,如果将渲染工作交给一个异步子线程处理,就能减轻GPU的负担了。

    3.4.2 异步渲染案例演示

    我们以Graver为例,看看它是怎么实现异步渲染的流程的👇

    ps:Graver2年多时间没有更新了,我们只是看看它的实现的思路和原理。

    1. 首先主队列发起网络请求,请求页面需要展示的数据,这个网络请求是交给网络队列子线程进行
    2. 接收到请求Response后,再交给排版队列子线程,实现预排版,再将排版后的数据丢给主队列
    3. 主队列中UIView接收到了排版数据,再交给CALayer,CALayer开启绘制队列子线程渲染生成位图丢给主队列
    4. 主队列接收位图并展示
    最终效果

    Graver源码的Demo中,运行起来的最终效果👇

    可以看到,cell里的布局就一个层级,一张图,这样展示起来会卡顿吗?当然不会!那么,这是怎样实现的呢?既然是一张图,它又是如何区分cell上子控件的交互事件呢?带着这两个疑问,我们逐一看看Graver异步渲染交互事件处理的代码流程。

    渲染流程

    首先我们来到WMGAsyncDrawView,这是负责异步渲染的类,就好比我们之前定义的LGView

    其中有两个重要的属性:BOOL contentsChangedAfterLastAsyncDrawingdispatch_queue_t dispatchDrawQueue标识是否异步绘制异步绘制的队列(由调用方指定的)。

    + (Class)layerClass
    {
        return [WMGAsyncDrawLayer class];
    }
    

    WMGAsyncDrawView的layer是类WMGAsyncDrawLayer。接着我们看看displayLayer:方法中,WMGAsyncDrawView是如何调用WMGAsyncDrawLayer去异步绘制的👇

    接着来到私有方法_displayLayer中👇

    //异步线程当中操作的~
    - (void)_displayLayer:(WMGAsyncDrawLayer *)layer
                     rect:(CGRect)rectToDraw
           drawingStarted:(WMGAsyncDrawCallback)startCallback
          drawingFinished:(WMGAsyncDrawCallback)finishCallback
       drawingInterrupted:(WMGAsyncDrawCallback)interruptCallback
    {
        BOOL drawInBackground = layer.isAsyncDrawsCurrentContent && ![[self class] globalAsyncDrawingDisabled];
        
        [layer increaseDrawingCount]; //计数器,标识当前的绘制任务
        
        NSUInteger targetDrawingCount = layer.drawingCount;
        
        NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
        
        //Core Graphic & Core Text
        void (^drawBlock)(void) = ^{
            
            void (^failedBlock)(void) = ^{
                if (interruptCallback)
                {
                    interruptCallback(drawInBackground);
                }
            };
            
            //不一致,进入下一个绘制任务
            if (layer.drawingCount != targetDrawingCount)
            {
                failedBlock();
                return;
            }
            
            CGSize contextSize = layer.bounds.size;
            BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
            CGContextRef context = NULL;
            BOOL drawingFinished = YES;
            
            if (contextSizeValid) {
                UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
                
                context = UIGraphicsGetCurrentContext();
                
                if (!context) {
                    WMGLog(@"may be memory warning");
                }
                
        
                CGContextSaveGState(context);
                
                if (rectToDraw.origin.x || rectToDraw.origin.y)
                {
                    CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
                }
                
                if (layer.drawingCount != targetDrawingCount)
                {
                    drawingFinished = NO;
                }
                else
                {
                    //子类去完成啊~父类的基本行为来说~YES
                    drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
                }
                
                CGContextRestoreGState(context);
            }
            
            // 所有耗时的操作都已完成,但仅在绘制过程中未发生重绘时,将结果显示出来
            if (drawingFinished && targetDrawingCount == layer.drawingCount)
            {
                CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
                
                {
                    // 让 UIImage 进行内存管理
                    UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
                    
                    void (^finishBlock)(void) = ^{
                        
                        // 由于block可能在下一runloop执行,再进行一次检查
                        if (targetDrawingCount != layer.drawingCount)
                        {
                            failedBlock();
                            return;
                        }
                        
                        //赋值的操作~
                        layer.contents = (id)image.CGImage;
                        
                        [layer setContentsChangedAfterLastAsyncDrawing:NO];
                        [layer setReserveContentsBeforeNextDrawingComplete:NO];
                        if (finishCallback)
                        {
                            finishCallback(drawInBackground);
                        }
                        
                        // 如果当前是异步绘制,且设置了有效fadeDuration,则执行动画
                        if (drawInBackground && layer.fadeDuration > 0.0001)
                        {
                            layer.opacity = 0.0;
                            
                            [UIView animateWithDuration:layer.fadeDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
                                layer.opacity = 1.0;
                            } completion:NULL];
                        }
                    };
                    
                    if (drawInBackground)
                    {
                        dispatch_async(dispatch_get_main_queue(), finishBlock);
                    }
                    else
                    {
                        finishBlock();
                    }
                }
                
                if (CGImage) {
                    CGImageRelease(CGImage);
                }
            }
            else
            {
                failedBlock();
            }
            
            UIGraphicsEndImageContext();
        };
        
        if (startCallback)
        {
            startCallback(drawInBackground);
        }
        
        if (drawInBackground)
        {
            // 清空 layer 的显示
            if (!layer.reserveContentsBeforeNextDrawingComplete)
            {
                layer.contents = nil;
            }
            
            //[self drawQueue] 异步绘制队列,绘制任务
            dispatch_async([self drawQueue], drawBlock);
        }
        else
        {
            void (^block)(void) = ^{
                //
                @autoreleasepool {
                    drawBlock();
                }
            };
            
            if ([NSThread isMainThread])
            {
                // 已经在主线程,直接执行绘制
                block();
            }
            else
            {
                // 不应当在其他线程,转到主线程绘制
                dispatch_async(dispatch_get_main_queue(), block);
            }
        }
    }
    

    其中最最核心的一句代码就是dispatch_async([self drawQueue], drawBlock);通过异步在绘制队列中执行layer的渲染👇

    1. 准备上下问对象👇
    UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
                
    context = UIGraphicsGetCurrentContext();
    
    1. 交由子类进行绘制👇
    drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
    
    1. 绘制完毕,则转成位图👇
    CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
    

    再生产UIImage

    UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
    
    1. 绘制完成交给主队列
    dispatch_async(dispatch_get_main_queue(), finishBlock);
    

    注意:CGImageRef用完之后需要释放CGImageRelease(CGImage);

    1. 关闭上下文
    UIGraphicsEndImageContext();
    

    这个渲染的流程,和LGView的基本上一样。其中,第2步交由子类完成绘制,视图类从父到子主要为 WMGAsynceDrawViewWMGCanvasViewWMGMixedView

    • WMGCanvasView 继承自 WMGAsyncDrawView, 主要负责圆角,边框,阴影和背景图片的绘制,绘制通过 CoreGraphics API 。

    • WMGMixedView 则是上层视图,属性仅有水平/垂直对齐方式,行数和绘制内容 attributedItem 。drawInRect 中则根据对齐方式来决定绘制文字位置, 然后调用 textDrawer 来进行文字渲染,如果其中有图片则会读取后直接通过 drawInRect: 方法来渲染图片(通过 TextDrawer 的 delegate)。

    交互事件处理WMGCanvasControl

    WMGCanvasControl继承自 WMGCanvasView,在这层处理事件响应,自定义实现了一套 Target-Action 模式,重写了 touchesBegin/Moved/Cancelled/Moved 一系列方法,来进行响应状态决议,然后将事件发给缓存的 targets 对象看能否响应指定的 control events

    - (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
    {
        for(__WMGCanvasControlTargetAction *t in [self _targetActions])
        {
            if(t.controlEvents == controlEvents)
            {
                if(t.target && t.action)
                {
                    [self sendAction:t.action to:t.target forEvent:nil];
                }
            }
        }
    }
    

    其中__WMGCanvasControlTargetAction类定义如下👇

    @interface __WMGCanvasControlTargetAction : NSObject
    
    @property (nonatomic, weak) id target;
    @property (nonatomic, assign) SEL action;
    @property (nonatomic, assign) UIControlEvents controlEvents;
    
    @end
    

    然后对外提供方法,并用数组去管理,对target-action的增、删、查👇

    - (NSMutableArray *)_targetActions
    {
        if(!_targetActions)
            _targetActions = [[NSMutableArray alloc] init];
        return _targetActions;
    }
    

    总结

    本篇文章针对UI卡顿这一常见现象,首先分析了卡顿的原理,然后提供了几种卡顿监测的方案,最后重点提供了几种常用的方案去优化卡顿,包括预排版预解码 & 预渲染按需加载异步渲染

    相关文章

      网友评论

        本文标题:界面优化解析

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