一、前言
和crash一样,卡顿对于用户的体验也是糟糕的,比如你曾经遇到过的几个场景:
- 在你滑动列表寻找某个商品时,页面卡顿,感觉总是要等一次滑动停止时才能继续滑动。
- 你点击后进入某个页面或者在页面中点击返回上一页,感觉总是会停顿一下之后页面才开始切换,并且切换速度也缓慢。
- 在一个页面中什么也点击不了了,并且没有提示,只能关闭APP再启动。
作为一个iOS开发者,做好用户体验是我们分内的事,那么解决卡顿就是必须的,下面总结了自己对卡顿的认识,希望跟大家探讨!
二、认识卡顿
上图是一个卡顿的例子,官方有个更全面的说明
三、线上监控
在iOS中,跟UI界面相关的UIKit元素是要求在主线程进行的,并且用户的交互也是要回调到主线程中处理,所以卡顿问题可以认为是主线程阻塞的问题,Bugly中卡顿监控的依据是主线程runloop的执行是否达到阈值。在卡顿发生时
-
借助第三方完成:bugly、 听云、字节的火山引擎新出的一个性能监控平台:zjsms.com/ed8ktbb/。 一个开源库:github.com/zixun/ANREy…
-
自建APM平台:思路可以参考这个文章: iOS获取任意线程调用堆栈信息 运行时获取函数堆栈 卡顿和卡死监控
[iOS系统库符号还原] 这个待学了逆向课程再回过头来看
四、卡顿处理
4.1、卡顿堆栈复现
我们根据上报的卡顿堆栈来解决对应的卡顿问题,首先是分析一波,然后是堆栈复现,然后定位问题,做出解决方案。
4.2、卡顿堆栈复现
我们复现卡顿堆栈、定位问题、解决问题之后,即使个人有99%把握是这样的,但别人还是会有疑问:“你是否能复现这个卡顿”? 对于这个问题我的理解是,把卡顿分为易复现和不容易复现的分情况处理:
- 不容易复现的,它是由系统内部的因素产生的,比如低电量模式、涉及进程间通信的API,所以对于这类卡顿复现的成本过高,我们按照原理出发尝试性解决,线上结果即可验证。
- 常规的耗时操作导致的,这一类型可以先找到监控平台SDK里面的上报函数,里面添加打印函数;再把耗时阈值设置为比较低的值,比如线上设置为3000ms, 我们在测试时可以设置为300ms;最后运行到该卡顿问题的页面查看。如果是找不到上报函数,或无法hook,那么可以尝试使用下面代码来测试:
class ZLBlockMonitor2 {
static let shared = ZLBlockMonitor2()
var beginActicity: CFRunLoopActivity = .entry
var endActicity: CFRunLoopActivity = .entry
var semaphore = DispatchSemaphore(value: 1)
var timeoutCount = 0
func start() {
registerObserver()
startMonitor()
}
private func registerObserver() {
//定义两个观察者:因为如果runloop通知观察者时被阻塞,那么下一个观察者是会比较晚的
let beginObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, Int.min) { observer, activity in
print("==beginObserver==")
self.beginActicity = activity
self.semaphore.signal()
}
let endObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, Int.max) { observer, activity in
print("==endObserver==")
self.endActicity = activity
self.semaphore.signal()
}
if let beginObserver = beginObserver,
let endObserver = endObserver{
//添加当前RunLoop的观察者
CFRunLoopAddObserver(CFRunLoopGetMain(), beginObserver, .commonModes)
CFRunLoopAddObserver(CFRunLoopGetMain(), endObserver, .commonModes)
}
}
private func startMonitor() {
let queue = DispatchQueue.init(label: "com.caton.monitor")
queue.async {
let milliseconds = 200
while true {
// 设置200毫秒为发生一次卡顿
let result = self.semaphore.wait(timeout: .now() + .milliseconds(milliseconds))
if result == .timedOut {
if self.beginActicity == .beforeSources
|| self.beginActicity == .afterWaiting
|| self.endActicity == .beforeSources
|| self.endActicity == .afterWaiting {
self.timeoutCount += 1
if self.timeoutCount < 2 {
continue
}
print("发生了一次耗时\(milliseconds)ms的卡顿")
print(Thread.callStackSymbols)
}
}
self.timeoutCount = 0
}
}
}
}
4.3、常见卡顿
-
- 多个小耗时任务累积成了卡顿 在一次runloop循环中调用方法多、执行链路较长的情况下,如果该链路中存在多个小耗时的操作,就大概率会发生卡顿. 在某个网络请求回调到主线程后,Json解析成Model,之后有创建和更新UI、多个地方调用layoutIfNeed立即布局视图、磁盘IO存取数据等多个小段耗时操作.
- 2.调用涉及进程间通信的系统API: NSUserDefault调用写操作、CLLocationManager当前定位权限状态的频繁获取、给通用剪贴板UIPasteboard设置值获取值、CNCopyCurrentNetworkInfo获取WiFi信息等。
-
- 递归调用自身方法做动画 比如:动画完了后,需要延时2s后再次动画,如果用延时函数延时2s后再次调用动画方法的方式,如果处于后台时也会一直掉,这样就会有卡顿。
-
- 通过for循环遍历所有frameWork的中bundle的方式加载图片 在工程完成pod组件化之后,组件里面的图片加载需要加载对应组件bundle路径下的图片,这个时候我们如果为了方便封装一个for循环遍历找图片的方法就可能会卡顿。
四、总结
“预防大于治疗”,除了自己做好卡顿处理外,还可以在组内多分享卡顿处理的经历,让大家开发时就尽量避免,倘若做到这样,那么设备卡顿率降到千分之1-5,次数卡顿率降到万分位并不难。 卡顿治理要做好,学习原理少不了。需要学习的大概有:画面掉帧原理、主线程阻塞的因素、锁、runloop原理、监控方案原理、卡顿耗时方法其底层实现原理。可以看到另一面就是在处理好卡顿问题的同时,也会使我们加深理解,编程水平得到精进。
五、奇巧淫技
这里介绍几个三方库:
1.文本异步渲染库
文本渲染是一个比较耗时的操作,而文本的真正渲染前的计算是可以使用CTRun异步计算好的,这里推荐一个文本异步渲染库: github.com/texturegrou…
2.阿里巴巴的开源iOS协程库
协程是一种在非抢占式多任务场景下生成可以在特定位置挂起和恢复执行入口的程序组件
在多期处理卡顿之后,会新增很多放到异步block的代码,这样影响阅读,所以可以看看这个库能否带来改进: github.com/alibaba/coo…
参考文章: 美团高效渲染