卡死崩溃监控

YVTU

问题概述

iOS系统中App卡死崩溃问题无成熟解决方案,因iOS封闭生态,App层面无权限获取卡死崩溃日志。用户通常不会等待卡死超过20s,且手动关闭应用不生成日志,导致监控困难。基于卡顿监控,但难以区分轻微卡顿与严重卡死,且性能损耗大,存在误导性。

崩溃日志格式,如Exception Type: EXC_CRASH (SIGKILL)等,指出App因watchdog超时而被终止。watchdog崩溃定义是App启动、退出或响应系统事件时耗时过长,被系统强制结束。

监控原理

注册runloop观察者,检测耗时,记录调用栈,上报后台分析。长时间卡顿后,若未进入下一个活跃状态,则标记为卡死崩溃上报。

以下是一个 iOS 卡死监控的代码示例:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <execinfo.h>
#import <sys/time.h>

// 定义 Runloop 模式的枚举
typedef enum {
eRunloopDefaultMode, // 默认模式
eRunloopTrackingMode // 追踪模式
} RunloopMode;

// 全局变量,用于记录 Runloop 的活动状态和模式
static CFRunLoopActivity g_runLoopActivity;
static RunloopMode g_runLoopMode;
static BOOL g_bRun = NO; // 标记 Runloop 是否在运行
static struct timeval g_tvRun; // 记录 Runloop 开始运行的时间

// HangMonitor 类,用于监控卡死情况
@interface HangMonitor : NSObject
@property (nonatomic, assign) CFRunLoopObserverRef runLoopBeginObserver; // Runloop 开始观察者
@property (nonatomic, assign) CFRunLoopObserverRef runLoopEndObserver; // Runloop 结束观察者
@property (nonatomic, strong) dispatch_semaphore_t semaphore; // 信号量,用于同步
@property (nonatomic, assign) NSTimeInterval timeoutInterval; // 超时时间
- (void)addRunLoopObserver; // 添加 Runloop 观察者的方法
- (void)startMonitor; // 启动监控的方法
- (void)logStackTrace; // 记录调用栈的方法
- (void)reportHang; // 上报卡死的方法
@end

@implementation HangMonitor

// 单例模式,确保 HangMonitor 只有一个实例
+ (instancetype)sharedInstance {
static HangMonitor *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[HangMonitor alloc] init];
});
return instance;
}

// 初始化方法
- (instancetype)init {
self = [super init];
if (self) {
_timeoutInterval = 8.0; // 设置超时时间为8秒
_semaphore = dispatch_semaphore_create(0); // 创建信号量
[self addRunLoopObserver]; // 添加 Runloop 观察者
[self startMonitor]; // 启动监控
}
return self;
}

// 添加 Runloop 观察者的方法
- (void)addRunLoopObserver {
NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop]; // 获取当前 Runloop

// 创建第一个观察者,监控 Runloop 是否处于运行状态
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
CFRetain(beginObserver); // 保留观察者,防止被释放
self.runLoopBeginObserver = beginObserver;

// 创建第二个观察者,监控 Runloop 是否处于睡眠状态
CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
CFRetain(endObserver); // 保留观察者,防止被释放
self.runLoopEndObserver = endObserver;

// 将观察者添加到当前 Runloop 中
CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
}

// 第一个观察者的回调函数,监控 Runloop 是否处于运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
HangMonitor *monitor = (__bridge HangMonitor *)info;
g_runLoopActivity = activity; // 更新全局变量,记录当前的 Runloop 活动状态
g_runLoopMode = eRunloopDefaultMode; // 更新全局变量,记录当前的 Runloop 模式
switch (activity) {
case kCFRunLoopEntry:
g_bRun = YES; // 标记 Runloop 进入运行状态
break;
case kCFRunLoopBeforeTimers:
case kCFRunLoopBeforeSources:
case kCFRunLoopAfterWaiting:
if (g_bRun == NO) {
gettimeofday(&g_tvRun, NULL); // 记录 Runloop 开始运行的时间
}
g_bRun = YES; // 标记 Runloop 处于运行状态
break;
case kCFRunLoopAllActivities:
break;
default:
break;
}
dispatch_semaphore_signal(monitor.semaphore); // 发送信号量
}

// 第二个观察者的回调函数,监控 Runloop 是否处于睡眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
HangMonitor *monitor = (__bridge HangMonitor *)info;
g_runLoopActivity = activity; // 更新全局变量,记录当前的 Runloop 活动状态
g_runLoopMode = eRunloopDefaultMode; // 更新全局变量,记录当前的 Runloop 模式
switch (activity) {
case kCFRunLoopBeforeWaiting:
gettimeofday(&g_tvRun, NULL); // 记录 Runloop 进入睡眠状态的时间
g_bRun = NO; // 标记 Runloop 进入睡眠状态
break;
case kCFRunLoopExit:
g_bRun = NO; // 标记 Runloop 退出运行状态
break;
case kCFRunLoopAllActivities:
break;
default:
break;
}
dispatch_semaphore_signal(monitor.semaphore); // 发送信号量
}

// 启动监控的方法
- (void)startMonitor {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
while (YES) {
long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, self.timeoutInterval * NSEC_PER_SEC));
if (result != 0) {
if (g_runLoopActivity == kCFRunLoopBeforeSources || g_runLoopActivity == kCFRunLoopAfterWaiting) {
[self logStackTrace]; // 记录调用栈
[self reportHang]; // 上报卡死
}
}
}
});
}

// 记录调用栈的方法
- (void)logStackTrace {
void *callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
NSMutableString *stackTrace = [NSMutableString stringWithString:@"\n"];
for (int i = 0; i < frames; i++) {
[stackTrace appendFormat:@"%s\n", strs[i]];
}
free(strs);
NSLog(@"%@", stackTrace);
}

// 上报卡死的方法
- (void)reportHang {
// 在这里实现上报后台分析的逻辑
NSLog(@"检测到卡死崩溃,进行上报");
}

@end

// 主函数,程序入口
int main(int argc, char * argv[]) {
@autoreleasepool {
HangMonitor *monitor = [HangMonitor sharedInstance]; // 获取 HangMonitor 单例
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); // 启动应用程序
}
}

这段代码中 HangMonitor 类会在主线程的 RunLoop 活动中检测是否有长时间的卡顿,并在检测到卡顿时记录调用栈并上报后台进行分析。超时时间设定为 8 秒,以覆盖大部分用户感知场景并减少性能损耗。

On this page
卡死崩溃监控