卡顿的原理
想要进行界面优化
,首先就要了解怎么产生卡顿
?
通常来说计算机中的显示过程
是下面这样的,通过CPU
、GPU
、显示器
协同工作来将图片显示到屏幕上
![](https://img.haomeiwen.com/i1212147/890c29d6cb185ced.png)
-
CPU
计算好显示内容,提交至GPU
-
GPU
经过渲染完成后将渲染的结果放入FrameBuffer
(帧缓存区) - 随后视频控制器会按照
VSync
垂直信号逐行读取FrameBuffer
的数据 - 经过可能的
数模转换
传递给显示器
进行显示
最开始时FrameBuffer
只有一个,这种情况下FrameBuffer
的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区
即双缓冲机制
。在这种情况下,GPU
会预先渲染好一帧放入FrameBuffer
,让视频控制器读取。当下一帧渲染好后,GPU
会直接将视频控制器
的指针指向第二个FrameBuffer
。
双缓存机制
解决了效率问题,但随之而来的是新的问题。比如当前这一帧处理比较慢,GPU
会将视频控制器的指针指向第二个FrameBuffer
,那么上一帧的图像处理就会丢掉即掉帧
。现象就是屏幕出现跳屏
即卡顿
。
屏幕卡顿原因
在VSync
信号到来后,系统图形服务会通过CADisplayLink
等机制通知App
。App
主线程开始在CPU
中计算显示内容,随后CPU
会将计算好的内容提交到GPU
,由GPU
进行变换、合成、渲染
。随后GPU
会把渲染结果提交到帧缓冲区
,等待下一次VSync
信号到来时显示到屏幕上。由于垂直同步
的机制,如果在一个VSync
时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变
。
如下图显示过程,第1帧在VSync到
来前,处理完成,正常显示,第2帧在VSync到来后,仍在处理中,此时屏幕不刷新依旧显示第1帧
,此时就出现了掉帧
情况,渲染时就会出现明显的卡顿现象
。
![](https://img.haomeiwen.com/i1212147/771da9e30f97568b.png)
由上图可知CPU
和GPU
无论哪个阻碍了显示流程,都会造成掉帧现象
。为了给用户提供更好的体验,我们需要进行卡顿检测
以及相应的优化。
卡顿的检测
卡顿监控的方案一般有两种
-
FPS监控
:为了保持流程的UI交互,App的刷新频率应该保持在60fps左右
,其原因是iOS设备默认的刷新频率是60次/秒
,而1次刷新(即VSync信号发出)的间隔是1000ms/60 = 16.67ms
。如果在16.67ms
内没有准备好下一帧数据,就会产生卡顿
-
主线程卡顿监控
:通过子线程监测主线程的RunLoop
,判断两个状态(kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
)之间的耗时是否达到一定阈值
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];
- 方案三:直接使用三方库
-
Swift
可以使用ANREye,其实现思路是:创建一个子线程通过信号量去ping主线程
,因为ping的时候主线程肯定是在kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
之间。每次检测时设置标记位为YES
,然后派发任务到主线程中将标记位设置为NO
。接着子线程沉睡超过阈值时,判断标志位是否成功设置成NO
,如果没有说明主线程发生了卡顿。ANREye
是使用子线程Ping的方式
监测卡顿的。 -
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
如上面所示。
界面优化-预解码
比如页面加载一张图片,其加载流程如下
![](https://img.haomeiwen.com/i1212147/5ae9dd8b28eab58a.png)
![](https://img.haomeiwen.com/i1212147/cab21f14a86a3cf8.png)
-
UIImageView
的本质是一个模型,里面包含了UIImage
-
UIImage
中包含了Data Buffer
,图片是通过Data Buffer
二进制流转换过来的。 - 再通过
image Buffer
缓存区进行储存。 - 最后通过
ViewController
显示到UIImageView
上。
UIImageView
的model
属性依赖于Data Buffer
的解码过程,解码之后Image Buffer
才能够进行缓存,缓存之后才能在帧缓存区Frame Buffer
中进行渲染。
我们在加载图片的时候,一般使用SDWebImage
,下面探索其原理......
- 查看
sd_setImageWithURL
方法
![](https://img.haomeiwen.com/i1212147/dd496edbfa072c42.png)
-
image图片
来源于网络请求didComplete
![](https://img.haomeiwen.com/i1212147/fae0d64a080016d1.png)
- 这里拿到的是二进制文件
imageData
![](https://img.haomeiwen.com/i1212147/9fe9071c8c45e620.png)
- 对二进制文件进行解码
![](https://img.haomeiwen.com/i1212147/6a7e35d3b23ced07.png)
把图片的所有二进制流进行解码,比如对图片的宽高
、imageRef
、大小
、缩放因子maxPixelSize
进行解码,最终形成了UIImage
,最终就是显示
。
图片为什么需要预解码
SDWebImage
在子线程对图片的二进制文件imageData
做了解码操作,那么图片的展示为什么需要进行上面的解码呢?SDWebImage
的解码操作又放在了哪里?通过添加符号断点
、打印堆栈信息
进行调试查看......
![](https://img.haomeiwen.com/i1212147/059e254ce7db4145.png)
![](https://img.haomeiwen.com/i1212147/099bc92fb9129858.png)
最终发现SDWebImage
在这一层面先做了预解码
操作,原因是页面的卡顿大都是来自于图片展示。
图片加载流程
- 网络请求中获取到了
Data Buffer
即ImageData
-
ImageData
交给子线程进行解码,解码完成之后进行回调,回调回来的就是Image Buffer
即像素缓存区
- 最后交给
Frame Buffer
去显示。
最终优化的就是Data Buffer
解码成Image Buffer
的过程,所以大部分的三方框架都是在这一过程做了大量处理。
苹果在底层提供了一图形编解码插件,比如原生音视频框架AVFoundation
、FFmpeg
。其中FFmpeg
中最好的点就是对视频的编解码
过程。
异步渲染
按需加载
只有需要了才去加载
。例如TableView
滑动时,滑动的越快也就意味着计算、渲染的频率越高。这样就有可能导致页面卡顿......
- 优化思路一:比如滑动时使用
默认占位图
,当滑动了10条cell,我们只处理可视范围内
的3条cell - 优化思路二:滑动时使用
默认占位图
,而是在滑动停止
时处理加载图片的数据
异步渲染
关于UIView
和Layer
之间的关系?
- UIView主要是用于
页面交互
,比如页面点击等等 - Layer主要用于
页面的渲染
真正的页面展示并不是UIView
去做,而是Layer
层做的。
![](https://img.haomeiwen.com/i1212147/313afe284f307820.png)
渲染的过程是非常耗时的,这个过程称之为事物
。事务里面有如下环节
layout构建视图
displayer绘制
prepare关于coreAnimation动画的操作
commit提交事务 reader server去做事务相关的处理
drawRect
的流程
-
drawRect
是依赖于当前UIView
提供的一个UIViewRendering
的功能
![](https://img.haomeiwen.com/i1212147/3d0749fdb3e6cf6e.png)
- 查看
drawRect
方法的堆栈信息
![](https://img.haomeiwen.com/i1212147/4083fc6f3a971ca2.png)
- 将
绘制图层的耗时操作
放在子线程进行,最后渲染的步骤
放在主线程
<!-- 下面绘制的耗时操作放在子线程处理 -->
//绘制流程的发起函数
- (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
渲染流程
![](https://img.haomeiwen.com/i1212147/ecff27e10e2d1104.png)
界面优化总结
CPU层面的优化
- 尽量
用轻量级的对象
代替重量级的对象,可以对性能有所优化。例如不需要相应触摸事件的控件,用CALayer
代替UIView
- 尽量减少对
UIView
和CALayer
的属性修改
- CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时
resolveInstanceMethod
为对象临时添加一个方法,并将对应属性值保存在内部的Dictionary
中,同时还会通知delegate
、创建动画
等,非常耗时 -
UIView
相关的显示属性,例如frame
、bounds
、transform
等,实际上都是从CALayer
映射来的,对其进行调整时,消耗的资源比一般属性要大
- 当有大量对象释放时,也是非常耗时的,尽量挪到
后台线程
去释放 - 尽量
提前计算视图布局
即预排版
,例如计算cell的行高
-
Autolayout
在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout
带来的CPU消耗
是呈指数上升的,所以尽量使用代码布局
。如果不想手动调整frame
等,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
- 文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
- 如果对文本没有特殊要求,可以使用
UILabel
内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
文本绘制:[NSAttributedString drawWithRect:options:context:]
- 自定义文本控件,利用
TextKit
或最底层的CoreText
对文本异步绘制。并且CoreText
对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次)。CoreText
直接使用了CoreGraphics
占用内存小,效率高
- 图片处理(解码 + 绘制)
- 当使用
UIImage
或CGImageSource
的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU渲染前,CGImage
中的数据才进行解码)。这一步是无可避免
的,且是发生在主线程
中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片,例如SDWebImage
三方框架中对图片编解码的处理。这就是Image的预解码
- 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的
绘制
在子线程
中进行
- 图片优化
- 尽量使用
PNG
图片,不使用JPGE
图片 - 通过
子线程预解码,主线程渲染
,即通过Bitmap
创建图片,在子线程赋值image
- 优化图片大小,尽量避免动态缩放
- 尽量将多张图合为一张进行显示
- 尽量
避免使用透明view
,因为使用透明view,会导致在GPU计算像素时,会将透明view下层图层的像素也计算进来即颜色混合
处理。 -
按需加载
,例如在TableView
中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载 - 少使用
addView
给cell
动态添加view
GPU层面优化
相对于CPU
而言,GPU
主要是接收CPU
提交的纹理+顶点
,经过一系列transform
,最终混合并渲染
输出到屏幕上。
- 尽量
减少在短时间内大量图片的显示
,尽可能将多张图片合为一张显示
,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧
的情况 - 尽量避免图片的尺寸超过
4096×4096
,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗
-
尽量减少视图数量和层次
,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的 - 尽量避免
离屏渲染
-
异步渲染
,例如可以将cell中的所有控件、视图合成一张图片进行显示。参考Graver异步渲染框架
网友评论