0%

一次卡顿优化

为什么出现卡顿

屏幕显示图像是需要CPU和GPU结合工作。CPU 负责计算显示内容,包括视图创建、布局计算、图片解码、文本绘制等,CPU 完成计算后,会将计算内容提交给 GPU;GPU 进行变换、合成、渲染,将渲染结果提交到帧缓冲区,当下一次垂直同步信号(简称 V-Sync)到来时,将渲染结果显示到屏幕上。

UI视图显示到屏幕中的过程:
此处输入图片的描述

在屏幕显示图像前,CPU 和 GPU 需要完成自身的任务,系统会每(1000/60=16.67ms)将UI的变化重新绘制,渲染到屏幕上。如果在16ms内,主线程进行了耗时操作,CPU和GPU没有来得及生产出一帧缓冲,那么这一帧会被丢弃,显示器就会保持不变,继续显示上一帧内容,用户的视觉上就出现了卡顿;因此卡顿产生的原因就是,CPU和GPU没有及时处理好数据。

卡顿的检测

卡顿分析工具

从Xcode12 开始,Instrument 新增 AnimationHitches 用以检测卡顿,这里用它分析页面卡顿情况
此处输入图片的描述

苹果于 20 年的 Session 中提出了 Hitch 的概念,用以衡量滑动时的卡顿情况。Hitch 指的是 卡顿时间(一帧延后出现的时间,ms)/ 总时间(一般是 1 秒),简单来说 卡顿时间比就是一个区间内的总卡顿时间除以它的持续时间。
此处输入图片的描述

  • 低于 5 ms/s 说明比较优秀,是最不易被用户察觉到的
  • 介于 5ms/s 和 10ms/s 之间,说明发生了中等卡顿,用户会察觉到一些中断,但并不严重
  • 高于 10 ms/s 说明发生了较严重的卡顿,已经影响了用户体验。
    耗时分析工具
    此处输入图片的描述

选中某一次卡顿,结合Instrument - TimeProfile, 就可以定位到具体的函数堆栈

页面卡顿检测

此处输入图片的描述
此处输入图片的描述

大于16.6ms /s 的为较严重卡顿,大约有33个,有一些卡顿达到了116ms/s。

某次卡顿分析

查看某次(116ms/s。)卡顿函数主线程调用栈,查看其使用的时间占比:
此处输入图片的描述

可知耗时操作有:

AWHomeCommunityTagTableCell的 setModel

UIImage+WebPConfig 的 aw_imageNamed方法、

UIApplication+AWTAutomaticTracks的 awt_sendEvent

NBSLens_iOSANREntity 的 runloopCycleStart

AWHomeSearchNetManager 的 getRecommendListWithList

map2JsonString

卡顿原因及优化

动态卡setModel的耗时:

1、主线程卡片高度计算

滑动过程中视图的宽高,文本对象宽高,排版及绘制都有一定耗时

优化:异步计算视图宽高,富文本对象宽高、排版并缓存到model里,这里富文本处理主要使用的是:YYText ,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 如果你在显示字符串时有性能问题,可以这样开启异步模式:
YYLabel *label = ...
label.displaysAsynchronously = YES;
    
// 如果需要获得最高的性能,你可以在后台线程用 `YYTextLayout` 进行预排版: 
YYLabel *label = [YYLabel new];
label.displaysAsynchronously = YES; //开启异步绘制
label.ignoreCommonProperties = YES; //忽略除了 textLayout 之外的其他属性
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   // 创建属性字符串
   NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"];
   text.yy_font = [UIFont systemFontOfSize:16];
   text.yy_color = [UIColor grayColor];
   [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
 
   // 创建文本容器
   YYTextContainer *container = [YYTextContainer new];
   container.size = CGSizeMake(100, CGFLOAT_MAX);
   container.maximumNumberOfRows = 0;
   
   // 生成排版结果
   YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
   
   dispatch_async(dispatch_get_main_queue(), ^{
       label.size = layout.textBoundingSize;
       label.textLayout = layout;
   });
});

2、网络请求数据缓存归档

数据的IO读写先对是比较耗时的

优化:改为异步的方式

3、不必要的操作

整个页面都是通过TableCell实现的,对于顶部的轮播卡,快捷入口等只展示一次的卡片在上下滑动过程中仍会reload,显然是多余的

优化:只对table的底部会复用的卡片通过UITableViewCell来实现,对于顶部分类tabbar、轮播卡、notice卡和快捷入口卡等只展示一次的卡片通过TableHeader来实现,避免花朵过程中多余的排版

4、滑动过程中的操作: 移除视图、初始化视图、添加视图

优化:

  • 视图的初始化:懒加载的方式进行初始化,只初始化一次,如需要重置则可以在重用方法里重置。
  • 移除视图、添加视图:通过hidden来控制视图的隐藏显示。

5、xib、masonry、自动布局的使用

优化:纯代码方式创建cell,frame方式布局

6、一些对象在反复创建,或在滑动过程中拼接生成

如富文本内容、标签等

优化:将 NSAttributeString的初始化逻辑和数据源逻辑处理放在异步子线程中,然后缓存到model里。

7、视图图层复杂

原代码用xib实现的卡片,一些自定义视图、活动卡片标签的实现等较为复杂

优化:尽量减少冗余视图,活动卡片标签改为富文本方式实现

8、native图片加载,会反复进行IO读取,并在主线程上的图片解码


项目里用了组件化,故hook了imageNamed,查找相应组件的bundle,然后调YYImage的解码方式在主线程解码图片,这里的问题是在反复滑动过程中,会反复的查找bundle - 解码, 较为耗时
优化: 对解码过的图片进行LRU缓存,提高读取效率

9、高分辨率图的加载会造成一定卡顿

网络图片的加载过程:加载 - 解码 - 渲染 ,除此之外还会有IO读写 ,如果是高分辨率图,也是很耗内存和CPU的

优化主要有两种方式:

方式 特点 支持
下采样加载 根据size加载相应大小的图片,图片源不变 只针对下载后的图片做的操作
不需要图片服务器支持
SDWebimage支持,
YYWebImage需要自己扩展
按需请求缩略图 根据size请求相应大小的图片 节约流量,
请求快,
降低内存占用
需要图片服务器支持
YYWebImage支持
SDWebimage支持

项目里图片服务器是用的阿里云OSS,支持图片缩放,故采用第二种方案,请求缩略图实例:

1
https://oss-console-img-demo-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,h_100,m_lfit

10、轮播动画

icarousel step,轮播图等,滑出界面后,这个头部banner也一直在轮播,有一定耗时

优化: 在动画卡滑出界面或在离开当前页时,停止动画, 再出现后再开启

11、视频播放器自动播放时持续得在主线程写缓存

1
JPVideoplayerManager savePlaybackElapsedSeconds:forVideoUrl

视频播放过程会不断在主线程写文件,较为耗时

优化: 改为异步方式

12、离屏渲染问题

主要是针对如下场景:阴影、遮罩、组不透明等进行优化

该页面引起离屏幕渲染的问题主要是绘制阴影:

1
2
3
4
self.customTabbr.layer.shadowOffset = CGSizeMake(5, 0);
self.customTabbr.layer.shadowColor = [UIColor blackColor].CGColor;
self.customTabbr.layer.shadowOpacity = 0.2;//阴影透明度,默认0
self.customTabbr.layer.shadowRadius = 5;//阴影半径,默认3

优化:

1
2
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.customTabbr.bounds];
self.customTabbr.layer.shadowPath = path.CGPath;

其它耗时

  1. UIApplication+AWTAutomaticTracks的 awt_sendEvent
    这里是通过hook的方式对事件做了自动采集

这个目前耗时占得比重不是很高,且影响较大,暂时没做优化

后续优化:出于性能考虑,是把这种自动埋点方式去掉,改成部分hook(比如页面pv)结合手动埋点。

2、NBSLens_iOSANREntity 的 runloopCycleStart

这个是一个第三方sdk,用来监测线上crash

1
[NBSAppAgent setStartOption: NBSOption_Crash | NBSOption_UI];

优化:替换成性能更好的其它方案(bugly),支持卡顿分析
3、BaiduMobStat

1
[[BaiduMobStat defaultStat] startWithAppId:@""];

百度热力图,占用耗时根听云sdk时差不多的,如果不是很需要可以考虑去掉
主要是用来分析用户行为的

优化: 后续重构数据采集sdk,一起替换掉

效果:
此处输入图片的描述

可以看到卡顿已经下降了很多

卡顿监控

参考:

https://github.com/ibireme/YYText
https://developer.apple.com/videos/play/tech-talks/10855/