原文 : 与佳期的我的博客(gonghonglou.com)git
你们都知道,向业已回收的对象发送消息是不安全的。这么作有时能够,有时不行。具体可行与否,彻底取决于对象所占内存有没有为其余内容所覆写。而这块内存有没有移做他用,又没法肯定,所以,应用程序只是偶尔崩溃。在没有崩溃的状况下,那块内存可能只复用了其中一部分,因此对象中的某些二进制数据依然有效。还有一种可能,就是那块内存刚好为另一个有效且存活的对象所占据。在这种状况下,运行期系统会把消息发到新对象那里,而此对象也许能应答,也许不能。若是能,那程序就不崩溃github
这是《Effective Objective-C 2.0》书中”第 35 条:用“僵尸对象”调试内存管理问题“一章中对野指针的介绍,这即是野指针出现的缘由。缓存
本篇是 Crash 防御方案系列的第二篇文章,一样是很是常见的 Crash 类型:EXC_BAD_ACCESS,文章会涉及如下几点:安全
咱们先模拟一下看看野指针崩溃的样子:bash
崩溃的缘由是 obj 对象是用 assign 修饰的,self 并未强引用该对象,GHLTestObject 对象建立以后由于没人引用他因此就被回收了,以后再次调用 GHLTestObject 的 log 方法则出现了 EXC_BAD_ACCESS 崩溃,这即是向已回收的对象发送消息产生的崩溃。函数
多说一句,这里若是把 obj 对象的 assign 修饰改为 strong,则 GHLTestObject 的 log 方法能够正常执行,由于 obj 对象被 self 强引用了。若是把 obj 对象的 assign 修饰改为 weak,虽然 GHLTestObject 的 log 方法不会执行,但程序也不会崩溃,由于被 weak 修饰的指针会在对象销毁后自动置空,在 OC 中向一个空对象发消息是不会崩溃的。性能
咱们开启 Xcode 的 Zoombie Objects 选项看一下效果(Edit Scheme -> Diagnostics -> Zoombie Objects): 测试
能够看到控制台打印了明确的报错信息:ui
2019-07-09 18:59:43.894822+0800 GHLCrashGuard_Example[51380:3729261] *** -[GHLTestObject retain]: message sent to deallocated instance 0x6000001356f0spa
而且能看到 self 的 obj 属性从 GHLTestObject 类变成了 _NSZombie_GHLTestObject 类。 其实,在启用僵尸对象后,在运行期发现 GHLTestObject 变成了僵尸对象,那么便动态的建立一个 _NSZombie_GHLTestObject 类,将 GHLTestObject 对象的 isa 指针指向这个新的类,再次向 GHLTestObject 对象发消息的话就会去 _NSZombie_GHLTestObject 这个类里去找相应的方法,然而 _NSZombie_GHLTestObject 这个类没有实现任何方法,那么发给他的所有消息都要通过“完整的消息转发机制”。 在发生崩溃的栈回溯消息能能看到 ___forwarding___
函数,该函数首先要作的事情就是检查接受对象所属的类名,若是类名前缀为 _NSZombie_
,则代表消息接收者是僵尸对象,那么会在控制台打印一条消息。将消息接受对象所属的类名去掉 _NSZombie_
前缀就能获得原始类名了。
有时咱们可能实现脱离 Xcode 的僵尸对象调试,方便开发和测试的调试工走,那么能够参照 Xcode 的思路本身来实现,即:
一、Hook NSObject 的 dealloc 方法 二、运行时动态生成新类,用 _GHLZoombie_
作前缀拼接原始类名 三、将僵尸对象的 isa 指针指向 _GHLZoombie_
新类 四、给 _GHLZoombie_
新类添加 forwardingTargetForSelector 方法 五、在 forwardingTargetForSelector 方法里去掉 _GHLZoombie_
前缀获取原始类名,和调用方法名打印出来 六、终止程序
代码实现:
+ (void)load {
// Bad Access
[self jr_swizzleMethod:NSSelectorFromString(@"dealloc") withMethod:@selector(zoombie_dealloc) error:nil];
}
- (void)zoombie_dealloc {
[[GHLBadAccessManager sharedInstance] handleDeallocObject:self];
}
复制代码
GHLBadAccessManager 类里的处理:
NSString *GHLZoombieClassPrefix = @"_GHLZoombie_";
- (void)handleDeallocObject:(__unsafe_unretained id)object {
// 指向动态生成的类,用 _GHLZoombie_ 拼接原有类名
NSString *className = NSStringFromClass([object class]);
NSString *zombieClassName = [GHLZoombieClassPrefix stringByAppendingString: className];
Class zombieClass = NSClassFromString(zombieClassName);
if(zombieClass) return;
zombieClass = objc_allocateClassPair([NSObject class], [zombieClassName UTF8String], 0);
objc_registerClassPair(zombieClass);
class_addMethod([zombieClass class], @selector(forwardingTargetForSelector:), (IMP)forwardingTargetForSelector, "@@:@");
object_setClass(object, zombieClass);
}
id forwardingTargetForSelector(id object, SEL _cmd, SEL aSelector) {
NSString *className = NSStringFromClass([object class]);
NSString *realClass = [className stringByReplacingOccurrencesOfString:GHLZoombieClassPrefix withString:@""];
NSLog(@"[%@ %@] message sent to deallocated instance %@", realClass, NSStringFromSelector(aSelector), object);
abort();
}
复制代码
2019-07-09 19:37:01.766612+0800 GHLCrashGuard_Example[51942:3759054] [GHLTestObject log] message sent to deallocated instance <_GHLZoombie_GHLTestObject: 0x600002b05e90>
运行程序发现可以实现和 Xcode 开启僵尸对象一样的效果
既然咱们实现了和 Xcode 开启僵尸对象一样的效果,那咱们能够在最后一步不选择终止程序,而是让程序进入消息转发机制。
不过咱们的防御方案里也能够更简单的将原始僵尸对象的 isa 指针指向一个固定的类:GHLZoombie,没必要在运行时动态的建立,至于获取原始类名的问题,能够经过 objc_setAssociatedObject 的方式将原始类名保存进 GHLZoombie 对象里,在 GHLZoombie 对象里重载 - (id)forwardingTargetForSelector: 方法,经过 objc_getAssociatedObject 取出原始类名,在控制台打印,并将消息转发给 GHLCrashGuardProxy 对像,在上一篇 Crash 防御方案(一):Unrecognized Selector 里讲过,GHLCrashGuardProxy 对像里重载了 + (BOOL)resolveInstanceMethod: 方法避免崩溃,并收集堆栈,上报 Crash。
代码实现 GHLBadAccessManager 类里的处理:
- (void)handleDeallocObject:(__unsafe_unretained id)object {
// 指向固定的类,原有类名存储在关联对象中
NSString *originClassName = NSStringFromClass([object class]);
objc_setAssociatedObject(object, "originClassName", originClassName, OBJC_ASSOCIATION_COPY_NONATOMIC);
object_setClass(object, [GHLZoombie class]);
}
复制代码
GHLZoombie 类里的实现:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, "originClassName"), NSStringFromSelector(aSelector), self);
return [GHLCrashGuardProxy new];
}
复制代码
剩下的就是上一篇文章的内容了,这样就能作到 EXC_BAD_ACCESS Crash 的防御。
但仍然存在问题是延迟释放内存会形成性能浪费,因此能够设置一个默认的缓存僵尸对象的实例数量(50)或者给定一个固定内存大小(2M),超出这个限制就会释放,固然在释放以后若是再此触发了恰好释放掉的野指针,仍是会形成 Crash 的。
Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/EXC_BAD_ACCESS
小白出手,请多指教。如言有误,还望斧正!
转载请保留原文地址:gonghonglou.com/2019/07/06/…