iOS 崩溃防护

崩溃防护

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 );

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值