XXPerformanceMonitor是一个Swift版轻量卡顿监控工具,支持主线程和子线程,一句代码便可轻松集成,开源在蜗牛的Github,能够结合代码来阅读本文。ios
曾几什么时候跟项目大佬有过这样的对话git
大佬:最近有用户反馈用起来卡卡的,不太流畅,有找到缘由吗? 蜗牛:没找到,用户的操做路径太泛,没有复现。github 大佬:那你想一想办法如何监控线上卡顿吧。swift 蜗牛:.....api |
行吧,那就本身撸一个。缓存
这时候可能有小机灵举手问了:国内主流集成平台如友盟、听云、Bugly等均有卡顿监控,为啥还要本身开发?安全
由于想装逼。性能优化
开个玩笑,其实是由于公司项目处于隐私合规考虑,没有使用国内平台而使用了Fabric
,但它又没有提供卡顿监控这部分功能,否则你觉得蜗牛闲的蛋疼→_→。bash
基于咱们的项目来看,用户在使用上会感受到卡顿的场景,主要分为两种:微信
很明显第一种状况最为致命,卡顿监测工具首先可以监测主线程阻塞,而且可以及时打印主线程上的方法栈,上传到统计平台便于开发者修复。
那么接下来就有两个问题须要考虑,如何监测线程阻塞和收集方法栈并上传。
较早以前蜗牛就本身写过runloop
版的实现,其实这也算是卡顿监控的标准答案,微信很早就是经过runloop
来实现的,可能有些同窗还看过那篇博文,后来微信推广到了整个Bugly平台。
此次有时间去作这个事情,并不想拿着之前的代码再修修补补,通过一两天的海选,最终有四种实现方式在等待亮灯:
fps | ping | runloop | hook msgSend | |
---|---|---|---|---|
卡顿反馈 | 高,但在table滑动、转场动画等状况也会有下滑,会收集较多无用记录 | 高,能有效收集主线程卡顿,且能够控制卡顿阈值 | 中高,监控状态切换耗时,但timer、dispatchMain、source1事件可能反馈不到位 | 极高,可能会采集到大量系统方法消耗 |
采集精度 | 低,需cpu空闲时才能回调,栈信息采集不够及时 | 高,卡顿时能准确获取到栈信息 | 中高 | 极高,只要是方法耗时,均会拦截 |
性能损耗 | 中低,闲置时会频繁唤醒runloop处理 | 中,须要一个常驻子线程 | 低,仅监控runloop状态 | 高,任意方法均会hook,处理量太大 |
开发成本 | 低,使用CADisplayLink实现 | 低,常驻子线程ping主线程,及时释放临时变量 | 中低,实现代码相对较多 | 中高,依赖runtime,需使用OC编写 |
fps
的方案可能有些同窗比较熟悉,由于这是一个监测页面流畅度的比较常见的手段(有兴趣的同窗能够谷歌,烂大街了不必再写),可是精度实在是比较低,而且不支持子线程,不符合咱们的要求。
runloop
的方式也不错,可是最关键的一点,咱们平常的多线程开发,使用最多的是GCD
和OperationQueue
,这二者都是本身维护的线程池,咱们无法插手,想要监控子线程,还得使用Thread
来开发多线程,我选择狗带。
若是有同窗对
runloop
的方案感兴趣,能够移步iOS开发小记-RunLoop篇,在实际应用中有相关介绍及核心代码。
hook msgSend
的方案是我惟一没有实践的,其实runtime
实现上问题却是不大,可是一想到全部方法都被hook,而后先后添加耗时打印,程序一运行起来无很多天志满屏飞就头大,而且该方案肉眼可见的性能损耗,不予考虑。
ping
的方案卡顿反馈、采集精度都有不错的表现,监控效果强,且性能损耗和开发成本较低,轻松支持全线程,彻底符合个人要求。
ping
的实现说白了就是线程同步,提供一个额外的worker线程去按期在目标线程里修改全局状态位,若是目标线程此时有空,必然能对标记位进行修改,若是worker线程超时发现标记位没变,那么能够推测目标线程必然仍在处理其余任务,此时上报全部线程的堆栈。
private final class WorkerThread: Thread {
// 监控间隔
private let threshold: CGFloat
// 捕获闭包
private let catchHandler: () -> Void
// 信号量控制,避免重复上报
private let semaphore = DispatchSemaphore(value: 0)
// 递归锁保证全局变量多线程安全
private let lockObj = NSObject()
private var _isResponse = true
private var isResponse: Bool {
get {
objc_sync_enter(lockObj)
let result = _isResponse
objc_sync_exit(lockObj)
return result
}
set {
objc_sync_enter(lockObj)
_isResponse = newValue
objc_sync_exit(lockObj)
}
}
init(_ t: CGFloat, _ handler: @escaping () -> Void) {
threshold = t
catchHandler = handler
super.init()
}
override func main() {
// 生命不息,监控不止
while !isCancelled {
// 及时释放临时变量
autoreleasepool {
// 全局标记位,实际上使用局部变量也能够,只要注意OC语法下在block中修改须要对局部变量声明__weak
isResponse = false
// 主线程同步标志位,同时释放信号量
DispatchQueue.main.async {
self.isResponse = true
self.semaphore.signal()
}
// 暂停指定间隔,检验此时标志位是否修改,没有修改则说明线程卡顿,须要上报
Thread.sleep(forTimeInterval: TimeInterval(threshold))
if !isResponse {
catchHandler()
}
// 避免重复上报,一次卡顿仅上报一次(这里与微信runloop方案有比较大的区别,微信会按照斐波拉契间隔重复上报)
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
}
}
}
}
复制代码
关于堆栈获取的实现能够移步戴铭-深刻剖析 iOS 性能优化,在这里就再也不赘述。同窗们也能够找找相关的开源库或者按须要改改铭神的demo,应该比较容易,若是有须要,蜗牛本身撸的swift版整理下也能够开源出来。
既然捕获到了卡顿,在捕获闭包里,咱们须要获取到全线程的堆栈信息而且上报
func handleThread(reason: String, domain: XXPerformanceMonitorDomain) {
// 1.获取堆栈
// 2.上报
}
复制代码
实际上咱们捕获回调时仍然在worker线程中,因为此时目标进程还在执行,想要更精确的结果,最好的方式就是暂停目标线程,保证此时捕获的堆栈是准确的,这里能够经过pthread_kill
来实现,大体代码以下:
// 注册signal handle
signal(CALLSTACK_SIG, thread_singal_handler)
// 捕获闭包中暂停主线程
func handleThread(reason: String, domain: XXPerformanceMonitorDomain) {
pthread_kill(threadID, CALLSTACK_SIG)
}
// 在signal handle中获取堆栈信息
func thread_singal_handler(sig: int) {
// 捕获当前堆栈
}
复制代码
可是这里当时遇到一个很是棘手的问题,因为咱们上报数据使用的是Fabric的Non-fatals
上报,在thread_singal_handler
中调用相关API上传堆栈地址数据时,老是收集到异常崩溃,因为thread_singal_handler
中须要确保safe的调用,而翻阅官方文档发现相关API在主线程多是不安全的,配合使用可能会致使偶现死锁崩溃。
最终无奈放弃了pthread_kill
的方案选择直接进行上报,实际上因为堆栈地址获取耗时并不明显,直接上报形成的偏差实际上仍是能够接受的。
问题1:捕获回调能够在目标线程中处理么?
问题2:XXPerformanceMonitor中子线程监控仅支持
OperationQueue
,若是说不支持Thread
是因为较少使用,那为何GCD
也不支持?
首先明确监测到卡顿的落点堆栈,并不必定表明最后一个调用栈单个耗时就超过了阈值,它只是表示在整个方法执行中,执行到最后一个调用栈时已经超过了阈值,因此咱们须要根据堆栈信息的上下文来分析和判断可能存在的卡顿点,不要只盯着最后一个调用栈分析。
它跟崩溃的强定位不一样,更多的只是定位到可能存在的地方,用于辅助开发者去分析。以下图
虽然最终定位的子方法c,但实际子方法b才是真正形成卡顿的缘由。
这里简单举几个栗子🌰,有兴趣的同窗欢迎留言补充:
项目里为了控制内存,在读取图片时,每每使用UIImage(contentsOfFile: )
,资源量一上来耗时每每超乎你的想象,针对该问题,有如下建议:
deinit
时手动释放通常来讲,文件io不建议在主线程操做,一是不支持文件的并发处理(多线程读单线程写),二是不方便管理,三是相对耗时。除非场景须要且没法经过其余方式实现,不然不要放在主线程。
另外主线程中同步等待文件IO也是个比较骚的操做,尽可能避免。
可能有两种状况:
文本计算单个操做耗时不算多,出现此类问题通常是使用问题,建议遇到该问题时,考虑以下两点:
自己存在线程安全的类,在实现之初内部就应该管理好线程安全,而不是随意让外部调用者决定在哪条线程来执行,很是容易形成难以发现的崩溃。
通常开发时有两种骚操做:
建议在修改时考虑:
若是咱们在开发中使用到了包含线程同步的方法,考虑下是否有主线程卡顿风险:
上线后经过数据收集和分析,发现了很多以前不易排查的卡顿点,而且经过两三个版本的迭代优化,将主线程上卡顿率由1.1%降低至0.6%,减小45%,总体效果仍是使人满意的。
原创不易,文章有任何错误,欢迎批(feng)评(kuang)指(diao)教(wo),顺手点个赞,不甚感激! |