性能优化之Crash收集与防护

177 阅读11分钟

背景

crash指标是项目稳定性的一个重要指标,本文主要针对crash崩溃原理以及防护做了总结和初步研究。

如何让crash变得可控

针对这个问题我们需要从以下几方面着手

  • 崩溃发生的原因有哪些
  • 崩溃底层触发流程
  • 崩溃的归类
  • 怎么防护崩溃

一.崩溃发生的原因有哪些

1.cpu无法执行的代码

  • 执行了非法指令,通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号
  • 无效指令或操作、访问无效地址及不具有权限的内存地址、除以0等 
  • 僵尸对象,野指针导致的访问无效内存地址等等

2.被系统强杀

1.应用内存消耗过高 

2.主线程长时间无法响应 

  • 操作系统采用看门狗来监控应用程序的响应能力。如果应用程序没有响应,看门狗会终止它,这会使用0x8badf00d终止原因中的代码创建崩溃报告

3.资源异常 

  • 1.线程频繁唤醒 ,Wakeups 是“资源异常”下的一个子类,指的是频繁唤醒线程,消耗cpu资源并增加 功耗,在超过阈值并处于 FATAL CONDITION 的条件下触发崩溃;如果300秒内的 总wakeup数超过45000(300 * 150)就会被判定为超出阈值;
  • 2.进程中的线程过多的占用了cpu,限制为 50% ,时间不超过 180秒 ;
  • 3.线程短时间过多的磁盘写入
  1. 死锁
  2. 非法的应用签名 6.后台执行超时 
  • 1.App退至后台后若执行时间过⻓就会导致被系统被杀,比如 Backgroud Task 方式可以在后台 执行3min,若超过3min还未运行完成就会被系统强杀

7.设备总内存吃紧

  • 因为Mac平台存在内存交换机制,而iOS平台没有,就导致整个设备内存吃紧的时候,系统就 会杀掉优先级不高且占用内存多大的应用 

8.设备过热

  • 一般见于低端的设备

3.语言触发异常

1.OC语言抛出异常 

  • 1.数组越界
  • 2.未找到的方法或者 NSDictionary 添加 nil 对象等导致的非法参数异 常 NSInvalidArgumentException 
  • 3.KVC未找到相对应的 key 抛出 NSUnknownKeyException )
  1. C++抛出异常
  • 如图为c++环境下的一些异常类

image.png 语言异常抛出后最后都会调用到 abort 来终止应用,调用栈如下图所示

image.png

4.开发者触发 (断言, NSAssert 或者 asset 函数等 )

5 …(其他)

二.崩溃如何发生

大部分应用程序都可能崩溃,在Unix中崩溃和一个信号有关系,但是追究其根本原因,是来自于内核,内核发现进程无法继续执行时,生成这个信号作为最后的补救办法。

系统是如何把异常转换成singnal呢

Mach->singnal->异常

程序的崩溃都会转换为异常,被cpu通过中断向量表指定的异常类型捕获,进而触发异常处理程序处理, 比如cpu无效指令、无效的地址或者无权限的访问,这些都是硬件产生的异常;被系统强杀的崩溃最终 会调用到 kill 函数发送 SIGKILL 信号进而引发应用被强杀;语言及开发者触发崩溃最终会通过 abort 函数毅然会最终调用到 kill 函数发送 SIGABRT 信号引发应用被杀,这都是软件引发的异常

Mach层

Mach在系统中处于最接近底层的模块,是XNU内核的内核,被BSD包裹。Mach内核作为系统一个底层的基础,仅与驱动操作系统所需的最低需要有关。 其他所有内容都由操作系统的更高层来实现,然后再利用Mach并以其认为合适的任何方式对其进行操作。

image.png

image.png

整个操作系统核心包括了 Mach 微内核、BSD层(大家熟悉的 POSIX 接口就在这一层)、I/O Kit设备驱动框 架以及核心库,其中 Mach 微内核负责进程和线程抽象、虚拟内存管理、任务管理以及进程间通信和消息 传递机制,所以 Mach 微内核就是整个操作系统的核心。

如runloop mach port 的消息就是基于mach微内核的消息机制的体现 

Mach 异常是在已有的消息传递架构上实现的一种独有的异常处理方法,是一种轻量级的架构。

信号处理流程图

  • 异常封装、转换、发送。
  • 异常消息接收处理。
  • 异常消息转换为BSD信号。

image.png

三.崩溃的归类

根据上图可以总结为以下几大类

  • 1.mach exception
  • 2.signal(BSD信号)
  • 3.NSException
  • 4.其他 如c++异常 和 主线程死锁

最终不管是硬件异常还是语言异常及用户抛出异常都会产生信号,那信号与Mach异常有何关 系?首先来带大家了解下什么是 Mach 异常。

1.mach 异常

  • Mach 异常是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常

  • Mach异常由处理器陷阱引发,在异常发生后会被异常处理程序转换成Mach消息,接着依次投递到thread、task和host端口。如果没有一个端口处理这个异常并返回KERN_SUCCESS,那么应用将被终止。每个端口拥有一个异常端口数组,系统暴露了后缀为task_set_exception_ports的多个API让我们注册对应的异常处理到端口中。

image.png

Mach异常处理流程

针对mach 异常处理参考开源库有完整的crash处理流程。 github.com/kstenerud/K…

KSCrash处理流程

  • 备份当前异常端口
  • 分配新的异常端口并设置给task作为新的接收异常的端口
  • 创建两个线程用于监听异常端口
  • 线程中监听并处理异常
1.备份当前异常端口

image.png

2.分配新的异常端口并设置给task作为新的接收异常的端口

image.png

3.创建两个线程用于监听异常端口,线程中监听并处理异常

image.png

image.png

4.所有Mach异常未处理,它将在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。

image.png

2.signal(UNIX信号)

信号是什么?信号是一种异步处理的软中断,内核会发送给进程某些异步事件,这些异步事件可能 来自硬件,比如除0或者访问了非法地址;也可能来自其他进程或用户输入,比如 ctrl+c ,就会产 生 SIGINT 信号由内核发送至当前终端执行进程,若进程未处理该信号,就会导致进程退
出; ctrl+\ 就会产生 SIGQUIT 退出信号来终止进程执行,并且会产生崩溃日志报告,如下图所 示:

信号(signal)是一种POSIX标准通信方式。通常用于Unix类 Unix和其他符合POSIX的操作系统。信号是发送到进程或同一进程内的特定线程以通知其事件的异步通知。

那最终信号如何处理呢? 

上面我们讲到用户态进程若指定了信号处理函数(比如 SIGINT )则可以自己来处理,若未指定呢?比较有 意思的地方开始了,内核发现信号未存在异常处理函数,就会将其抛给崩溃报告守护进
程 ReportCrash 

因此异常消息是通过 Mach 消息的形式发送出去的,那我们就可以截获这个消息

image.png

3.NSException 异常捕获(应用级别的异常)

image.png

image.png

image.png

4.c++异常

image.png

5.主线程死锁

image.png

image.png

四. 怎么防护崩溃?

  • 1.crash防护 增加程序的健壮性(代码规范,cr等等…)
  • 2.crash拦截 尽可能多的拦截所有的crash类型
  • 3.crash上报日志
  • 4.crash 后续流程 优雅的crash 提升用户体验度

crash拦截

主动防护

  • 在将要执行的api前增加异常处理判断等(如通过扩展分类添加异常判断)
  • 通过其他手段如中间类处理(如通过weakProxy 处理timer 强引用)
  • 通过proxy类处理kvc 安全添加移除
  • ……..

AOP拦截防护

  • 通过拦截调用添加异常判断,之后再调用本身的Selector

AOP拦截调用

1.Hook的类添加Category,在各个分类中通过Method Swizzling拦截容易造成崩溃的系统方法

2.将系统原有方法与添加的防护方法的selector(方法选择器)与IMP(函数实现指针)进行对调

3.然后在替换方法中添加防护操作,从而达到避免复崩溃的目的。

image.png

目前可以处理掉的crash类型

  • 1.unrecognized selector crash
  • 2.KVO
  • 3.NSNotification
  • 4.NSTimer
  • 5.Container crash(数组越界,插nil等)
  • 6.NSString crash (字符串操作的crash)
  • 7.Bad Access crash (野指针)

1.Unrecognized Selector类型crash防护

  • 原因: 通常是因为一个对象调用了一个不属于它方法的方法导致的是
  • 方法的查找流程->runtime消息转发流程->最终未处理->程序报错crash
  • 防护方案:在消息转发过程中拦截

image.png

所以当函数找不到时,runtime提供了三种方式去补救:

1.调用resolveInstanceMethod给个机会让类添加这个实现这个函数。
2.调用forwardingTargetForSelector让别的对象去执行这个函数。

3.调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

优缺点

  • resolveInstanceMethod需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的。
  • forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写。
  • forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

image.png

  • 动态创建一个桩类。

  • 动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP。

  • 将消息直接转发到这个桩类对象上。

注意:如果对象的类本事如果重写了forwardInvocation方法的话,就不应该对forwardingTargetForSelector进行重写了,否则会影响到该类型的对象原本的消息转发流程。

2.KVO crash

** crash原因**

  • 1.添加KVO重复添加观察者或重复移除观察者。
  • 2.KVO的被观察者dealloc时仍然注册着KVO导致的crash。
  • 解决方法
  • 通过中间delegate来进行管理,delegate通过建立一张map来维护KVO整个关系。

image.png

image.png

  • 由于是分类会在多个地方使用,添加移除kvo 需要加线程锁。

3.KVC

image.png

  • 1.重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key
  • 2.重写- (nullable id)valueForUndefinedKey:(NSString *)key
    1. key 为 nil 可以通过hook setValue:forKey:
  • 4.value 为nil重写setNilValueForKey:

1和2可以解决key不是对象的属性和keyPath不正确情况

4.NSNotification crash

  • 原因:由于dealloc时候没有移除通知导致。
  • 解决方法:保证dealloc移除通知。

image.png

5.NSTimer crash

1.NSTimer所产生的问题的主要原因是因为其没有再一个合适的时机invalidate。

2.同时还有NSTimer对target的强引用导致的内存泄漏问题。

解决方法一般通过中间对象 弱引用

image.png

6.Container类型,NSString 类型 crash

  • Container 类型的crash 指的是容器类的crash,常见的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常见的越界、插入nil等错误操作均会导致此类crash发生(容易排查 但是crash概率高)
  • Container crash 类型的防护方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,(或者通过添加相关分类方法处理)从而让这些API变的安全。

7.野指针crash

如果一个指针先前指向一个对象,但这个对象随后被释放了,如果该指针没有做任何的修改,导致仍然指向着那块内存地址,则该指针已成为了野指针。

image.png

image.png

可以利用dealloc方法会自动实现父类的dealloc方法的特性,hook住NSObject和NSProxy两个oc的根类的dealloc方法,然后在调剂方法中将本来即将释放的对象的isa指针改为指向我们创建的一个新的僵尸类,然后外界对这个僵尸类发送任何消息(objc_msgSend)都会走方法调用流程,可以在此流程做处理。(需要考虑Zombie池子的释放)

参考 :sindrilin.com/2017/11/01/…

总结:

image.png