iOS性能优化-卡顿
2022-12-11 本文已影响0人
wuyukobe
前言:本文旨在介绍iOS性能优化中有关页面卡顿的产生、优化以及监控。
一、屏幕的显示
图片加载到显示的过程:通常计算机在显示图片都是CPU与GPU协同合作完成一次渲染工作。在屏幕成像的过程中CPU与GPU起着至关重要的作用。
1、CPU(Central Processing Unit,中央处理器)
CPU负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码以及图像的绘制。
2、GPU(Graphics Processing Unit,图形处理器)
GPU负责纹理的渲染
二、卡顿原因
1、产生卡顿的原因:
- CPU、GPU渲染流水线耗时过长。
- 垂直同步Vsync + 双缓存区以掉帧为代价解决了图片撕裂问题。
2、图片撕裂原理:
页面撕裂:图片渲染是需要经过CPU和GPU的共同处理,但是如果CPU和GPU流水线耗时过长,可能出现的情况是,当扫描完当前帧缓存区的图片之后,下一帧还未渲染完成,这时候显示器会从帧缓存区继续扫描旧的图片,因为扫描过程是一行一行扫描的,而如果在旧的图片扫到一部分时发现下一帧新图片来了,然后显示器就会立即扫描下一帧的图片,此时就会出现上半部图片是上一帧的图片,下半部是新的一帧,导致图片看起来是撕裂的,也就是出现了图片撕裂的问题。
3、卡顿解决方法
- 使用多缓存区,以空间换取时间,最大限制利用CPU和GPU的闲置时间。
- 提高CPU、GPU的性能和使用效率。
三、CPU层面的卡顿优化
- 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
- 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
- Autolayout会比直接设置frame消耗更多的CPU资源
- 图片的size最好刚好跟UIImageView的size保持一致
- 控制一下线程的最大并发数量
- 尽量把耗时的操作放到子线程,比如:文本处理(尺寸计算、绘制)、图片处理(解码、绘制)
四、GPU层面的卡顿优化
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
- 尽量减少视图数量和层次
- 减少透明的视图(alpha<1),不透明的就设置opaque为YES
- 尽量避免出现离屏渲染
五、离屏渲染
1、在OpenGL中,GPU有2种渲染方式
- On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作。
- Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
2、离屏渲染消耗性能的原因
- 需要创建新的缓冲区。
- 在触发离屏渲染之后会增加GPU的工作量,导致增加了CPU和GPU的总耗时量,有可能会导致UI的卡顿和掉帧。
- 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。
3、哪些操作会触发离屏渲染?
- 光栅化,layer.shouldRasterize = YES
(光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上
的过程,光栅化的本质是坐标变换、几何离散化) - 遮罩,layer.mask
- 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
- 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
- 阴影,layer.shadowXXX
- 如果设置了layer.shadowPath就不会产生离屏渲染
六、RunLoop监控卡顿原理
- 创建一个Observer来监听RunLoop,在RunLoop的 kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting 获取一个开始时间值start;
- 开启一个子线程,在while循环中监控RunLoop的状态,并获取当前时间作为结束时间end。
- 拿end减去start 获取一个时间段 和一个阀值(long usec_anrShortTime = 2s)做比较,若大于则说明存在卡顿。则获取卡顿信息并上传。
相关代码参考(类名 APMANRMonitorPlugin)
// 创建一个Observer来监听RunLoop
- (void)addRunLoopObserver {
NSRunLoop *curRunLoop = [NSRunLoop mainRunLoop];
//1.注册default RunloopMode
//监听开始的observer
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &RunLoopBeginCallback, &context);
CFRetain(beginObserver);
//监听结束的observer
CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &RunLoopEndCallback, &context);
CFRetain(endObserver);
//监听监听
CFRunLoopAddObserver([curRunLoop getCFRunLoop], beginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver([curRunLoop getCFRunLoop], endObserver, kCFRunLoopCommonModes);
//保留observer
runLoopBeginObserver = beginObserver;
runLoopEndObserver = endObserver;
}
// RunLoop的状态
//最高优先级
void RunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
g_RunloopActivity = activity;
g_blockArised = NO;
switch (activity) {
case kCFRunLoopEntry:
g_Run = YES;
break;
case kCFRunLoopBeforeTimers:
if (g_Run == NO) {
gettimeofday(&g_TimevalMark, NULL);
}
g_Run = YES;
break;
case kCFRunLoopBeforeSources:
if (g_Run == NO) {
gettimeofday(&g_TimevalMark, NULL);
}
g_Run = YES;
break;
case kCFRunLoopAfterWaiting:
if (g_Run == NO) {
gettimeofday(&g_TimevalMark, NULL);
}
g_Run = YES;
break;
case kCFRunLoopAllActivities:
break;
default:
break;
}
}
// 开启一个子线程,在while循环中监控RunLoop的状态
- (void)addMonitorThread {
monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMonitoringProcess) object:nil];
[monitorThread start];
}
- (void)threadMonitoringProcess {
//保存堆栈信息
g_BlockStartTimeval = {0,0};
APMDumpType reportDumpType = APMDumpTypeNormal;
// 卡死卡顿信息打包上报
[self packageBlockToKillReport];
//重置监控状态
[self resetANRMonitorStatus];
while (YES) {
@autoreleasepool {
//退出策略
if (_bStop) { break; }
APMDumpType dumpType = [self checkDumpType];
if (g_Run == YES && dumpType != APMDumpTypeNormal) {
if (g_BlockReportPath.length <= 0) {
g_BlockReportPath = [APMANRMonitorTool dumpReportWithType:dumpType];
g_BlockStartTimeval = g_TimevalMark;
g_blockArised = YES;
}
reportDumpType = dumpType;
}else{
if (g_blockArised == NO && g_BlockReportPath.length > 0 && VALID_TIMEVAL(g_BlockStartTimeval)) {
//保存当前堆栈信息并上传
struct timeval cur;
gettimeofday(&cur, NULL);
[self recordBlockReportWithStartTimeval:g_BlockStartTimeval
endTimeval:cur
dumpType:reportDumpType];
//重置监控状态
[self resetANRMonitorStatus];
}
}
//睡一会
usleep(_defaultCheckPeriodTime);
//退出策略
if (_bStop) { break; }
}
}
}
七、Runloop监听卡顿为什么是beforesource,afterwaiting这两个?
----------BeforeTimers
----------BeforeSources
++++++++++ 处理Block
++++++++++ 处理Source0(非port),手动处理
++++++++++ 如果Source0处理成功,则处理Block
==== 如果有Source1事件,直接跳转到HandleMSG
----------BeforeWaitting
====
唤醒条件【即有任务待处理】:
runloop时间超时,timer触发,Port有消息出发, 被手动唤醒
====
----------AfterWaiting
==== HandleMSG【被唤醒后需要处理的任务】
++++++++++++ 处理计时器事件
++++++++++++ 处理主异步到主队列的消息
++++++++++++ 处理source1事件
++++++++++++ 处理block事件
从上面任务执行流程可以看出:其主要执行任务的时机为:BeforeSources和AfterWaiting。
- 对于任务比较多,导致CPU+GPU处理不完的卡顿情形可以通过监听这两个通知执行时间是否超时来达到卡顿监控。
- 对于屏幕静止是卡顿,由于一直是处于BeforeWaiting状态,我们可以通过ping主线程的方式验证主线程是否卡顿。
- 总结:卡顿监控就是利用Runloop在发出BeforeWaiting通知后触发UI刷新的特点,再依靠任务执行是否超出阈值来判断卡顿的。