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

Mrzhou
Mrzhou
Mrzhou
257
文章
1
评论
2019年11月7日03:47:24 评论 18

前言

大白(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:)];

结果肯定会导致app的崩溃,因为testObj是一个UIButton对象,而UIButton并没有实现 someMethod: 这个方法,所以向testObj发送someMethod:这个方法的时候,将会导致该方法无法在相关的方法列表里找到,最终导致app的crash。

但是通过我们的

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

可见对应的crash的信息(crash类型,原因,调用栈信息)均可以完整的打印在XCode的Console中。

说明我们的大白系统已经捕捉到了这个crash,将该crash消灭掉并且吐出来该crash的完整信息。

当然目前系统的功能并没有强大到可以把所有的crash都处理掉,不过一些常见的高频次发生的crash,系统均会针对他们一一处理。目前可以处理掉的crash类型具体有以下几种:

  • unrecognized selector crash
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • Container crash(数组越界,插nil等)
  • NSString crash (字符串操作的crash)
  • Bad Access crash (野指针)
  • UI not on Main Thread Crash (非主线程刷UI(机制待改善))

对于每种类型的crash,安全系统都采取不同的方式,进行了对应的处理。 具体的处理细节详见下章

Chapter 3 - 实现原理

前面已经提过,目前的安全防护系统可以覆盖到8中类型的Crash,分别为:

  • unrecognized selector crash
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • Container crash(数组越界,插nil等)
  • NSString crash (字符串操作的crash)
  • Bad Access crash (野指针)
  • UI not on Main Thread Crash (非主线程刷UI (机制待改善))

接下来将一一详细介绍这8种类型的Crash的防护的实现的具体原理:

3.1 Unrecognized Selector类型crash防护(Unrecognized Selector)

3.1.1 unrecognized selector crash 产生原因

unrecognized selector类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法导致的。

例如调用以下一段代码就会产生crash

//test code

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

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

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

3.1.2 方法调用流程

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

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

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

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

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

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

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

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

3.1.2 方法调用流程

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

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

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

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

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

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

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

3.1.3 拦截调用

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

那么什么是拦截调用呢?

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

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

那么什么是拦截调用呢?

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

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

拦截调用的整个流程即Objective——C的消息转发机制。其具体流程如下图:

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

由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:

1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数

2、调用forwardingTargetForSelector让别的对象去执行这个函数

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

如果都不中,调用doesNotRecognizeSelector抛出异常。

3.1.4 unrecognized selector crash 防护方案

既然可以补救,我们完全也可以利用消息转发机制来做文章。那么问题来了,在这三个步骤里面,选择哪一步去改造比较合适呢。

这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下:

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

选择了forwardingTargetForSelector之后,可以将NSObject的该方法重写,做以下几步的处理:

1.动态创建一个桩类

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

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

流程图如下:

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

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

通过重写NSObject的forwardingTargetForSelector方法,我们就可以将无法识别的方法进行拦截并且将消息转发到安全的桩类对象中,从而可以使app继续正常运行。

3.2 KVO类型crash防护(KVO)

3.2.1 KVO crash 产生原因

KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受收到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

KVO机制在iOS的很多开发场景中都会被使用到。不过如果一不小心使用不当的话,会导致大量的crash问题。所以如果能找到一种方法能够自动抓取这些由于开发者粗心所导致的KVO Crash问题的话,是有一定的价值的。

首先我们来看看通过会导致KVO Crash的两种情形:

KVO的被观察者dealloc时仍然注册着KVO导致的crash,见下图

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

添加KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)导致的crash,见下图

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

3.2.2 KVO crash 防护方案

通常一个对象的KVO关系图如下:

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

一个被观察的对象(Observed Object)上有若干个观察者(Observer),每个观察者又观察若干条KeyPath。

如果观察者和keypath的数量一多,很容易理不清楚被观察对象整个KVO关系,导致被观察者在dealloc的时候,还残存着一些关系没有被注销。 同时还会导致KVO注册观察者与移除观察者不匹配的情况发生。

笔者曾经还遇到过在多线程的情况下,导致KVO重复添加观察者或移除观察者的情况。这类问题通常多数发生的比较隐蔽,不容易从代码的层面去排查。

由上可见多数由于KVO而导致的crash原因是由于被观察对象的KVO关系图混乱导致。那么如何来管理混乱的KVO关系呢。可以让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系。如下图:

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

这样做的好处有两个:

1.如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以直接阻止这些非正常的操作。

2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。

被swizzle的方法分别是:

- (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<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

关于

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

方法改造流程如下图:

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

通过上面的流程,将observerd对象的所有kvo相关的observer信息全部转移到KVOdelegate上,并且避免了相同kvoinfo被重复添加多次的可能性。

继续阅读
Mrzhou
  • 本文由 发表于 2019年11月7日03:47:24
  • 转载请务必保留本文链接:https://zhouxiaosong.com/1611.html
视频网HTML5播放小工具 脚本编程

视频网HTML5播放小工具

默认支持视频站点:油管、TED、优酷、土豆、QQ、B站、西瓜视频、爱奇艺、A站、PPTV、芒果TV、新浪、微博、网易娱乐、云课堂、新闻、搜狐、风行、百度云视频等; 直播:斗鱼、YY、虎牙、龙珠、战旗;...
Pycharm IDE 安装及注册激活笔记(1) 脚本编程

Pycharm IDE 安装及注册激活笔记(1)

一、Windows 下的安装及激活. 1、首先去Pycharm官网,或者直接进入网址点击进入:,下载PyCharm安装包,根据自己电脑的操作系统进行选择,对于windows系统选择下图的框框所包含的安...
PHP SPL 标准库之 Countable 脚本编程

PHP SPL 标准库之 Countable

本文实例讲述了PHP标准库 (SPL)——Countable用法。分享给大家供大家参考,具体如下: 类实现 Countable 可被用于 count() 函数. 接口摘要 Countable { /*...
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: