原文连接html
如何去衡量一款应用的质量好坏?为了回答这一问题,APM
这一目的性极强的工具向开发顺应而生。最先的APM
开发只关注于crash
、cpu
这类的硬性指标。而随着移动开发市场的成熟,愈来愈多的数据指标也被加入到了APM
的采集范畴中,包括感官体验相关的数据和使用习惯等。node
然而,不管APM
最终如何发展,其最核心的采集指标必定是crash
数据。一套完善的crash
监控方案能够快速的发现并协助完成问题定位,从而可以及时止损,避免更多的损失。而反过来讲,若是crash
不能及时被发现,又或者由于采集链中出现异常致使了数据丢失,对于开发者和公司来讲,这都会是一个噩梦。linux
细分之下,crash
分别存在mach exception
、signal
以及NSException
三种类型,每一种类型表示不一样分层上的crash
,也拥有各自的捕获方式。ios
mach exception
c++
mach异常
由处理器陷阱引起,在异常发生后会被异常处理程序转换成Mach消息
,接着依次投递到thread
、task
和host
端口。若是没有一个端口处理这个异常并返回KERN_SUCCESS
,那么应用将被终止。每一个端口拥有一个异常端口数组
,系统暴露了后缀为_set_exception_ports
的多个API
让咱们注册对应的异常处理到端口中。git
mach异常
即使注册了对应的处理,也不会致使影响原有的投递流程。此外,即使不去注册mach异常
的处理,最终通过一系列的处理,mach异常
会被转换成对应的UNIX信号
,一种mach异常
对应了一个或者多个信号类型。所以在捕获crash
要提防二次采集的可能。github
NSException
api
NSException
发生在CoreFoundation
以及更高抽象层,在CoreFoundation
层操做发生异常时,会经过__cxa_throw
函数抛出异常。在经过NSSetUncaughtExceptionHandler
注册NSException
的捕获函数以后,崩溃发生时会调用这个捕获函数。但若是没有任何函数去捕获这个异常 若是在捕获函数中没有进行操做终止应用,最终异常会经过abort()
来抛出一个SIGABRT
信号。数组
因为NSException
的抽象层次足够高,相比较其余的crash
类型,NSException
是能够被人为的阻止crash
的。好比@try-catch
机制可以捕获块中发生的异常,避免应用被杀死。但因为try-catch
的开销和回报不成正比,每每不会使用这种机制。其二是crash防御
,这一手段经过hook
掉上层接口来规避crash
风险,可是只建议用于线上防御,并且hook
未必不会致使其余的问题。安全
signal
signa
会致使crash
,这是多数iOS
开发者对于信号的印象。传递crash
信息其实只是信号的一部分功能,信号是一套基于POSIX标准
开发的通讯机制,具体能够阅读Signal-wikipedia。在signal.h
中声明了32
种异常信号,下面列出一部分的信号异常对:
信号 | 异常 |
---|---|
SIGILL | 执行了非法指令,通常是可执行文件出现了错误 |
SIGTRAP | 断点指令或者其余trap指令产生 |
SIGABRT | 调用abort产生 |
SIGBUS | 非法地址。好比错误的内存类型访问、内存地址对齐等 |
SIGSEGV | 非法地址。访问未分配内存、写入没有写权限的内存等 |
SIGFPE | 致命的算术运算。好比数值溢出、NaN数值等 |
虽然存在三种crash
,但因为mach exception
会在BSD
层被转换成UNIX信号
,NSException
在未被捕获的状况下会调用abort
抛出信号,所以即使是咱们只注册了signal
的处理,只要注册的signal
足够多,理论上也是能捕获到所有的crash
。
因为crash
的捕获机制只会保存最后一个注册的handle
,所以若是项目中残留或者存在另外的第三方框架采集crash
信息时,常常性的会存在冲突。解决冲突的作法是在注册本身的handle
以前保存已注册的处理函数,便于发生崩溃后能将crash
信息连续的传递下去。
struct sigaction my_action;
static struct sigaction registered_action;
static NSUncaughtExceptionHandler *previousHandle;
void signal_handler(int signal) {
......
}
void exception_handler(NSException *exception) {
......
}
void registerCrashHandle() {
previousHandle = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&exception_handler);
myAction.sa_handler = &signal_handler;
sigemptyset(&my_action.sa_mask);
sigaction(SIGABRT, &my_action, ®istered_action);
}
复制代码
通常来讲,一个经验丰富的开发者在注册crash
回调时都会主动的去保存其余函数,避免由于冲突致使别人的数据丢失。可是即使按照这样的方式来注册你的回调,也不表明咱们的处理函数是安全的。最重要的缘由在于完成回调的注册以后,咱们没法保证后续会不会有其余人继续注册,若是有就会存在被替换掉的风险
按照正常方式的作法,能保证先于咱们注册的crash
回调不会被咱们拦截致使失败,但若是在咱们后方存在另外的注册,咱们须要一个有效的机制来保护咱们的采集数据。解决问题的收益是不变的,因此解决方案理当尽量的低开销和低风险。
如何去判断咱们的handle
是否安全?这要求咱们对已注册的handle
进行检测。首先检测时机要选择在哪?因为crash
是可能发生在应用启动阶段的,所以crash
采集通常也是发生在didLaunch
这个时间,下图是我绘制的应用启动到彻底启动的几个重要阶段:
applicationActive
这个阶段基本上是能保证crash
相关的注册都完成的,所以冲突检测能够放到这个阶段进行。
利用已有的周期性机制或者使用定时器来进行handle
冲突检测。能够分别使用通知
和定时器
两个机制来完成周期性检测方案
监听应用状态
监听UIApplicationDidBecomeActiveNotification
在应用进入活跃状态时作检测:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......
[[NSNotificationCenter defaultCenter] addObserver: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) name: UIApplicationDidBecomeActiveNotification object: nil];
......
}
static struct sigaction existActions[32];
static int fatal_signals[] = {
SIGILL,
SIGBUS,
SIGABRT,
SIGPIPE,
};
- (void)checkRegisterCrashHandler {
struct sigaction oldAction;
for (int idx = 0; idx < sizeof(fatal_signals) / sizeof(int); idx++) {
sigaction(fatal_signals[idx], NULL, &oldAction);
if (oldAction.sa_handler != &signal_handler) {
existActions[fatal_signals[idx]] = oldAction;
struct sigaction myAction;
myAction.sa_handler = &signal_handler;
sigemptyset(&myAction.sa_mask);
sigaction(SIGABRT, &myAction, NULL);
}
}
}
复制代码
定时器检测
建立定时器来进行周期性的检测,相比通知的机制,能够控制检测间隔:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
......
NSTimer *timer = [[NSTimer alloc] initWithFireDate: [NSDate date] interval: 30 target: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];
[timer fire];
......
}
复制代码
经过hook
调用注册handle
的对应函数,创建一个回调数组来保存非exception_handle
的全部回调,后续处理完咱们的采集,再逐个调起。因为捕获函数都是基于C
接口的,所以咱们须要fishhook来提供相应的hook
功能。
struct SignalHandler {
void (*signal_handler)(int);
struct SignalHandler *next;
}
struct SignalHandler *previousHandlers[32];
void append(struct SignalHandler *handlers, struct SignalHandler *node) {
......
}
static int (*origin_sigaction)(int, const struct sigaction *__restrict, struct sigaction * __restrict) = NULL;
int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
if (new_action.sa_handler != signal_handler) {
append(previousHandlers[signal], new_action);
return origin_sigaction(signal, NULL, old_action);
} else {
return origin_sigaction(signal, new_action, old_action);
}
}
复制代码
在周期性检测的方案下,假设存在handle
注册链(依次从左到右):
previous
<- exception_handle
<- other
在检测时发现当前回调是other
,因而从新注册咱们的回调,保存other
。可是假如other
也保存了咱们的回调,这样可能会致使崩溃发生的时候,调用顺序变成一个死循环。
hook
方案则是由于在调用origin_sigaction
时会传入old_action
,可能致使另外的注册者保存了咱们的exception_handle
,并在最后处理的时候出现一样的循环调用问题。对于hook
方案来讲,解决方法要简单不少,只须要在非咱们的注册调用origin_sigaction
时不传入old_action
就能保证其余注册者没法获取到咱们的回调:
int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
if (new_action.sa_handler != signal_handler) {
append(previousHandlers[signal], new_action);
return origin_sigaction(signal, NULL, NULL);
} else {
return origin_sigaction(signal, new_action, old_action);
}
}
复制代码
而使用周期性监测,就须要考虑是否放弃other
的回调,最终只保证exception_handle
和previous
和更早以前的注册可以被顺利调起。
另外,hook
还存在一个风险是假如第三方一样作了hook
掉注册函数的处理,而且作了筛选处理,最终致使的结果是没办法完成任何一个注册。两害相较取其轻,我的的建议是使用周期性检测方案。
上述的两套方案都存在风险点,并且这些风险点对于应用来讲都算是致命的。那么有没有几乎没有风险又能解决问题的办法呢?答案是确定的,那就是不要用有潜在风险的第三方,或者和第三方开发者商量提供一个无需crash
采集的版本。
在应用发生崩溃的时候,此时的崩溃所在线程
是极不稳定的,不稳定性包括几点:
内存不稳定
若是是内存相关错误引起的crash
,好比内存过载、野指针等,此时线程的内存是危险状态。若是这时候在handle
中再次分配内存,极有可能致使二次crash
死锁
大多数底层的的核心API
会涉及到加锁处理,这一状况在signal
错误中出现的较多。而做为上层调用方的咱们是不自知的,此时错误的操做可能致使线程陷入死锁状态
理论上当咱们拦截了一个signal
的时候,此时的应用会陷入内核并中止工做,应用页面卡死,这时候咱们可执行时长是无限的。若是处理链过长,耗时过多或者陷入某种循环,会形成一种应用卡死而非崩溃的错觉,而通过我厂大量的统计,应用卡死
要比应用崩溃
更让人难以接受。此外,过多的处理链会增长回调流程上的风险点。若是链条上的某个点发生了二次崩溃,会致使后续的处理都没法执行。所以,不用第三方或者让第三方去除crash
采集,是一种可行且高效的手段。
文中提到过一次如今比较流行的crash防御
手段,这里仍是想说两句。在开发中,crash防御
会形成依赖心理,下降对风险的敏感。而在线上,这种方案可能屏蔽了大量的低级错误,也是让我不能容忍的,固然循环引用的防御属于例外。最后安利一波寒神的XXShield,除了容器类的防crash
都值得学习,尤为是正确的method swizzling
姿式。