做为一个资深的技术团队,app的性能是咱们技术团队首要的任务,其中最主要的一项就是app的崩溃率。数组
目前虽然不能把系统全部的crash都处理掉,不过一些常见的高频次发生的crash,系统都会处理。目前主要能够处理掉的crash类型有一下几种:安全
1.unrecognized selector crashapp
2.KVO crashasync
3.NSNotification crash函数
4.NSTimer crash性能
5.Container crash(数组越界,插nil等)spa
6.NSString crash (字符串操做的crash)线程
7.UI not on Main Thread Crash (非主线程刷UI(机制待改善))指针
下面会一一讲解如何解决这些carshserver
unrecognized selector crash
unrecognized selector类型的crash是常常发生的carsh,咱们要解决这个carsh就必须先了解它产生的具体缘由和流程。
何时会报unrecognized selector的异常?
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,而后在该类中的方法列表以及其父类方法列表中寻找方法运行,若是,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
在找不到方法时,查找方法将会进入方法Forward流程,系统给了三次补救的机会,因此咱们要解决这个问题,在这三次都可以解决这个问题
编辑
请点击输入图片描述
由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:
一、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
二、调用forwardingTargetForSelector让别的对象去执行这个函数
三、调用forwardInvocation(函数执行器)灵活的将目标函数以其余形式执行。
若是都不中,调用doesNotRecognizeSelector抛出异常。既然能够补救,咱们能够用消息转发机制来作,咱们选择了第二步forwardingTargetForSelector来作,缘由以下:
一、resolveInstanceMethod 须要在类的自己上动态添加它自己不存在的方法,这些方法对于该类自己来讲冗余的
二、forwardInvocation能够经过NSInvocation的形式将消息转发给多个对象,可是其开销较大,须要建立新的NSInvocation对象,而且forwardInvocation的函数常常被使用者调用,来作多层消息转发选择机制,不适合屡次重写
三、forwardingTargetForSelector能够将消息转发给一个对象,开销较小,而且被重写的几率较低,适合重写
选择了forwardingTargetForSelector以后,能够将NSObject的该方法重写,作如下几步的处理:
一、动态建立一个桩类
二、动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
三、将消息直接转发到这个桩类对象上。
流程图以下:
编辑
请点击输入图片描述
注意若是对象的类本事若是重写了forwardInvocation方法的话,就不该该对forwardingTargetForSelector进行重写了,不然会影响到该类型的对象本来的消息转发流程。
经过重写NSObject的forwardingTargetForSelector方法,咱们就能够将没法识别的方法进行拦截而且将消息转发到安全的桩类对象中,从而可使app继续正常运行。
KVO crash
若是观察者和keypath的数量一多,很容易理不清楚被观察对象整个KVO关系,致使被观察者在dealloc的时候,还残存着一些关系没有被注销。 同时还会致使KVO注册观察者与移除观察者不匹配的状况发生。
那么如何来管理混乱的KVO关系呢。可让被观察对象持有一个KVO的delegate,全部和KVO相关的操做均经过delegate来进行管理,delegate经过创建一张map来维护KVO整个关系
这样作的好处有两个:
一、若是出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的状况,delegate能够直接阻止这些非正常的操做。
二、被观察对象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:(nullable void *)context;
方法改造流程以下图:
编辑
请点击输入图片描述
经过上面的流程,将observerd对象的全部kvo相关的observer信息所有转移到KVOdelegate上,而且避免了相同kvoinfo被重复添加屡次的可能性。
关于
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
方法改造流程以下图:
编辑
请点击输入图片描述
移除一个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)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
delegate对于observeValueForKeyPath方法的修改最主要的地法规,在于将对应的响应方法转移给真正的KVO Observer,经过keyInfoMap找到keypath对应的KVOInfo里面预先存储好的observer,而后调用observer本来的响应方法
同时在遍历InfoArray的时候,发现info.observerw == nil的时候,须要及时将其清除掉,避免KVO的观察者observer被释放后value变化致使的crash
最后,针对 KVO的被观察者dealloc时仍然注册着KVO致使的crash 的状况
能够将NSObject的dealloc swizzle, 在object dealloc的时候自动将其对应的kvodelegate全部和kvo相关的数据清空,而后将kvodelegate也置空。避免出现KVO的被观察者dealloc时仍然注册着KVO而产生的crash
NSNotification crash
当一个对象添加了notification以后,若是dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc以前先调用一下[[NSNotificationCenter defaultCenter] removeObserver:self] 便可。
注意到并非全部的对象都须要作以上的操做,若是一个对象历来没有被NSNotificationCenter 添加为observer的话,在其dealloc以前调用removeObserver彻底是画蛇添足。 因此咱们hook了NSNotificationCenter的addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject 函数,在其添加observer的时候,对observer动态添加标记flag。这样在observer dealloc的时候,就能够经过flag标记来判断其是否有必要调用removeObserver函数了。
NSTimer crash
NSTimer存在如下问题:
• Target是强引用,内存泄漏
• 在宿主不存在的时候,清理NSTimer
解决方法: Hook NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats方法
一、当repeats为NO时,走原始方法
二、当repeats为YES时,新建一个对象,声明一个target属性为weak类型,指向参数的target,当中间对象的target为空时,清理NSTimer
Container crash(数组越界,插nil等)
Container 类型的crash 指的是容器类的crash
常见的有
• NSArray
• NSMutableArray
• NSDictionary
• NSMutableDictionary
• NSCache
一些常见的越界,插入nil,等错误操做均会致使此类crash发生
Container crash 类型的防御方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些经常使用的会致使崩溃的API进行method swizzling,而后在swizzle的新方法中加入一些条件限制和判断,从而让这些API变的安全,这里就不展开来具体描述了。
NSString crash (字符串操做的crash)
NSString/NSMutableString 类型的crash的产生缘由和防御方案与Container crash很相像,这里也不展开来描述了。
UI not on Main Thread Crash (非主线程刷UI)
在非主线程刷UI将会致使app运行crash,有必要对其进行处理。 目前初步的处理方案是swizzle UIView类的如下三个方法:
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在这三个方法调用的时候判断一下当前的线程,若是不是主线程的话,直接利用
dispatch_async(dispatch_get_main_queue(), ^{
//调用本来方法
});
来将对应的刷UI的操做转移到主线程上,同时统计错误信息