面试ios知识

界面优化解析

2020-12-07  本文已影响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,感兴趣的童鞋,可以去官方文档上下载源码,自行查看具体的实现流程。这里大致说一下其中的实现原理

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

三、卡顿的解决措施

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

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

3.1 预排版

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

如何避免这些损耗呢?

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

这些都可以专门自定义一个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 预解码 & 预渲染

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

那么,其中最重要的一步就是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:]方法,我们去到帮助文档搜索该方法👇

所以,我们在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里需要做的事情
@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();
}
+ (Class)layerClass{
    return [LGLayer class];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer{
    [super layoutSublayersOfLayer:layer];
    [self layoutSubviews];
}
//绘制的准备工作,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

交互事件处理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卡顿这一常见现象,首先分析了卡顿的原理,然后提供了几种卡顿监测的方案,最后重点提供了几种常用的方案去优化卡顿,包括预排版预解码 & 预渲染按需加载异步渲染

上一篇下一篇

猜你喜欢

热点阅读