界面优化

卡顿的原理

想要进行界面优化,首先就要了解怎么产生卡顿
通常来说计算机中的显示过程是下面这样的,通过CPUGPU显示器协同工作来将图片显示到屏幕上

图像显示过程
  • CPU计算好显示内容,提交至GPU
  • GPU经过渲染完成后将渲染的结果放入FrameBuffer(帧缓存区)
  • 随后视频控制器会按照VSync垂直信号逐行读取FrameBuffer的数据
  • 经过可能的数模转换传递给显示器进行显示

最开始时FrameBuffer只有一个,这种情况下FrameBuffer的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区双缓冲机制。在这种情况下,GPU会预先渲染好一帧放入FrameBuffer,让视频控制器读取。当下一帧渲染好后,GPU会直接将视频控制器的指针指向第二个FrameBuffer

双缓存机制解决了效率问题,但随之而来的是新的问题。比如当前这一帧处理比较慢,GPU会将视频控制器的指针指向第二个FrameBuffer,那么上一帧的图像处理就会丢掉即掉帧。现象就是屏幕出现跳屏卡顿

屏幕卡顿原因

VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知AppApp主线程开始在CPU中计算显示内容,随后CPU 会将计算好的内容提交到GPU,由GPU进行变换、合成、渲染。随后GPU会把渲染结果提交到帧缓冲区,等待下一次VSync信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个VSync时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变

如下图显示过程,第1帧在VSync到来前,处理完成,正常显示,第2帧在VSync到来后,仍在处理中,此时屏幕不刷新依旧显示第1帧,此时就出现了掉帧情况,渲染时就会出现明显的卡顿现象

掉帧图示

由上图可知CPUGPU无论哪个阻碍了显示流程,都会造成掉帧现象。为了给用户提供更好的体验,我们需要进行卡顿检测以及相应的优化。

卡顿的检测

卡顿监控的方案一般有两种

  • FPS监控:为了保持流程的UI交互,App的刷新频率应该保持在60fps左右,其原因是iOS设备默认的刷新频率是60次/秒,而1次刷新(即VSync信号发出)的间隔是1000ms/60 = 16.67ms。如果在16.67ms内没有准备好下一帧数据,就会产生卡顿
  • 主线程卡顿监控:通过子线程监测主线程的RunLoop,判断两个状态(kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting)之间的耗时是否达到一定阈值
FPS监控
  • 方案一:参考YYKit中的YYFPSLabel通过CADisplayLink实现。借助link的时间差,来计算一次刷新所需的时间,然后通过刷新次数 / 时间差得到刷新频次,并判断是否符合范围,通过显示不同的文字颜色来表示卡顿严重程度
<!-- YYFPSLabel.h -->
#import <UIKit/UIKit.h>

/**
 Show Screen FPS...
 
 The maximum fps in OSX/iOS Simulator is 60.00.
 The maximum fps on iPhone is 59.97.
 The maxmium fps on iPad is 60.0.
 */
@interface YYFPSLabel : UILabel

@end

<!-- YYFPSLabel.m -->
#import "YYFPSLabel.h"
#import "YYKit.h"

#define kSize CGSizeMake(55, 20)

@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;
}

// 60 vs 16.67ms
// 1/60  * 1000 
- (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
主线程卡顿监控
  • 方案二:通过RunLoop来监控,因为卡顿的是事务,而事务是交由主线程RunLoop处理的。
    实现原理:检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿。
<!-- LGBlockMonitor.h -->
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN


@interface LGBlockMonitor : NSObject

+ (instancetype)sharedInstance;

- (void)start;

@end

NS_ASSUME_NONNULL_END

<!-- LGBlockMonitor.m -->
#import "LGBlockMonitor.h"

@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;
        }
    });
}

@end

使用方式:

[[LGBlockMonitor sharedInstance] start];
  • 方案三:直接使用三方库
  1. Swift可以使用ANREye,其实现思路是:创建一个子线程通过信号量去ping主线程,因为ping的时候主线程肯定是在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之间。每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超过阈值时,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。ANREye是使用子线程Ping的方式监测卡顿的。
  2. OC可以使用 微信matrix滴滴DoraemonKit

界面优化-预排版

开发图文混排页面时,滑动页面需要不停的计算和渲染,比如计算cell高度。案例代码如下

// 页面数据源
- (void)loadData{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
    NSData *data = [[NSData alloc] initWithContentsOfFile:path];
    NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
   
    for (id json in dicJson[@"data"]) {
        LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
        [self.timeLineModels addObject:timeLineModel];
    }
    [self.timeLineTableView reloadData];
}

#pragma mark -- UITableViewDelegate
// 返回cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    LGTimeLineModel *timeLineModel = self.timeLineModels[indexPath.row];
    timeLineModel.cacheId = indexPath.row + 1;
    NSString *stateKey = nil;
    
    if (timeLineModel.isExpand) {
        stateKey = @"expanded";
    } else {
        stateKey = @"unexpanded";
    }
    
    LGTimeLineCell *cell = [[LGTimeLineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
    
    [cell configureTimeLineCell:timeLineModel];
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];
    [cell setNeedsLayout];
    [cell layoutIfNeeded];
    
    CGFloat rowHeight = 0;
    for (UIView *bottomView in cell.contentView.subviews) {
        if (rowHeight < CGRectGetMaxY(bottomView.frame)) {
            rowHeight = CGRectGetMaxY(bottomView.frame);
        }
    }
    return rowHeight;
}

其实在网络请求的时候,我们已经拿到了数据。有了这些数据,我们就能知道cell的高度。这个时候可以对页面进行预排版,而不需要等到tableView渲染的时候才去进行大量计算。我们可以在model中提前计算好cell行高,页面frame,富文本等等。其主要思想是把耗时的操作放在页面显示前处理,这样页面滑动的时候就不需要计算很多遍,只是在处理数据的时候计算一次,这就是对页面做了优化处理。

// 优化后代码
- (void)loadData{
    //外面的异步线程:网络请求的线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       //加载`JSON 文件`
       NSString *path = [[NSBundle mainBundle] pathForResource:@"timeLine" ofType:@"json"];
       NSData *data = [[NSData alloc] initWithContentsOfFile:path];
       NSDictionary *dicJson=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
       for (id json in dicJson[@"data"]) {
            LGTimeLineModel *timeLineModel = [LGTimeLineModel yy_modelWithJSON:json];
            [self.timeLineModels addObject:timeLineModel];
       }
           
       for (LGTimeLineModel *timeLineModel in self.timeLineModels) {
            LGTimeLineCellLayout *cellLayout = [[LGTimeLineCellLayout alloc] initWithModel:timeLineModel];
            [self.layouts addObject:cellLayout];
       }
           
       dispatch_async(dispatch_get_main_queue(), ^{
            [self.timeLineTableView reloadData];
       });
    });
}

#pragma mark -- UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return  self.layouts[indexPath.row].height;
}

<!-- LGTimeLineCellLayout.m文件 -->
// 把cell行高,页面frame,富文本等等提前处理好
- (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);
}

本质是在model中把所有页面相关逻辑提前处理好,转成layoutModel如上面所示。

界面优化-预解码

比如页面加载一张图片,其加载流程如下

image.png
image.png
  • UIImageView的本质是一个模型,里面包含了UIImage
  • UIImage中包含了Data Buffer,图片是通过Data Buffer二进制流转换过来的。
  • 再通过image Buffer缓存区进行储存。
  • 最后通过ViewController显示到UIImageView上。

UIImageViewmodel属性依赖于Data Buffer的解码过程,解码之后Image Buffer才能够进行缓存,缓存之后才能在帧缓存区Frame Buffer中进行渲染。

我们在加载图片的时候,一般使用SDWebImage,下面探索其原理......

  • 查看sd_setImageWithURL方法
image.png
  • image图片来源于网络请求didComplete
image.png
  • 这里拿到的是二进制文件imageData
image.png
  • 对二进制文件进行解码
image.png

把图片的所有二进制流进行解码,比如对图片的宽高imageRef大小缩放因子maxPixelSize进行解码,最终形成了UIImage,最终就是显示

图片为什么需要预解码

SDWebImage在子线程对图片的二进制文件imageData做了解码操作,那么图片的展示为什么需要进行上面的解码呢?SDWebImage的解码操作又放在了哪里?通过添加符号断点打印堆栈信息进行调试查看......

添加符号断点
查看堆栈信息

最终发现SDWebImage在这一层面先做了预解码操作,原因是页面的卡顿大都是来自于图片展示。

图片加载流程
  • 网络请求中获取到了Data BufferImageData
  • ImageData交给子线程进行解码,解码完成之后进行回调,回调回来的就是Image Buffer像素缓存区
  • 最后交给Frame Buffer去显示。

最终优化的就是Data Buffer解码成Image Buffer的过程,所以大部分的三方框架都是在这一过程做了大量处理。

苹果在底层提供了一图形编解码插件,比如原生音视频框架AVFoundationFFmpeg。其中FFmpeg中最好的点就是对视频的编解码过程。

异步渲染

按需加载

只有需要了才去加载。例如TableView滑动时,滑动的越快也就意味着计算、渲染的频率越高。这样就有可能导致页面卡顿......

  • 优化思路一:比如滑动时使用默认占位图,当滑动了10条cell,我们只处理可视范围内的3条cell
  • 优化思路二:滑动时使用默认占位图,而是在滑动停止时处理加载图片的数据
异步渲染

关于UIViewLayer之间的关系?

  • UIView主要是用于页面交互,比如页面点击等等
  • Layer主要用于页面的渲染

真正的页面展示并不是UIView去做,而是Layer层做的。

页面渲染原理

渲染的过程是非常耗时的,这个过程称之为事物。事务里面有如下环节

  • layout构建视图
  • displayer绘制
  • prepare关于coreAnimation动画的操作
  • commit提交事务 reader server去做事务相关的处理
drawRect的流程
  • drawRect是依赖于当前UIView提供的一个UIViewRendering的功能
image.png
  • 查看drawRect方法的堆栈信息
drawRect的堆栈信息
  • 绘制图层的耗时操作放在子线程进行,最后渲染的步骤放在主线程
<!-- 下面绘制的耗时操作放在子线程处理 -->
//绘制流程的发起函数
- (void)display{
    // Graver 实现思路
    CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
    // 渲染整个图层
    [self.delegate layerWillDraw:self];
    [self drawInContext:context];
    [self.delegate displayLayer:self];
    [self.delegate performSelector:@selector(closeContext)];
}

<!-- 渲染的步骤放在主线程 -->
//layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        layer.contents = (__bridge id)(image.CGImage);
    });
}

异步渲染框架Graver渲染流程

Graver异步渲染流程

界面优化总结

CPU层面的优化

  • 尽量用轻量级的对象代替重量级的对象,可以对性能有所优化。例如不需要相应触摸事件的控件,用CALayer代替UIView
  • 尽量减少对UIViewCALayer的属性修改
  1. CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并将对应属性值保存在内部的Dictionary中,同时还会通知delegate创建动画等,非常耗时
  2. UIView相关的显示属性,例如frameboundstransform等,实际上都是从CALayer映射来的,对其进行调整时,消耗的资源比一般属性要大
  • 当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
  • 尽量提前计算视图布局预排版,例如计算cell的行高
  • Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的,所以尽量使用代码布局。如果不想手动调整frame等,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
  • 文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
  1. 如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
    计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
    文本绘制:[NSAttributedString drawWithRect:options:context:]
  2. 自定义文本控件,利用TextKit 或最底层的 CoreText 对文本异步绘制。并且CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次)。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 图片处理(解码 + 绘制)
  1. 当使用UIImageCGImageSource 的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的数据才进行解码)。这一步是无可避免的,且是发生在主线程中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap 直接创建图片,例如SDWebImage三方框架中对图片编解码的处理。这就是Image的预解码
  2. 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制子线程中进行
  • 图片优化
  1. 尽量使用PNG图片,不使用JPGE图片
  2. 通过子线程预解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
  3. 优化图片大小,尽量避免动态缩放
  4. 尽量将多张图合为一张进行显示
  • 尽量避免使用透明view,因为使用透明view,会导致在GPU计算像素时,会将透明view下层图层的像素也计算进来即颜色混合处理。
  • 按需加载,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载
  • 少使用addViewcell动态添加view

GPU层面优化

相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染输出到屏幕上。

  • 尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况
  • 尽量避免图片的尺寸超过4096×4096,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗
  • 尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的
  • 尽量避免离屏渲染
  • 异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示。参考Graver异步渲染框架
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,398评论 6 532
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,510评论 3 416
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,346评论 0 374
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,972评论 1 311
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,739评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,196评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,260评论 3 441
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,413评论 0 288
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,951评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,779评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,980评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,522评论 5 359
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,217评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,647评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,887评论 1 286
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,659评论 3 391
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,967评论 2 374

推荐阅读更多精彩内容