0%

iOS ANR

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次的最近调用栈。