「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。
- 界面优化是一个老生常谈的问题,这篇文章主要介绍
界面渲染
流程,界面卡顿的检测
,解决卡顿
的方法。
1. 界面渲染流程
通常我们知道视图显示是通过GPU
进行图像渲染加载出来的
CPU
计算出要显示的内容,交由GPU
处理。GPU渲染
完成后把渲染的结果交由Frame Buffer
(帧缓存区)处理。- 帧缓存区会把把数据交给
视频控制器
进行读取 - 经过
数模转换
后显示在屏幕上
1.1 双缓存区设计
为了提高渲染效率
,开发者设计出了双缓存区
,2个FrameBuffer
切换读取,当GPU渲染好放入帧缓存区
,交给视屏控制器读取,之后读取显示过程中GPU渲染
好的结果放在另一个
帧缓存区,这样视频控制器来回切换读取,大大提高了效率
。
但是随之而来的问题是,当视频控制器还未读取完成
时,即屏幕内容刚显示一半
时,GPU
将新的一帧内容提交到帧缓冲区
并把两个缓冲区进行交换
后,视频控制器就会把新的一帧
数据的下半段
显示到屏幕上,造成画面撕裂现象
为了解决这个问题引入了VSync
:垂直同步信号机制
。开启后GPU
会等待屏幕的VSync
发出信号后进行新一帧的渲染和缓存
。这样避免了加载不完全的问题,但是也会消耗更多的内存
。
1.2 垂直同步信号机制:V-sync
目前主流的移动设备是什么情况呢?从网上查到的资料可以知道,iOS 设备会始终使用双缓存
,并开启垂直同步
。 但是随之而来会有新的问题,界面的卡顿
。我们知道每一帧的显示是经过CPU计算
好要显示的内容后,交由GPU进行渲染
,之后放入缓存
区,视频控制器读取
显示。如果某一帧CPU
计算时间过长,或者GPU
渲染过长。加起来的时间超过
了垂直同步信号的间隔时间
,这个时候视频控制器就不会读取
没有处理好的数据,我们感觉到卡顿。其实就是掉帧
,如下图所示:
通过图中可知我们需要对CPU的计算还是CGPU的渲染进行优化
,减少用户卡顿。
2. 卡顿检测
卡顿的检测通常有2种方式:
1.FPS检测
:我们可以使用YYKit
中的YYFPSLabel
,也可以仿照它自己自定义一个。
主要是通过CADisplayLink
来实现,通过link
的时间差计算一秒刷新的次数,根据刷新的次数显示刷新频次。对于一般的检测FPS已经够用了。
- 主线程卡顿监控
我们通过RunLoop
来监控,因为卡顿的是事务,而事务是交由主线程
的RunLoop
处理的。
@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
复制代码
通过runloop
监测主线程每次执行任务循环
的时间,如果这个时间超过我们定义的时间说明发生了超时
,出行了卡顿。首先给mainRunloop
添加观察者,通过CFRunLoopAddObserver
,每次结束任务循环都会调用回调函数
。之后再子线程
中监控时长
,这里采用信号量
计算间隔。当间隔超过1
秒的话,并且两种状态kCFRunLoopBeforeSources
、kCFRunLoopAfterWaiting
,则说明发生了卡顿
。
我们把线程睡眠2秒,监听超过1秒所以报卡顿了。
一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!,说明3秒了卡顿2次,减去默认1秒。
不卡顿的情况。 也可以直接使用三方库
Swift
的卡顿检测第三方ANREye,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true
,然后派发任务到主线程,标记置为false
,接着子线程睡眠超过阈值
时,判断标记是否为false
,如果没有,说明主线程发生了卡顿OC
可以使用 微信matrix、滴滴DoraemonKit
3. 界面优化
3.1 CPU方面优化
预排版
:对于我们常用的比如ableview,我们可以在拿到数据的时候就计算好它的布局情况
,避免渲染的时候进行计算。比如cell的行高
等。- 减少
动态的添加
view,比如cell添加view。 - 对于一些
没有交互
的显示是图可以用CALayer
代替UIView
,用轻量级的对象
- 减少
Autolayout
的使用,对于简单的布局Autolayout
可以大量节约我们时间,对于复杂页面,嵌套布局较多会造成CPU运算
消耗会呈指数级上升
如果你不想手动调整frame
等属性,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
按需加载
,我们可以使用懒加载
和复用机制
。不要一次性创建所有的subview
,当需要时才创建,当完成任务,进行切换的时候可以放入一个可重用
的队列,这样下次滚动或者显示的时候,避免不必要的内存分配。- 当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
- 对于一些处理
较慢的objects
,比如NSDateFormatter
和NSCalendar
。比如请求的数据中显示日期,想要避免使用这个对象的瓶颈就需要复用他们,可以通过添加属性
到类中,或者创建静态变量
来实现。 - 尽量
避免使用透明view
,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来 - 请求或者耗时操作异步子线程处理,
不要阻塞主线程
。 - 正确的
设置背景图片
,全屏背景图的话添加一个UIImageview
作为子View。某个小小的view
的背景图,使用UIColor
的colorWithPatternImage
来做,它会更快的渲染不会浪费很多内存。 - 尽量使用
PNG
图片,不使用JPGE
图片;优化图片大小,尽量避免动态缩放
,在运行中缩放图片很耗费资源,特别UIImageview
嵌套UIScrollView
中。如果是从服务器中下载的,可以在下载前调整到合适大小。也可以在下载完成后用background thread
进行缩放一次,之后UIImageview
使用缩放后的图片。 - 图片在使用
UIImage
或者CGImageSource
创建时,图片不会立即解码,而是在设置的时候进行解码,我们可以在子线程中先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片,例如SDWebImage
三方框架中对图片编解码的处理。这就是Image的预解码
- 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的
绘制
在子线程
中进行 - 图片是否进行缓存,
imageNamed
会加载成功后缓存到内存中,而对于imageWithContentOfFile
则不会,实用于加载一张大图并且使用一次。
3.2 GPU方面
对于GPU方面主要是优化它渲染进行优化
-
尽量
减少在短时间内大量图片的显示
,尽可能将多张图片合为一张显示
,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况 -
尽量
避免离屏渲染
,它会开辟新的缓存区
,同时整个过程会多次切换上下文
,显示从当前的屏幕切换到离屏,离屏渲染结束后把离屏缓冲区的结果显示到屏幕上,又要将上下文环境从离屏切换到当前屏幕。这样会造成大量内存浪费,更多的开销。 -
离屏渲染通常有:
光栅化
layer.shouldRasterize = YES;遮罩层
mask,阴影
shadow,圆角
cornerRadius+clipsToBounds,毛玻璃
效果等。 -
异步渲染
,例如可以将cell中的所有控件、视图合成一张图片
进行显示,可以参考Graver三方框架。