前言
在iOS的开发中,内存管理的问题一直我们要解决的问题,crash reason = EXC_BAD_ACCESS 是发生频次比较高的错误。
发生类似问题一般是内存管理问题,大概率是野指针问题,指针指向的对象已经被回收掉了,但是指针还在,如果还在通过指针操作对象,就报坏内存访问错误,野指针又称悬垂指针。
如何揪出坏指针
在系统的Xcode开发工具中,在如下的操作路径Product -> Scheme -> Edit Scheme -> Diagnostics 勾选是上Zombie Objects选项;
早期的Xcode版本中是在Arguments下的Environment Variables添加变量NSZombieEnabled = YES
配置好后,就会发现Xcode的占用内存比之前变大了,大家可以猜想下为什么?
之前的问题就很显然的暴露出来了,比如
*** -[Student test]: message sent to deallocated instance 0x6000038a2ac0
复制代码
这个时候就知道是Student对象的内存管理上出现了野指针问题了,那接下来问题就好解决了,给了一双我们发现问题的眼睛了。
庖丁解牛
1. Print Class superClass
系统是如何实现的呢?我们可以借助于runtime来发现一些蛛丝马迹。
接下来进行如下操作
Student* st = [Student new];
printClassInfo(st);
[st release];
printClassInfo(st);
[st test];
复制代码
- 在第一个printClassInfo打印的信息看出来
self:Student - superClass:NSObject
- 在release之后,第二个printClassInfo打印出来的结果是
self:_NSZombie_Student - superClass:nil
- 说明在release之后,student对象的类由Student变成了_NSZombie_Student,但是这个类的父类是空,那就说明_NSZombie_Student跟Student不是父类子类的关系;
- 在开启NSZombieEnabled的前提下,在Instrument的Zombies的调试中发现,在对象调用release后,调用堆栈中有NSObject的_dealloc_zombie方法;
- 在Xcode中添加_dealloc_zombie的Symbolic Breakpoint;
- 重新重启后,发现进入如下的汇编代码逻辑
2. __dealloc_zombie背后的逻辑
CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
-> 0x109509b28 <+0>: pushq %rbp
0x109509b29 <+1>: movq %rsp, %rbp
0x109509b2c <+4>: pushq %r14
0x109509b2e <+6>: pushq %rbx
0x109509b2f <+7>: subq $0x10, %rsp
0x109509b33 <+11>: movq 0x2dd60e(%rip), %rax ; (void *)0x000000010ac290b0: __stack_chk_guard
0x109509b3a <+18>: movq (%rax), %rax
0x109509b3d <+21>: movq %rax, -0x18(%rbp)
0x109509b41 <+25>: testq %rdi, %rdi
0x109509b44 <+28>: js 0x109509be3 ; <+187>
0x109509b4a <+34>: movq %rdi, %rbx
0x109509b4d <+37>: cmpb $0x0, 0x4856ac(%rip) ; __CFConstantStringClassReferencePtr + 7
0x109509b54 <+44>: je 0x109509bfc ; <+212>
0x109509b5a <+50>: movq %rbx, %rdi
0x109509b5d <+53>: callq 0x10958455a ; symbol stub for: object_getClass
0x109509b62 <+58>: leaq -0x20(%rbp), %r14
0x109509b66 <+62>: movq $0x0, (%r14)
0x109509b6d <+69>: movq %rax, %rdi
0x109509b70 <+72>: callq 0x109584068 ; symbol stub for: class_getName
0x109509b75 <+77>: leaq 0x24086f(%rip), %rsi ; "_NSZombie_%s"
0x109509b7c <+84>: movq %r14, %rdi
0x109509b7f <+87>: movq %rax, %rdx
0x109509b82 <+90>: xorl %eax, %eax
0x109509b84 <+92>: callq 0x109583f90 ; symbol stub for: asprintf
0x109509b89 <+97>: movq (%r14), %rdi
0x109509b8c <+100>: callq 0x1095844c4 ; symbol stub for: objc_lookUpClass
0x109509b91 <+105>: movq %rax, %r14
0x109509b94 <+108>: testq %rax, %rax
0x109509b97 <+111>: jne 0x109509bb6 ; <+142>
0x109509b99 <+113>: leaq 0x240282(%rip), %rdi ; "_NSZombie_"
0x109509ba0 <+120>: callq 0x1095844c4 ; symbol stub for: objc_lookUpClass
0x109509ba5 <+125>: movq -0x20(%rbp), %rsi
0x109509ba9 <+129>: movq %rax, %rdi
0x109509bac <+132>: xorl %edx, %edx
0x109509bae <+134>: callq 0x109584476 ; symbol stub for: objc_duplicateClass
0x109509bb3 <+139>: movq %rax, %r14
0x109509bb6 <+142>: movq -0x20(%rbp), %rdi
0x109509bba <+146>: callq 0x10958423c ; symbol stub for: free
0x109509bbf <+151>: movq %rbx, %rdi
0x109509bc2 <+154>: callq 0x109584470 ; symbol stub for: objc_destructInstance
0x109509bc7 <+159>: movq %rbx, %rdi
0x109509bca <+162>: movq %r14, %rsi
0x109509bcd <+165>: callq 0x109584572 ; symbol stub for: object_setClass
0x109509bd2 <+170>: cmpb $0x0, 0x485628(%rip) ; __CFZombieEnabled
复制代码
所以该方法背后的逻辑大概如下
- object_getClass 获取当前对象的class;
- class_getName 获取当前对象class对应的字符串;
- 进行字符串拼接_NSZombie_%s,通过objc_lookUpClass寻找是否已经存在zombie的类,比如_NSZombie_Student;
- 如果不存在的话,则重新创建,先获取通过objc_lookUpClass模板类_NSZombie_;
- 然后通过objc_duplicateClass,基于类_NSZombie_,复制一个副本_NSZombie_Student;
- 不同于objc_allocateClassPair方法,该方法是基于一个父类,重建一个子类,而objc_duplicateClass只是拷贝一个副本,不是父类子类的关系,所以在之前获取_NSZombie_Student的superclass的是nil;
- 然后通过objc_destructInstance将该对象的关联全部解除,对象的内存其实没有回收掉;
- 最后调用object_setClass将原对象Student的isa指向了_NSZombie_Student;
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
复制代码
二者的区别在于object_dispose多了一步操作free(obj),这才是真正的内存释放。
调试中,接下来进入___forwarding___,消息转发的逻辑中。
3. forwarding
为什么会进入消息转发的逻辑里,说明_NSZombie_Student没有实现该方法,在官方的解释中说道_NSZombie_,是一个没有任何方法实现的空类,所以任何被zombied对象的任何方法,一旦在内存释放后,再调用任何方法,就会进入消息转发的逻辑中。
在消息转发的实现类中,打印出相应的类和方法信息,就选择强制崩溃了。
代码简单实现
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = NSSelectorFromString(@"dealloc");
SEL swizzledSelector = @selector(_dealloc_zombie);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
-(void)_dealloc_zombie{
__NSZombie_Enabled(self);
}
void __NSZombie_Enabled(id obj){
Class baseClass = object_getClass(obj);
const char *className = class_getName(baseClass);
const char *zombieClassName = [NSString stringWithFormat:@"_NSZombie_%s",className].UTF8String;
Class zombieClass = objc_lookUpClass(zombieClassName);
if (!zombieClass) {
Class baseZombieClass = objc_lookUpClass("_NSZombie_");
zombieClass = objc_duplicateClass(baseZombieClass, zombieClassName, 0);
class_addMethod(zombieClass, @selector(forwardingTargetForSelector:),(IMP)forwardSelector, "@@::");
objc_destructInstance(obj);
object_setClass(obj, zombieClass);
}
}
void forwardSelector(NSObject* self, SEL selector, SEL forwardSelector){
Class zombieClass = object_getClass(self);
NSString* zombieClassName = [NSString stringWithUTF8String:class_getName(zombieClass)];
NSString* originalClassName = [zombieClassName substringFromIndex:10];
NSLog(@"*** - [%@ %@]: message sent to deallocated instance %p", originalClassName, NSStringFromSelector(forwardSelector), self);
abort();
}
复制代码
最后打印出来的log就是*** - [Student test]: message sent to deallocated instance 0x600003a78360
跟系统就一样了。