如何去衡量一款应用的质量好坏?为了回答这一问题,
这一目的性极强的工具向开发顺应而生。最早的
- APM
开发只关注于
- APM
、
- crash
这类的硬性指标。而随着移动开发市场的成熟,越来越多的数据指标也被加入到了
- cpu
的采集范畴中,包括感官体验相关的数据和使用习惯等。
- APM
然而,无论
最终如何发展,其最核心的采集指标一定是
- APM
数据。一套完善的
- crash
监控方案可以快速的发现并协助完成问题定位,从而能够及时止损,避免更多的损失。而反过来说,如果
- crash
不能及时被发现,又或者因为采集链中出现异常导致了数据丢失,对于开发者和公司来说,这都会是一个噩梦。
- crash
细分之下,
分别存在
- crash
、
- mach exception
以及
- signal
三种类型,每一种类型表示不同分层上的
- NSException
,也拥有各自的捕获方式。
- crash
- mach exception
由处理器陷阱引发,在异常发生后会被异常处理程序转换成
- mach异常
,接着依次投递到
- Mach消息
、
- thread
和
- task
端口。如果没有一个端口处理这个异常并返回
- host
,那么应用将被终止。每个端口拥有一个
- KERN_SUCCESS
,系统暴露了后缀为
- 异常端口数组
的多个
- _set_exception_ports
让我们注册对应的异常处理到端口中。
- API
即便注册了对应的处理,也不会导致影响原有的投递流程。此外,即便不去注册
- mach异常
的处理,最终经过一系列的处理,
- mach异常
会被转换成对应的
- mach异常
,一种
- UNIX信号
对应了一个或者多个信号类型。因此在捕获
- mach异常
要提防二次采集的可能。
- crash
- NSException
发生在
- 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
开发的通信机制,具体可以阅读Signal-wikipedia。在
- POSIX标准
中声明了
- 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
接口的,因此我们需要fishhook来提供相应的
- C
功能。
- 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防护
会造成依赖心理,降低对风险的敏感。而在线上,这种方案可能屏蔽了大量的低级错误,也是让我不能容忍的,当然循环引用的防护属于例外。最后安利一波寒神的XXShield,除了容器类的防
- crash防护
都值得学习,尤其是正确的
- crash
姿势。
- method swizzling
Foundation
iOS异常捕获
libc++ api spec
Linux信号处理机制
浅谈Mach Exceptions
漫谈iOS Crash收集框架
源码剖析signal和sigaction的区别
iOS Crash捕获及堆栈符号化思路剖析
来源: https://juejin.im/post/5a30bb065188257dd239a283