为什么出现卡顿
屏幕显示图像是需要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、网络请求数据缓存归档
数据的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 | self.customTabbr.layer.shadowOffset = CGSizeMake(5, 0); |
优化:
1 | UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.customTabbr.bounds]; |
其它耗时
- 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/