防护类型
参考文章Crash防护
unrecognized selector crash
当消息定义了, 但是没有实现, 也就是SEL没有对应的imp的时候, 消息发送就会报错.
消息流程
需要了解消息转发流程, 大致说一下:
-
+ (BOOL)resolveInstanceMethod:(SEL)sel- 可以做消息的添加操作
-
- (id)forwardingTargetForSelector:(SEL)aSelector- 可以指定消息的接受者
-
+ (IMP)instanceMethodForSelector:(SEL)aSelector; -
- (void)forwardInvocation:(NSInvocation *)anInvocation;aSelector正确的情况下, 实现forwardInvocation就不会报错
-
- (void)doesNotRecognizeSelector:(SEL)aSelector;- 内部是调用
objc_fatal()函数, 抛出异常
- 内部是调用
选择哪个阶段处理
为什么选择在forwardingTargetForSelector阶段做处理
-
在
resolveInstanceMethod阶段添加方法, 对类来说添加无用方法, 是一种冗余代码. -
forwardInvocation和instanceMethodForSelector是绑定的, 且返回正确的函数签名. 且通过NSInvocation可以做多种转发. -
forwardingTargetForSelector只需要返回一个消息接收者即可, 比较适合. -
doesNotRecognizeSelector直接hook, 所有走doesNotRecognizeSelector方法直接报错的都会被失效, 需要自己处理收集堆栈.
大白实现
添加NSObject的分类添加替换的方法, hookforwardingTargetForSelector方法, 替换了自己的实现.
KVO崩溃
KVO崩溃场景:
-
已经添加观察者的
KVO, 观察者被释放, 且没有移除掉的时候, 触发会崩溃 -
重复移除
KVO导致的崩溃 -
同一个对象添加不同的观察者, 必须要实现回调方法.
-
同一个对象添加相同的观察者, 会出发多次回调.
iOS11以前, 重复添加, 被观察者销毁时还存在观察者会产生崩溃
解决思路
所以, 我们要做到
- 不能重复添加
- 不能重复移除
- 要在观察者的生命周期, 在消失之前移除掉KVO.
解决方案
大白系统之中, 使用的是一个BayMaxKVODelegate的类. 并不是一个真正的代理.
主要做了以下几件事
-
做一个
NSObject(KVOProtector)分类, 动态添加BayMaxKVODelegate的属性 -
KVOProtectorKey动态添加了保护属性, 在dealloc的时候, 根据字段判断是否要进行移除. -
hook以下几个方法:addObserver:forKeyPath:options:context:removeObserver:forKeyPath:removeObserver:forKeyPath:context:dealloc
-
BayMaxKVODelegate-
_keyPathMaps类型是NSMutableDictionary<NSString*, NSMutableArray<KVOInfo *> *> *_keyPathMaps; -
封装
KVOInfo接受整个消息的所有入参, 并且存储 -
addObserver的操作如下:- 先根据
keyPath拿出KVOInfo数组 - 遍历找到对应的
observer观察者, 如果已经存在则返回错误信息 - 如果不存在, 则添加新的
KVOInfo对象
- 先根据
-
添加移除都使用
NSLock进行了线程安全保护 -
移除的时候
keyPath取出观察数组- 比对
observer的MD5的值进行验证 - 找到了就移除, 找不到则不处理.
-
observeValueForKeyPath回调的时候, 判断observer是否存在在回调
-
所以通过以上的操作, 重复添加, 重复移除, observer不存在出发的崩溃 都解决了.
NSNotification
iOS9之之前, 对象销毁的时候, 没有移除会产生bug.
需要注意注意点:
-
NSNotificationQueue的使用- 可以指定通知策略, 是否合并等.
- 可以设定同步, 异步
-
通知流程:
- 新建一个数组, 添加通知
wildcard链表, 添加没有name也没有object的通知对象nameless没有名字的有object的通知表,object->链表named有名字的通知, 这个表是个二级结构.- 根据
name找到表, 取出表根据objectobject找到链表 - 如果没有
object, 会有一个默认的key
- 根据
- 找完了之后, 遍历数组
- 通过
[observerNode->observer performSelector: o->selector withObject: notification];发送通知.
- 通过
NSNotification防护
现在基本上支持的版本都是在iOS10以上了, 所以不太需要处理.
兼容iOS9以下的, 文中给出的是dealloc的hook方案. 但是hook整个dealloc并不是很好, 可以考虑使用其他方式.
NStimer类
主要是Timer会持有Target, 所以会造成内存泄漏.
NSTimer->Target, 即便是传入的weakSelf依旧是无效的, 因为Timer是直接指向weakSelf的内存地址的.
防护
文中给出的方案是:
hook以下方法scheduledTimerWithTimeInterval:....timerWithTimeInterval...
- 使用
BayMaxTimerSubTarget替代- 相当于
NSTimer->BayMaxTimerSubTarget- - - ->target也可以直接使用YYWeakProxy
- 相当于
文中的方案, 主要是兼容性的, 也是为了维护作者自己定义的BayMaxCatchError回调.
所以两种方案实际上原理是一样的
- 大白方案对使用没有要求,
- YY方案需要在写的时候自己处理.
Container类型crash
容器类的崩溃还是非常多且明显的, 在debug模式下, 正常开发不要开启防护. 上线版本开启.
BayMaxContainers直接查看这个类, 交换了很多.需要注意的点如下:
- 容器类是类簇, 所以真实的调用类型并不是容器类型. 需要根据堆栈打印出来的符号进行调用, 比如
__NSArrayI, __NSArray0, __NSSingleObjectArrayI等等实际的内部调用类型. NSNumber,NSCache类型容易被忽略.- 注意
TargetPointer类型
防护
BayMaxContainers, 这个类里面直接拿去用.
- 主要就是对需要防护的类, 添加分类方法, 然后
hook原方法. - 看下下面的例子
//objectAtIndex:
//__NSArrayI 这里换的就是__NSArrayI类型
BMP_EXChangeInstanceMethod(__NSArrayI, @selector(objectAtIndex:), __NSArrayI, @selector(BMP__NSArrayIObjectAtIndex:));
BMP_EXChangeInstanceMethod(__NSArrayI, @selector(objectAtIndexedSubscript:), __NSArrayI, @selector(BMP_objectAtIndexedSubscript:));
NSString
和容器类很像, 需要自己进行各方方法的调用测试, 然后根据报错的信息进行真实的hook.
防护
大白提供的防护也在BayMaxContainers类中, 原理和容器类一样.
野指针崩溃
不太好处理, 参考系统的僵尸对象. 调试模式关闭, 不然问题不好排查.
防护
最简单的思路, 不进行动态僵尸对象处理:
- 创建一个
Zoombie类, 添加一个出发类的ClassName的属性- 重写
forwardingTargetForSelector, 设置返回值为ClassName的实例
- 重写
- Hook
NSObject的dealloc的方法- 延时2秒判断
self是否存在. 如果存在则走如下流程: - 将
object通过object_setClass设置指向Zoombie类` - 设置
obj的ClassName属性为类名
- 延时2秒判断
- 使用一句全局双向链表, 把这
Zoombie对象链接起来- 使用头插法
- 发出内存警告的时候进行尾部删除. 一般设置为2M或者多少个
非主线程刷新UI
大白给出的方案是:
- hook以下三个方法
- (void)setNeedsLayout;- (void)setNeedsDisplay;- (void)setNeedsDisplayInRect;在调用的时候, 使用strcmp函数进行主线程判断, 如果不在主线程, 则放在主线程操作.
绘制方法调用顺序:
setNeedsDisplaysetNeedsLayout- 重写了
displayLayer就走自己的绘制流程 - 没有重写就走系统的绘制流程:
drawLayer:inContext:drawRect:
总结
如果有机会专门做某一方面, 就可以更加深入的做一些东西.
这里只是简单的记录和学习.