iOS动态化Crash防御

2020-05-12

背景

目前关于app crash的处理系统,大致都是crash产生了,统计crash信息并且上报到MAT平台这么一个流程。
关于app 运行时的crash,我们是不是可以做的更多?是否可以做到实时抓取app运行时产生的crash,然后直接自动修复它,从而不让app crash呢?
动态Crash防御就是干这件事的,APP运行时Crash自动防护功能,为app的流程顺利运行保驾护航!
所有防御的Crash都会上报到后端,并及时通知到owner,快速在版本迭代过程中fix。

原理

为了降低app的crash率。利用Objective-C语言的动态特性,采用AOP面向切面编程的设计思想,做到无痕植入。能够自动在app运行时实时捕获导致app崩溃的破环因子,然后通过特定的技术手段去化解这些破坏因子,使app免于崩溃,照样可以继续正常运行,再将crash的具体信息提取出来,实时上报给开发者,为app的持续运转保驾护航。
目前防护系统可以覆盖到8种常见类型的Crash,分别为:

  • unrecognized selector crash (函数没实现)
  • AddSelfSubView (添加自己作为subview)
  • ArrayContainer (数组越界)
  • DictionarContainer crash(字典插nil等)
  • NSString crash (字符串操作的crash)
  • Bad Access crash (野指针)
  • UI not on Main Thread Crash (非主线程刷UI )
  • List View Refresh Crash (列表刷新时,cell不存在)
    NSNotification 因为iOS9以上系统有优化,所有暂不需要保护
    NSTimer iOS10系统提供了block方式,弱引用方式比较成熟,暂不考虑保护
    KVO iOS11以后系统有优化,且目前都是用RAC,有保护。暂不处理

一,ArrayContainer和DictionarContainer

Container类型的crash 指的是容器类的crash,常见的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache /NSSet 的crash。一些常见的越界,插入nil,等错误操作均会导致此类crash发生。
该类crash虽然比较容易排查,但是其在app crash概率总比还是挺高。
防护:
Container crash 类型的防护方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,从而让这些API变的安全。

二,unrecognized selector crash
unrecognized selector类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法,或者不存在的方法导致的。
拦截调用的整个流程即Objective——C的消息转发机制。其具体流程如下图:
图片

由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:
1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2、调用forwardingTargetForSelector让别的对象去执行这个函数
3、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。
如果都不中,调用doesNotRecognizeSelector抛出异常。

unrecognized selector crash 防护方案:

  1. resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
  2. forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
  3. forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
    所以最终要Hook以下两个函数:
  • methodSignatureForSelector:(SEL)aSelector
  • forwardInvocation:(NSInvocation *)anInvocation
    会出现三种情况:
  1. 正常有方法签名的,按照正常方法调用流程走
  2. 没有方法签名的,没有实现,给出一个我们自定义的签名v@:@,并走到forwardInvocation方法记录错误
  3. 有方法签名,但是没有实现,用自己的方法签名,并走到forwardInvocation方法记录错误

三,String crash
和Container的防护方案类似,这里也不展开来描述了。

四,Bad Access crash
1.Hook住dealloc方法
2.如果当前示例在黑名单里,就把当前示例加入集合,并把当前对象objc_destructInstance清理引用关系,并未真正释放内存,并将object_setClass设置成自己的中间对象
3.Hook中间对象的方法,收到的消息都由中间对象来处理
4.维护的野指针集合,要么根据个数来维护,要么根据总大小来维护,当满了,就需要真正释放对象内存free(obj)
存在的问题:

  • 需要单独的内存那些问题对象
  • 最后释放内存后,再访问时会闪退,这个方法只是一定程度延迟了闪退时间
  • 需要后台维护黑名单机制,来指定那些问题对象

五,UI not on Main Thread Crash
在非主线程刷UI将会导致app运行crash,有必要对其进行处理。
目前初步的处理方案是swizzle UIView类的以下三个方法:

1
2
3
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

在这三个方法调用的时候判断一下当前的线程,如果不是主线程的话,直接利用

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{ 
//调用原本方法
});

来将对应的刷UI的操作转移到主线程上,同时统计错误信息。

六,List View Refresh Crash和AddSelfSubView
和Container的防护方案类似,hook函数reloadSections和AddSubview等,这里也不展开来描述了。

数据收集结果

图片

图片