ANR(Application Not Responding)应用程序无相应,就是我们常说的卡顿。直接原因可以是主线程阻塞,无法在规定时间内完成画面的渲染以及事件的响应。在开发过程中,遇到的造成主线程阻塞的原因可能是:
- 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据;
- 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算;
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间;
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
针对这些问题,如果我们能够捕获得到卡顿当时应用的主线程堆栈,那么问题就迎刃而解了。有了堆栈,就可以知道主线程在什么函数哪一行代码卡住了,是在等什么锁,还是在进行I/O操作,或者是进行复杂计算。有了堆栈,就可以对问题进行针对性解决。
监测原理
页面的绘制以及事件的处理,都是由主线程的 Runloop 来控制,Runloop 在一个循环的各个重要阶段都会给注册的 Observer
发送通知,可以注册对应的通知来获取一次循环 各个阶段的耗时,一次循环的耗时超过阈值,就会出现掉帧 卡顿。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| let beginObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, .min) { observer, activity in switch activity { case .entry: print("entry") case .beforeSources: print("source") case .beforeTimers: print("timer") case .afterWaiting: gettimeofday(&self.tvStart, nil) print("awake") case .beforeWaiting: print("sleep") default: print("def") } } let endObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, .max) { observer, activity in switch activity { case .entry: print("2 entry") case .beforeSources: print("2 source") case .beforeTimers: print("2 timer") case .afterWaiting: print("2 awake") case .beforeWaiting: // check var tvCur = timeval() gettimeofday(&tvCur, nil) let duration = self.getDuration(tvStart: self.tvStart, tvEnd: tvCur) print("2 sleep", duration) default: print("2 def") } } CFRunLoopAddObserver(CFRunLoopGetCurrent(), beginObserver, .commonModes) CFRunLoopAddObserver(CFRunLoopGetCurrent(), endObserver, .commonModes)
|
打印结果,可以看到每次从唤醒到休眠消耗的时间,单位是微妙:
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 30 31 32 33 34
| awake 2 awake timer 2 timer source 2 source sleep 2 sleep 6739 awake 2 awake timer 2 timer source 2 source sleep 2 sleep 189 awake 2 awake timer 2 timer source 2 source sleep 2 sleep 2057 awake 2 awake timer 2 timer source 2 source sleep 2 sleep 169
|
输出时间是微秒,上面输出最高 6739 为 6.7 毫秒
当然如果主线程卡住了,那么这种 observe
的回调也会被卡住,如果直接卡到程序崩溃,那么这次超长时间的卡顿就无法被检测到,所以需要启动一条常驻子线程,每隔一段时间获取状态,如果发现主线程卡住超过一定时间,就判定为发生了 ANR,开始获取所有线程当的调用栈、CPU使用率等数据保存至文件。
一个初步的监测类:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| class DetectorMainThread { static let shared = DetectorMainThread() private var isRunning = false @Protected private var isMainRunloopBegain = false private var tvStart: timeval = timeval() private let unfairLock: os_unfair_lock_t private let queue = DispatchQueue(label: "DetectorMainThread_Queue", attributes: .concurrent) private let semaphore = DispatchSemaphore(value: 0)
private init() { unfairLock = .allocate(capacity: 1) unfairLock.initialize(to: os_unfair_lock()) registMainRunloopObserver() }
func start() { os_unfair_lock_lock(unfairLock)
if isRunning { os_unfair_lock_unlock(unfairLock) return } isRunning = true os_unfair_lock_unlock(unfairLock) queue.async { while self.getIsRunning() { self.queue.asyncAfter(deadline: .now() + 1) { self.semaphore.signal() } self.semaphore.wait() self.check() } } } func stop() { os_unfair_lock_lock(unfairLock) isRunning = false os_unfair_lock_unlock(unfairLock) } func getIsRunning() -> Bool { os_unfair_lock_lock(self.unfairLock) var isRunning = self.isRunning os_unfair_lock_unlock(self.unfairLock)
return isRunning } private func check() { $isMainRunloopBegain.read { if $0 { var tvCur = timeval() gettimeofday(&tvCur, nil) let duration = getDuration(tvStart: tvStart, tvEnd: tvCur) print("ANR: \(Float(duration) / 1000)ms") } } }
private func registMainRunloopObserver() { let beginObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.afterWaiting.rawValue, true, .min) { observer, activity in gettimeofday(&self.tvStart, nil) self.$isMainRunloopBegain.write { $0 = true } }
let endObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue, true, .max) { observer, activity in self.$isMainRunloopBegain.write { $0 = false } }
CFRunLoopAddObserver(CFRunLoopGetMain(), beginObserver, .commonModes) CFRunLoopAddObserver(CFRunLoopGetMain(), endObserver, .commonModes) } }
|
有时候一次runloop的耗时过长 可能是由于此次runloop之前的循环引起的,所以一般来说会一直记录10-20次的最近调用栈。