iOS APP运行时Crash自动修复系统


前言

大白(Baymax),迪士尼动画《超能陆战队》中的健康机器人,是一个体型胖胖的充气机器人,因呆萌的外表和善良的本质获得大家的喜爱,被称为“萌神”。

Baymax项目是为了减少开发人员在开发中一些不规范的代码编写造成的内存泄露,界面卡顿,耗电等问题而来的一个监控系统。

现在Baymax迎来了它新的功能:APP运行时Crash自动防护功能,为app的流程顺利运行保驾护航!

下面将详细介绍一下 APP运行时Crash自动修复系统 开发的目的,设计的原理以及使用的方法。

APP运行时Crash自动修复系统

Chapter 1 - 开发目的

是否存在这样的夜晚,当刚刚躺下准备美美的睡一觉的时候, 突然来一记夺命电话Call,一接起来发现是你老板!!!“小王啊,刚刚上线的X.X.X版本出问题了啊,怎么样操作会crash啊,导致新功能都无法使用了,快定位一下是什么原因,抓紧hotpatch修复一下啊!”。心里一万头草泥马呼啸而过,瞬间已经满头大汗的你却还要故作镇静地回答:“嗯,老板我马上去看看,一定努力解决问题!” 急忙打开电脑的你,知道今夜注定无眠了。

是否又存在这样的情形,你老板把大家都聚起来开了一个年初KPI目标制定会议,说到:“作为一个资深的技术团队,app性能是我们技术团队首抓的目标,其中很最要的一项就是app的崩溃率,去年我们app统计出来的崩溃率是千分之五,而我们的竞争对手的崩溃率只有万分之五,相差了10倍!今年我们要赶超他们,最起码也要和他们持平。” 你甚是赞同,但是你心里却又有点怀疑,对方的开发资源是我们的好几倍而且个个都是资深老司机,我们团队里却大多都是应届生小鲜肉,这KPI能完成么?

如果你遇到过以上的情况并且对此深表头痛的话,那么 大白健康系统--APP运行时Crash自动修复系统 将会是你的不二选择!

APP运行时Crash自动修复+捕获系统 的设计初衷,就是为了降低app的crash率。利用Objective-C语言的动态特性,采用AOP(Aspect Oriented Programming) 面向切面编程的设计思想,做到无痕植入。能够自动在app运行时实时捕获导致app崩溃的破环因子,然后通过特定的技术手段去化解这些破坏因子,使app免于崩溃,照样可以继续正常运行,为app的持续运转保驾护航。

Chapter 2 - 功能简介

APP运行时Crash自动修复系统 的主要功能,可以用一句话来简单的概括:对业务代码的零侵入性地将原本会导致app崩溃的crash抓取住,消灭掉,保证app继续正常地运行,再将crash的具体信息提取出来,实时返回给用户。

通过下面的一个小例子就可以很直观的体现出来系统的作用:

调用以下的一段代码

//test code

UIButton * testObj = [[UIButton alloc] init];
[testObj performSelector:@selector(someMethod:)];

具体crash时的表现见下图:

  image

要解决这中类型的crash,我们需要先了解清楚它产生的具体原因和流程。

3.1.2 方法调用流程

让我们看一下方法调用在运行时的过程。

runtime中具体的方法调用流程大致如下:

1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。

2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行

3.如果没找到,去父类指针所指向的对象中执行1,2.

4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。

5.如果没有重写拦截调用的方法,程序报错。

3.1.3 拦截调用

在方法调用中说到了,如果没有找到方法就会转向拦截调用。

那么什么是拦截调用呢?

拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:

- (void)addObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options 
                context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary, id> *)change context:(nullable void *)context;

关于

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(void *)context

方法改造流程如下图:

  image

移除一个keypath的Observer时,当delegate的kvoInfoMap中找不到key为该keypath的时候,说明此时delegate并没有持有对应keypath的observer,即说明移除了一个不匹配的观察者,此时如果再继续操作会导致app崩溃,所以应该及时中断流程,然后统计异常信息。

当keypath对应的KVOInfo列表(infoArray)为空的时候,说明此时delegate已经不再持有任何和keypath相关的observer了。这时应该调用原有removeObserver的方法将delegate对应的观察者移除。

注意到在检查遍历infoArray的时侯,除了要删除对应的info信息,还多了一步检查info.observer == nil的过程,是因为如果observer为nil,那么此时如果keypath对应的值变化的话,也会因为找不到observer而崩溃,所以需要做这一步来阻止该种情况的发生。

关于

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

在这三个方法调用的时候判断一下当前的线程,如果不是主线程的话,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });

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

但是真正实施了之后,发现这三个方法并不能完全覆盖UIView相关的所有刷UI到操作,但是如果要将全部到UIView的刷UI的方法统计起来并且swizzle,感觉略笨拙而且不高效。

所以作者依旧在寻找,看是否有更好的方案来解决该问题。

Chapter 4 - 使用手册

目前sdk实现了以下的功能和配置:

1. 配置需要防护的crash类型

可以根据自身需要,选择一定的crash防护配置,通过以下的接口进行配置:

- (void)configSafetyGuardService:(HTSafetyGuardType)SafetyGuardType;

其中可以配置的SafetyGuardType有:

  • HTSafetyGuardType_None

  • HTSafetyGuardType_All

  • HTSafetyGuardType_UnrecognizedSelector

  • HTSafetyGuardType_KVO

  • HTSafetyGuardType_BadAccess

  • HTSafetyGuardType_Notification

  • HTSafetyGuardType_Timer

  • HTSafetyGuardType_Container

  • HTSafetyGuardType_String

  • HTSafetyGuardType_UI

可以根据自己项目的需求自行选择需要防护的类型。

2. 实时 开启/暂停 安全防护功能

配置完毕之后,需要调用- (void)start;来开启防护,防护的开关是实时的(无需重启app),可以在任意的时刻选择 开启/关闭 防护功能。

通过 - (BOOL)isWorking 接口可以获取当前防护功能的状态。

通过 - (void)start 接口实时开启防护功能

通过 - (void)stop 接口实时关闭防护功能

3. 配置白名单和黑名单,指定对应的想 加上/去掉 安全防护功能的类和对象

由于不同类实现的特殊性,考虑到可能某些类并不需要开启防护功能。 所以提供了黑名单的功能。 在黑名单里面的类本身以及其子类,都不会进入防护的范围。

白名单的出现是因为作者在开发的时候发现一些系统自带的类是没有必要进入防护范围的,所以将整体防护的范围调整到所有用户自定义的类里面。 但是之后又发现绝大多数的crash和一些常用的系统的类(例如NSString,NSDictionary,UIView等等)有很强的联系,针对于这些常用的系统类还是很有必要开启防护的。所以针对这些需要防护的系统类,专门提供了白名单的功能。

注意:野指针类型的防护,由于其特殊性,不适用于这套白名单和黑名单。 其自身会维护一套新的黑白名单,详见:3.7 野指针类型Crash防护

4. 设置异常处理handler,指定出现crash被抓取情况之后,用户想自定义的操作

出现了crash,并且被我们的系统捕捉到加以处理之后,用户可能还需要进一步的处理,例如上传埋点等。这时可以通过设置一个handler来实现, HTExceptionHandler会将crash的信息通过HTCrashInfo的形式来返回。

HTCrashInfo内包含了:

  • 导致crash的类型:crashType

  • crash线程的调用栈:callStackSymbols

  • crash的具体描述信息:crashDescription

  • 扩展信息:userinfo

以上接口具体详细的信息均可以在(HTSafetyGuardService.h)中找到。(注意HTSafetyGuardService是单例)

推荐??:

面试题持续整理更新中,如果你想一起进阶,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!