崩溃防护
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属性为类名
使用一句全局双向链表, 把这Zoombie对象链接起来
使用头插法
发出内存警告的时候进行尾部删除. 一般设置为2M或者多少个
非主线程刷新UI
大白给出的方案是:
hook以下三个方法
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect;
在调用的时候, 使用strcmp函数进行主线程判断, 如果不在主线程, 则放在主线程操作.
绘制方法调用顺序:
setNeedsDisplay
setNeedsLayout
重写了displayLayer就走自己的绘制流程
没有重写就走系统的绘制流程:
drawLayer:inContext:
drawRect:
在iphone上写socket的程序的时候,如果尝试send到一个disconnected socket上,就会让底层抛出一个SIGPIPE信号。
client端通过 pipe 发送信息到server端后,就关闭client端, 这时server端,返回信息给 client 端时就产生Broken pipe 信号了。
对于产生信号,我们可以在产生信号前利用方法 signal(int signum, sighandler_t handler) 设置信号的处理。
如果没有调用此方法,系统就会调用默认处理方法:中止程序,显示提示信息(就是我们经常遇到的问题)。
我们可以调用系统的处理方法,也可以自定义处理方法。
以前不清楚的时候,程序莫名其妙的崩溃不了解,后来才了解到,在代码中进行了以下处理后app就不会崩溃了。
/***************************************************/
//设置不被SIGPIPE信号中断,物理链路损坏时才不会导致程序直接被Terminate
//在网络异常的时候如果程序收到SIGPIRE是会直接被退出的。
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );