防护类型
参考文章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
函数进行主线程判断, 如果不在主线程, 则放在主线程操作.
绘制方法调用顺序:
setNeedsDisplay
setNeedsLayout
- 重写了
displayLayer
就走自己的绘制流程 - 没有重写就走系统的绘制流程:
drawLayer:inContext:
drawRect:
总结
如果有机会专门做某一方面, 就可以更加深入的做一些东西.
这里只是简单的记录和学习.