题外话:近来工作闲暇之余把以前看的网易大神写的crash防护手动实现了。纸上得来终觉浅,绝知此事要躬行。记录一下思路,大部分还是参考大神的经验。框架内部可接入日志上报系统,结合服务端进行收集。
Baymax:网易iOS App运行时Crash自动防护实践
自己实现的OCShield
1.unrecognized selector crash【实现】
2.KVO/KVC crash【实现】
3.NSNotification crash
4.NSTimer crash【实现】
5.Container crash(数组越界,插nil等)【实现】
6.NSString crash (字符串操作的crash)【实现】
7.Bad Access crash (野指针)【实现】
8.UI not on Main Thread Crash (非主线程刷UI(机制待改善))
调用方法时会转换成objc_msgSend()函数调用。
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,假如找到,转向相应实现并执行。
2.假如没找到,在相应操作的对象中的方法列表中找调用的方法,假如找到,转向相应实现执行。
3.假如没找到,去父类指针所指向的对象中执行1,2。
4.以此类推,假如一直到根类还没找到,转向阻拦调用,走消息转发机制。
5.假如没有重写阻拦调用的方法,程序报错。
消息转发流程
1.调用resolveInstanceMethod给个机会让类增加这个实现这个函数。
2.调用forwardingTargetForSelector让别的对象去执行这个函数。
3.调用forwardInvocation(函数执行器)灵活的将目标函数以其余形式执行。
基于此,我选择2、3都去实现比照方案。
方案一:重写NSObject的forwardingTargetForSelector
方法。尽管不会造成NSInvocation对象的开销,但是会阻拦到系统的其余方法,导致该方法调用屡次问题。
1.动态创立一个桩类。
2.动态为桩类增加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP。
3.将消息直接转发到这个桩类对象上。
方案二:与方案一思路相似,hook NSObject的methodSignatureForSelector
和forwardInvocation
,尽管频繁创立NSInvocation对象,但是到了这里已经过滤掉系统的方法。
kvo一般crash起因是
1.KVO的被观察者dealloc时依然注册着KVO。
2.增加KVO重复增加观察者或者重复移除观察者(KVO注册观察者与移除观察者不匹配)。
基于管理混乱问题,可以让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系。
通过上面的流程,将observerd对象的所有kvo相关的observer信息一律转移到KVOdelegate上,并且避免了相同kvoinfo被重复增加屡次的可能性。
移除一个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而崩溃,所以需要做这一步来阻止该种情况的发生。
observeValueForKeyPath
方法的修改最主要的地方是,在于将对应的响应方法转移给真正的KVO Observer,通过keyInfoMap找到keypath对应的KVOInfo里面预先存储好的observer,而后调用observer本来的响应方法。hook常用的方法,用Try catch方式守护。
主要针对iOS9系统之前不移除通知。苹果在iOS9之后专门针对于这种情况做了解决,所以在iOS9之后,即便开发者没有移除observer,Notification crash也不会再产生了。
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的scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
接口做重复性的定时任务时存在一个问题:NSTimer会强引用target实例,所以需要在合适的时机invalidate定时器,否则就会因为定时器timer强引用target的关系导致target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。 crash的展示形式和具体的target执行的selector有关。与此同时,假如NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的白费。
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
相关的方法fireProxyTimer:
被执行的时候,会自动判断原target能否已经被释放,假如释放了,意味着NSTimer已经无效,此时假如还继续调用原有target的selector很有可能会导致crash,而且是没有必要的。所以此时需要将NSTimer invalidate,而后统计上报错误数据。如此一来就做到了NSTimer在合适的时机自动invalidate。针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的少量常用的会导致崩溃的API进行method swizzling,而后在swizzle的新方法中加入少量条件限制和判断,从而让这些API变的安全。
NSString/NSMutableString 类型的crash的产生起因和防护方案与Container crash很相像。
allocWithZone
方法在新的方法中判断该类型对象能否需要加入野指针防护,假如需要,则通过objc_setAssociatedObject为该对象设置flag标记,被标记的对象后续会进入zombie流程。
dealloc
方法对flag标记的对象实例调用objc_destructInstance
,释放该实例引用的相关属性,而后将实例的isa修改为ShieldZombieObject。通过objc_setAssociatedObject 保存将原始类名保存在该实例中。
1.调用objc_destructInstance释放该实例引用的相关实例。
2.将该实例的isa修改为stubClass,接受任意方法调用。
3.释放该内存。
forwardingTargetForSelector
解决所有阻拦的方法注:
1.做了野指针防护,通过动态插入一个空实现的方法来防止出现Crash,但是业务层面的体现难以确定,可能会进入业务异常的状态。需要拟定一下如何展示该问题给客户的方案。
2.因为做了延时释放若干实例,对系统总内存会产生肯定影响,目前将内存的缓冲区开到5M左右,所以应该没有很大的影响,但还是可能潜在少量风险。
3.延时释放实例是根据相关功能代码会聚焦在某一个时间段调用的假设前提下,所以野指针的zombie保护机制只能在其实例对象依然缓存在zombie的缓存机制时才有效,若在实例真正释放之后,再调用野指针还是会出现crash,所以不能达到真正防止crash的目的。
据面试阿里的面试官说可以用计算内存堆栈信息的方式,作者表示不了解。
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在这三个方法调用的时候判断一下当前的线程,假如不是主线程的话,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //调用本来方法 });
来将对应的刷UI的操作转移到主线程上,同时统计错误信息。
但是真正实施了之后,发现这三个方法并不能完全覆盖UIView相关的所有刷UI到操作,但是假如要将一律到UIView的刷UI的方法统计起来并且swizzle,感觉略笨拙而且不高效。