1. 前言
在 iOS 利用程序开发过程中,咱们难免会碰到因各种异样而导致应用程序解体的状况。
对于开发过程中遇到的解体,咱们能够依据本地解体信息疾速定位问题。但对于线上版本产生的一些解体状况,咱们只能通过收集解体信息来剖析具体的起因。尽管 Apple 提供了解体信息上报的性能,然而并非所有的用户都开启了该性能。因而,对于数据采集 SDK 来说,采集解体信息并上报是一项必不可少的性能。
上面针对神策剖析 iOS SDK 解体采集模块进行解析,心愿可能给大家提供一些参考。
2. 解体类型
采集应用程序的解体信息,次要分为以下两种场景:
NSException 异样;
Unix 信号异样。
设计解体采集计划之前,咱们无妨先认识一下 NSException 和 Unix 信号。
2.1. NSException
NSException 是 Foundation 框架提供的一个类。用于封装一些异样信息,在须要的时候向外抛出。封装的异样信息包含异样名称、异样起因、调用堆栈。
@interface NSException : NSObject <NSCopying, NSSecureCoding> @property (readonly, copy) NSExceptionName name;@property (nullable, readonly, copy) NSString reason;@property (readonly, copy) NSArray<NSString *> callStackSymbols; @end
在 iOS 应用程序中,最常见的就是通过 @throw 抛出的异样,如图 2-1 所示:
图 2-1 异样解决流程(图片来源于 Apple 开发者文档)
比方常见的数组越界拜访异样:
@throw [NSException exceptionWithName:@”NSRangeException” reason:@”index 2 beyond bounds [0 .. 1]” userInfo:nil];
运行程序会呈现如下异样信息:
Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘index 2 beyond bounds [0 .. 1]’terminating with uncaught exception of type NSException
2.2. Unix 信号
在 iOS 零碎主动采集的解体日志中,常常能够看到相似上面的日志:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)Exception Subtype: KERNINVALIDADDRESS at 0x0000000001000010VM Region Info: 0x1000010is not in any region. Bytes before following region: 4283498480REGION TYPE START – END [VSIZE] PRT/MAX SHRMOD REGION DETAIL UNUSED SPACE AT START TEXT 0000000100510000-0000000100514000 [16K] r-x/r-x SM=COW.app/EkuaibaoTermination Signal: Segmentation fault: 11Termination Reason: Namespace SIGNAl, Code 0xb Terminating Process: exc handler [21776]Triggered by Thread: 9
其中,Exception Type 中的两个字段 EXC_BAD_ACCESS 和 SIGSEGV 别离指 Mach 异样和 Unix 信号。
那什么是 Mach 异样和 Unix 信号呢?
Mach 是 macOS 和 iOS 操作系统的微内核,Mach 异样是最底层的内核级异样。Mach 异样会被转换成相应的 Unix 信号,并传递给出错的线程。上述 Exception Type 中的 EXC_BAD_ACCESS 是 Mach 层的异样,被转换成了 Unix 信号 SIGSEGV,而后传递给出错的线程。之所以会将 Mach 异样转换成 Unix 信号,是为了兼容 POSIX 规范(SUS 标准),这样一来,开发者即便不理解 Mach 内核也能够通过 Unix 信号的形式进行兼容开发。
Unix 信号的品种有很多,在 iOS 应用程序中,常见的 Unix 信号有如下几种:
SIGILL:程序非法指令信号,通常是因为可执行文件自身呈现谬误,或者试图执行数据段。堆栈溢出时也有可能产生该信号;
SIGABRT:程序停止命令停止信号,调用 abort 函数时产生该信号;
SIGBUS:程序内存字节地址未对齐停止信号,比方拜访一个 4 字节长的整数,但其地址不是 4 的倍数;
SIGFPE:程序浮点异样信号,通常在浮点运算谬误、溢出及除数为 0 等算术谬误时都会产生该信号;
SIGKILL:程序完结接管停止信号,用来立刻完结程序运行,不能被解决、阻塞和疏忽;
SIGSEGV:程序有效内存停止信号,即试图拜访未调配的内存,或向没有写权限的内存地址写数据;
SIGPIPE:程序管道破裂信号,通常是在过程间通信时产生该信号;
SIGSTOP:程序过程停止信号,与 SIGKILL 一样不能被解决、阻塞和疏忽。
神策剖析 iOS SDK 针对 NSException 异样和 Unix 信号异样设计并实现了一套实用于数据分析的解体采集计划。
3. NSException 异样采集
3.1. 计划简介
NSException 类中定义的 NSSetUncaughtExceptionHandler 能够设置全局异样处理函数。因而,咱们能够先通过 NSSetUncaughtExceptionHandler 设置的函数来解决异样,而后收集异样堆栈信息并触发相应的事件($AppCrashed),来实现 NSException 异样的埋点。
NSSetUncaughtExceptionHandler 函数接管一个 C 语言函数的指针,函数定义如下:
typedef void NSUncaughtExceptionHandler(NSException exception); FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler _Nullable);
3.2. 具体实现
设计采集 $AppCrashed 事件的办法,将堆栈信息记录到事件属性 app_crashed_reason 中:
- (void)sa_handleUncaughtException:(NSException *)exception {// 采集 $AppCrashed 事件 SensorsAnalyticsSDK *sdk = [SensorsAnalyticsSDK sharedInstance]; if (sdk.configOptions.enableTrackAppCrash) {NSMutableDictionary *properties = [[NSMutableDictionary alloc] init]; if ([exception callStackSymbols]) {// 若有异样堆栈信息即获取异样堆栈信息 NSString *exceptionStack = [[exception callStackSymbols] componentsJoinedByString:@”\n”]; // 采集应用程序解体起因 [properties setValue:[NSString stringWithFormat:@”Exception Reason:%@\nException Stack:%@”, [exception reason], exceptionStack] forKey:@”app_crashed_reason”]; } else {// 若无异样堆栈信息即获取线程堆栈信息 NSString *exceptionStack = [[NSThread callStackSymbols] componentsJoinedByString:@”\n”]; // 采集应用程序解体起因 [properties setValue:[NSString stringWithFormat:@”%@ %@”, [exception reason], exceptionStack] forKey:@”app_crashed_reason”]; } // 触发 $AppCrashed 事件 [sdk trackPresetEvent:SA_EVENT_NAME_APP_CRASHED properties:properties]; } NSSetUncaughtExceptionHandler(NULL);}
创立 SensorsAnalyticsExceptionHandler 类并新增 + sharedHandler 办法:
- (instancetype)sharedHandler {static SensorsAnalyticsExceptionHandler *gSharedHandler = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ gSharedHandler = [[SensorsAnalyticsExceptionHandler alloc] init]; }); return gSharedHandler;}
实现 SensorsAnalyticsExceptionHandler 类的初始化办法 – init,设置全局异样处理函数并触发 $AppCrashed 事件:
- (instancetype)init {self = [super init]; if (self) {[self setupHandlers]; } return self;} – (void)setupHandlers {// 设置全局异样处理函数 NSSetUncaughtExceptionHandler(&SAHandleException);} static void SAHandleException(NSException exception) {SensorsAnalyticsExceptionHandler handler = [SensorsAnalyticsExceptionHandler sharedHandler]; // 解决捕捉的 NSException 异样,触发 $AppCrashed 事件 [handler sa_handleUncaughtException:exception];}
在 SensorsAnalyticsSDK 类的 – initWithConfigOptions:debugMode: 办法中初始化 SensorsAnalyticsExceptionHandler 类的单例对象:
- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {self = [super init]; if (self) {// 开启解体采集性能 if (_configOptions.enableTrackAppCrash) {[[SensorsAnalyticsExceptionHandler sharedHandler]; } } return self;}
3.3. 计划优化
在理论开发过程中,可能会集成多个 SDK,如果这些 SDK 都依照下面介绍的办法采集异样信息,总会有一些 SDK 采集不到异样信息。这是因为通过 NSSetUncaughtExceptionHandler 函数设置的是一个全局异样处理函数,前面设置的异样处理函数会主动笼罩后面设置的异样处理函数。
那么如何解决这个问题呢?
常见的做法是:在调用 NSSetUncaughtExceptionHandler 函数设置全局异样处理函数之前,先通过 NSGetUncaughtExceptionHandler 函数获取之前已设置的异样处理函数并保留,在解决完异样信息后,再被动调用已保留的处理函数,即可解决下面提到的笼罩问题。
新增一个 NSUncaughtExceptionHandler 类型的属性 defaultExceptionHandler,用来保留之前曾经设置的异样处理函数:
@property (nonatomic) NSUncaughtExceptionHandler *defaultExceptionHandler; – (void)setupHandlers {// 备份之前设置的异样处理函数 _defaultExceptionHandler = NSGetUncaughtExceptionHandler(); // 设置全局异样处理函数 NSSetUncaughtExceptionHandler(&SAHandleException);}
触发 $AppCrashed 事件后调用之前已设置的异样处理函数,传递 UncaughtExceptionHandler:
static void SAHandleException(NSException *exception) {// 解决捕捉的 NSException 异样,触发 $AppCrashed 事件 // 传递 UncaughtExceptionHandler if (handler.defaultExceptionHandler) {handler.defaultExceptionHandler(exception); }}
通过下面的解决,即可把所有的异样处理函数造成链条,确保之前设置的异样处理函数也能采集到异样信息。
4. Unix 信号异样采集
4.1. 计划简介
在 iOS 应用程序中,个别状况下会采集 SIGILL、SIGABRT、SIGBUS、SIGFPE 和 SIGSEGV 这几个常见的信号,即能满足日常采集应用程序异样信息的需要。咱们能够先新增信号处理函数,而后注册信号处理函数,应用 Unix 信号信息结构一个 NSException 对象,复用上节采集 $AppCrashed 事件的办法。
4.2. 具体实现
新增捕捉 Unix 信号的处理函数:
static NSString const UncaughtExceptionHandlerSignalExceptionName = @”UncaughtExceptionHandlerSignalExceptionName”;static NSString const UncaughtExceptionHandlerSignalKey = @”UncaughtExceptionHandlerSignalKey”; static void SASignalHandler(int crashSignal, struct __siginfo info, void context) {SensorsAnalyticsExceptionHandler handler = [SensorsAnalyticsExceptionHandler sharedHandler]; // 将 Unix 信号异样结构成 NSException 异样 NSDictionary userInfo = @{UncaughtExceptionHandlerSignalKey: @(crashSignal)}; NSString reason = [NSString stringWithFormat:@”Signal %d was raised.”, crashSignal]; NSException exception = [NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName reason:reason userInfo:userInfo]; // 解决捕捉的 Unix 信号异样,触发 $AppCrashed 事件 [handler sa_handleUncaughtException:exception];}
注册信号处理函数:
- (void)setupHandlers {// 备份和设置 NSException 全局异样处理函数 // 定义信号集构造体 struct sigaction action; // 将信号集初始化为空 sigemptyset(&action.sa_mask); // 在处理函数中传入 __siginfo 参数 action.sa_flags = SA_SIGINFO; // 设置信号处理函数 action.sa_sigaction = &SASignalHandler; // 定义须要采集的信号类型 int signals[] = {SIGABRT, SIGILL, SIGSEGV, SIGFPE, SIGBUS}; for (int i = 0; i < sizeof(signals) / sizeof(int); i++) {struct sigaction prev_action; int err = sigaction(signals[i], &action, &prev_action); if (err) {SALogError(@”Errored while trying to set up sigaction for signal %d”, signals[i]); } }}
留神:因为 Unix 信号异样对象是咱们本人构建的,因而没有堆栈信息,这里默认获取以后线程的堆栈信息。上节 – sa_handleUncaughtException: 办法中曾经解决该逻辑。
4.3. 计划优化
同样,为了防止影响其余 SDK 捕捉 Unix 信号,咱们该当在解决 Unix 信号之前保留曾经设置的 Unix 信号异样处理函数。而后,在解决完异样信息后再被动调用保留的 Unix 信号异样处理函数。传递 Unix 信号的逻辑与上节传递 UncaughtExceptionHandler 相似。
新增一个属性 prev_signal_handlers,用来保留之前曾经设置的 Unix 信号异样处理函数:
@property (nonatomic, unsafe_unretained) struct sigaction prev_signal_handlers; – (void)setupHandlers {// 备份和设置 NSException 全局异样处理函数 // 注册信号集 struct sigaction action; sigemptyset(&action.sa_mask); action.sa_flags = SA_SIGINFO; action.sa_sigaction = &SASignalHandler; int signals[] = {SIGABRT, SIGILL, SIGSEGV, SIGFPE, SIGBUS}; for (int i = 0; i < sizeof(signals) / sizeof(int); i++) {struct sigaction prev_action; int err = sigaction(signals[i], &action, &prev_action); if (err == 0) {char address_action = (char )&prev_action; // 保留 Unix 信号异样处理函数 char address_signal = (char *)(_prev_signal_handlers + signals[i]); strlcpy(address_signal, address_action, sizeof(prev_action)); } else {SALogError(@”Errored while trying to set up sigaction for signal %d”, signals[i]); } }}
触发 $AppCrashed 事件后向之前保留的异样处理函数传递 Unix 信号并调用:
static void SASignalHandler(int crashSignal, struct __siginfo info, void context) {// 解决捕捉的 Unix 信号异样,触发 $AppCrashed 事件 // 获取异样处理函数并其传递 Unix 信号 struct sigaction prev_action = handler.prev_signal_handlers[crashSignal]; if (prev_action.sa_flags & SA_SIGINFO) {if (prev_action.sa_sigaction) {prev_action.sa_sigaction(crashSignal, info, context); } } else if (prev_action.sa_handler && prev_action.sa_handler != SIG_IGN) {// SIG_IGN 示意疏忽信号 prev_action.sa_handler(crashSignal); }}
留神:如果其余 SDK 在解决 Unix 信号时疏忽了某个信号,那么在触发 $AppCrashed 事件后该当防止向其传递疏忽的 Unix 信号,咱们在调用 sa_handler 函数时做了判断以解决该逻辑。
5. 补发退出事件
一旦程序产生异样,咱们就采集不到 App 退出事件($AppEnd)。这样会造成在用户的行为序列中,呈现 App 启动事件($AppStart)和 App 退出事件($AppEnd)不成对的状况。因而,在应用程序产生解体时,咱们须要补发 $AppEnd 事件:
- (void)sa_handleUncaughtException:(NSException *)exception {// 采集 $AppCrashed 事件 // 补发 $AppEnd 事件 if (![sdk isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppEnd]) {[SACommonUtility performBlockOnMainThread:^{ if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {[sdk trackAutoEvent:SA_EVENT_NAME_APP_END properties:nil]; } }]; } // 阻塞以后线程,实现 serialQueue 中数据相干的工作 sensorsdata_dispatch_safe_sync(sdk.serialQueue, ^{});}
在进行这样的解决之后,当应用程序产生异样时,咱们不仅能够采集 $AppCrashed 事件,还能失常采集 $AppEnd 事件。
6. 总结
本文次要介绍了神策剖析 iOS SDK 解体采集模块的具体实现。SDK 解体采集涵盖了 NSException 异样和 Unix 信号异样,具体的实现能够参考 iOS SDK 源码。
最初,心愿通过这篇文章,大家可能对神策剖析 iOS SDK 的解体模块有一个零碎的理解。
7. 参考文献
https://developer.apple.com/d…
https://developer.apple.com/l…
https://mp.weixin.qq.com/s/hO…
https://zh.wikipedia.org/wiki…
https://blog.51cto.com/arthur…
https://github.com/sensorsdat…
文章起源:公众号神策技术社区