一、前言

咱们运用APP有时会遇到点击呼应愚钝、页面跳转缓慢、滑动列表不流畅、卡死无呼应,这些便是卡顿问题,它会影响用户体验,严重时会导致用户的丢失,因而卡顿管理是非常重要的。

但是要将卡顿管理好并不简单。许多卡顿是跟着时刻各种机型、体系、运转环境的呈现慢慢浮现的,而且卡顿总数量往往比较多,这就意味着不或许一次性处理,从卡顿防备、监控上报、上报处理都是长期的事情。那么该如何去做好卡顿管理呢?接下来我将对App的实践做个总结阐明,有爱好的同学能够在谈论区一同讨论。

二、管理作用

咱们运用的是Bugly进行卡顿的监控,设置的卡顿阈值是3000ms。经过多期的卡顿优化,iOS乘客端设备卡顿率从6.51%降到了0.21%。

版别号 v1.2.10(优化前) v1.3.30(优化后)
设备卡顿率 6.51% 0.21%

截止目前最新版别v1.3.30的设备卡顿率是0.21%,见下图。

出行iOS用户端卡顿治理实践

三、管理战略

卡顿管理是长期的事情,为了使管理工作有序且能看到作用,需求有战略得进行。

出行iOS用户端卡顿治理实践

首要,把卡顿分为四个阶段,A阶段是处理大头且卡顿原因显着的;B阶段处理大头的疑问卡顿;C阶段处理小头的卡顿;D阶段是到达方针后的阶段,主要维持卡顿不忽然大增。

其次,关于每个阶段,动态分期处理。分期是以一个发版窗口为一期,动态是每期环绕最近两个版别进行。别的针比照较疑问的卡顿,能够采取测验性地处理,尽或许削减它的产生次数。

再其次,各期卡顿处理计划和作用形成文档,卡顿防劣化依据此文档进行组内共享,别的这样也便利盯梢各期处理的作用。

最终,在这个战略之下再进行具体卡顿问题的处理。

四、管理办法

卡顿管理能起到直接作用的办法便是处理上报的卡顿问题。把上报的卡顿分为两类,一类是开发人员写的办法耗时导致的;另一类是比较隐晦的,通常触及到体系办法内部的履行。因为第一类卡顿在实践开发中大家都有认识防止,也简单处理,所以接下来要点讲述第二类卡顿的处理。

  1. 复现卡顿的思路

复现仓库是处理问题的要害,复现了仓库就确定了履行链路,接下来就好定位问题了。

出行iOS用户端卡顿治理实践

结合实践的经历,复现的具体操作如下:

先从Bugly上报仓库中找到要害办法或函数,在Xcode中增加符号断点,运转APP到对应的运用场景中,然后在断点停住时能够运用lldb调试指令bt打印出调用栈跟Bugly中的比对。 假如是相同的,那么履行的仓库就复现了。接下来从Debug Navgator栏中能够很便利的检查到这期间的办法履行状况,结合代码检查这个调用链路中存在的耗时操作,定位问题进行处理。

在实践的仓库比照时,假如发现Bugly仓库中有的办法在Xcode仓库中没有,但该办法前后都能对上,不用慌!这或许是Xcode在打包时将一些办法优化成内联函数了,这种状况咱们运转在release形式下检查就能够了,接下来经过两个示例来阐明。

  1. 示例

  1. 体系库函数带下划线

依据上述思路,首要剖析多个上报记载,检查到仓库都是下面这样:

0 libsystem_kernel.dylib _mach_msg_trap + 8
1 libsystem_kernel.dylib _mach_msg + 76
2 libdispatch.dylib __dispatch_mach_send_and_wait_for_reply + 540
3 libdispatch.dylib _dispatch_mach_send_with_result_and_wait_for_reply + 60
4 libxpc.dylib _xpc_connection_send_message_with_reply_sync + 240
5 Foundation ___NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ + 16
6 Foundation -[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:] + 2540
7 Foundation -[NSXPCConnection _sendSelector:withProxy:arg1:arg2:arg3:] + 152
8 Foundation __NSXPCDistantObjectSimpleMessageSend3 + 84
9 CoreLocation _CLCopyTechnologiesInUse + 32832
10 CoreLocation _CLCopyTechnologiesInUse + 26408
11 CoreLocation _CLClientStopVehicleHeadingUpdates + 94460
12 XLUser  + [ HLLMKLocationRecorder locationAuthorised] ( HLLMKLocationRecorder .m: 299 )
13 ... // 以下略

接下来找到要害办法、函数。从上面的仓库中,咱们能够看到是主线程运转到[LocationRecorder locationAuthorised]办法中第269行产生,该行代码是[CLLocationManager authorizationStatus],是获取体系用户当时定位权限的办法。对仓库中第0-12行中的办法做一番了解,开端发现xpc_connection_send_message_with_reply_sync函数或许会阻塞当时线程,点击检查官方阐明。

接下来增加符号断点xpc_connection_send_message_with_reply_sync, 留意假如是体系库中的带下划线的函数,咱们增加符号断点的时分一般需求少一个下划线_,又比方上述的__dispatch_mach_send_and_wait_for_reply函数,咱们增加符号断点时也要少一个下划线,即_dispatch_mach_send_and_wait_for_reply

然后在断点停住时,在lldb调试台敲bt后回车就能够打印出当时的调用栈。

出行iOS用户端卡顿治理实践

然后经过比对是共同的,这样确认了场景。

然后定位问题。在上述卡顿仓库调用链路中,我发现自己项目的办法调用链路中不存在耗时多的操作,那么接下来能够剖析体系函数是否存在耗时多的操作,一般是触及进程间通讯的,接着咱们经过xpc_connection_send_message_with_reply_sync办法查网上材料,发现这个办法触及到了进程间通讯。

最终出处理计划。咱们经过新增一个单例类,单例设置为CLLocationManager署理并依据署理办法更新单例的定位权限特点,项目中全局获取定位权限的办法改为访问单例类中的特点,上线后就处理了该问题。

  1. 体系库中办法调用,不带下划线

下面是在iPhone6、6plus之类的较老机型上产生的卡顿,是在push页面后键盘弹起时产生。

0 libsystem_kernel.dylib ___psynch_cvwait + 8
1 libsystem_pthread.dylib __pthread_cond_wait$VARIANT$mp + 688
2 Foundation -[NSCondition waitUntilDate:] + 128
3 Foundation -[NSConditionLock lockWhenCondition:beforeDate:] + 100
4 UIKitCore -[UIKeyboardTaskQueue lockWhenReadyForMainThread] + 420
5 UIKitCore -[UIKeyboardTaskQueue waitUntilAllTasksAreFinished] + 84
6 UIKitCore -[UIKeyboardImpl generateAutofillCandidate] + 136
7 UIKitCore -[UIKeyboardImpl setDelegate:force:] + 4884
8 UIKitCore -[UIPeripheralHost(UIKitInternal) _reloadInputViewsForResponder:] + 1544
9 UIKitCore -[UIResponder(UIResponderInputViewAdditions) reloadInputViews] + 80
10 UIKitCore -[UIResponder becomeFirstResponder] + 804
11 UIKitCore -[UIView(Hierarchy) becomeFirstResponder] + 156
12 UIKitCore -[UITextField becomeFirstResponder] + 244
13 ... // 以下略

从仓库中看到,体系库中的办法是一些OC办法。关于这个状况咱们增加符号断点时能够直接运用办法的姓名,比方上述的waitUntilDate:lockWhenCondition:beforeDate:。断点停住后,在lldb调试台bt就能够打印出当时的调用栈,跟Bugly卡顿仓库是对得上的。

经过办法调用链路剖析认为是这个锁产生的卡顿,在生成键盘上候选词时会调用到。在咱们设置输入框的特点autocorrectionTypeUITextAutocorrectionTypeNo后,就不会呈现了,从而这个卡顿就处理了。

if (低版别机型) {
    textField.autocorrectionType = UITextAutocorrectionTypeNo;
}

五、常见卡顿

  1. 多个小耗时使命累积成了卡顿

在一次runloop循环中调用办法多、履行链路较长的状况下,假如该链路中存在多个小耗时的操作,就大概率会产生卡顿。这种状况在测验时期不会暴露,但线上运转的环境更复杂,APP或许处于后台、低电量等设备CPU资源严重的状况。这种卡顿的特点是,在卡顿列表中,有该履行链路中的多个办法的卡顿问题。

一个比方,在某个网络请求回调到主线程后,Json解析成Model,之后有创建和更新UI、多个当地调用layoutIfNeed立即布局视图、磁盘IO存取数据等多个小段耗时操作。关于这样的卡顿,处理办法是,首要排查出链路中触及到耗时操作的当地,进行优化;然后将某些办法放到异步主行列中履行,这样链路就缩短了。

  1. Jetsam 机制下收到内存正告

iOS 体系在内存严重时,会紧缩一些内存内容,并在需求时解压,但副作用是会造成较高的 CPU 占用乃至卡顿,手机耗电量也会随之增加。为了处理上面的问题,苹果规划了 Jetsam 机制。 其工作办法是当内存不足时,体系会告诉前台应用去开释内存(经过 applicationDidReceiveMemoryWarning 办法和 UIApplicationDidReceiveMemoryWarningNotification 告诉),假如内存压力依然存在,将会终止一些后台APP, 最终内存还不够的话,就会终止当时APP(FOOM),而且上报日志。

咱们在卡顿上报中能够看到有一些卡顿是因为收到内存正告时产生的,这就需求咱们依据自己页面的状况在收到内存正告时恰当的清理内存,而且最好是在收到内存正告告诉的处理放到异步主行列中,防止过多的内存正告处理都集中在一次runloop中。

  1. 主线程履行了耗时使命

比方在聊天页面挑选高清大图后,先履行紧缩后保存到本地一份再发送,这个或许会产生卡顿。咱们能够将此操作放到异步子行列中处理。

  1. 调用触及进程间通讯的体系API

假如办法履行中调用了触及到进程间同步通讯的API,是或许产生卡顿的,特点是仓库中会有_xpc_connection_send_message_with_reply_sync这个函数,发现的几种状况是:

  • NSUserDefault调用写操作
  • CLLocationManager当时定位权限状态的获取
  • 给通用剪贴板UIPasteboard设置值、获取值。
  • UIApplication经过openURL翻开其他APP
  • CNCopyCurrentNetworkInfo获取WiFi信息
  • 给体系钥匙串keychain中设置值。

处理办法是尽量不频繁调用,或者寻觅其他的完结办法,比方NSUserDefault能够换为运用MMKV;跳转到其它APP进行共享或者第三方付出时,能够在跳转前将这个操作放到异步主行列中进行,也能防止极端状况下屡次调用呈现卡顿。

  1. 处于后台时递归调用本身办法

有个具体的比方是:有一个2S的Lotties动画,在动画完毕后3秒之后再次履行该动画,假如咱们的完结办法是如下,那么处于后台时就或许产生卡顿。

// 这个是有问题的写法,在后台时也会一直递归调用本身
private func beginCallCarAnimation() {
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        // 让 callCarBtnAnimation开端一个2s的lotties动画
        self.callCarBtnAnimation.play()
    }
    // 敞开延时等待5s后调用本身办法再次敞开动画
    DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
        self?.beginCallCarAnimation()
    }
}

处理办法为, 将这个5S的延时放到动画完毕后,因为动画形式设置为了pauseAndRestore, 在进入后台时会保存状态,进入前台时恢复动画,履行完动画才调用closure

private func beginCallCarAnimation() {
    // 留意:假如进入后台动画会中止,下次回来扫光完毕后才开端等3秒
    self.callCarBtnAnimation.play { finished in         guard finished else { return }
        // 动画完毕后,3秒后再次履行动画
        // 因为动画设置的后台形式为pauseAndRestore,这样就能确保在后台时不会递归履行
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in             self?.beginCallCarAnimation()
        }
    }
}
  1. 初始化比较大的Lotties动画

在Lotties动画文件较大时,在一些状况下也是有必要将动画文件的解析放到异步子线程中,完结后再回调主线程初始化视图。如下是一个比方:

// 串行行列去履行动画文件加载和解析,串行行列也写在全局引证的当地
DispatchQueue(label: "com.xxx.caton").async {
    // 在子线程加载解析动画文件
    let animation = Animation.named("lotties文件名")
    let provider = BundleImageProvider(bundle: Bundle.main, searchPath: nil)
    DispatchQueue.main.async { 
        // 回调主线程初始化动画视图
        let animationView = AnimationView.init(animation: animation, imageProvider: provider)
        animationView.loopMode = .playOnce
        animationView.backgroundBehavior = .pauseAndRestore
        self.addSubview(animationView)
        animationView.snp.makeConstraints { ... }
    }
}
  1. 监听体系告诉后履行太拥挤

APP中监听进入前台、后台、内存正告告诉的当地许多,导致告诉一来,CPU就上升,咱们能够恰当得将一些处理放到一个串行子行列中完结,假如是耗时的操作,在进入后台时,能够运用敞开后台使命的API来完结。

  1. 打印函数

项目中有大量的调用NSLogprintdebugPrint,在线上是会有卡顿问题上报的。实践上,抛开卡顿,release环境下也是不需求控制台打印的,咱们应该屏蔽。

关于OC中能够运用宏界说NSLog来处理。关于Swift来说,print和debugPrint都会在release下打印,应该封装运用自己的打印办法,而且宏界说在release下不生效。

六、总结

关于APP,卡顿管理是一件长期的事情,需求定好一个战略分期有序进行,这样下来也能看到每期的作用和价值。关于个人,在处理卡顿问题时,会遇到一些疑问卡顿,或许需求花费许多时刻查阅API文档、网上材料乃至是查源码才干处理,但这个过程也让咱们获得成长。

参考:

iOS 坚持界面流畅的技巧

iOS-runloop-ibrime

iOS内存abort(Jetsam) 原理探求

进程间同步通讯官方API文档

从底层剖析一下存在跨进程通讯问题的 NSUserDefaults