iOS 僵尸模式(NSZombieEnabled)实现原理

前言

在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];
复制代码
  1. 在第一个printClassInfo打印的信息看出来self:Student - superClass:NSObject
  2. 在release之后,第二个printClassInfo打印出来的结果是self:_NSZombie_Student - superClass:nil
  3. 说明在release之后,student对象的类由Student变成了_NSZombie_Student,但是这个类的父类是空,那就说明_NSZombie_Student跟Student不是父类子类的关系;
  4. 在开启NSZombieEnabled的前提下,在Instrument的Zombies的调试中发现,在对象调用release后,调用堆栈中有NSObject的_dealloc_zombie方法;
  5. 在Xcode中添加_dealloc_zombie的Symbolic Breakpoint;
  6. 重新重启后,发现进入如下的汇编代码逻辑

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
复制代码

所以该方法背后的逻辑大概如下

  1. object_getClass 获取当前对象的class;
  2. class_getName 获取当前对象class对应的字符串;
  3. 进行字符串拼接_NSZombie_%s,通过objc_lookUpClass寻找是否已经存在zombie的类,比如_NSZombie_Student;
  4. 如果不存在的话,则重新创建,先获取通过objc_lookUpClass模板类_NSZombie_;
  5. 然后通过objc_duplicateClass,基于类_NSZombie_,复制一个副本_NSZombie_Student;
  6. 不同于objc_allocateClassPair方法,该方法是基于一个父类,重建一个子类,而objc_duplicateClass只是拷贝一个副本,不是父类子类的关系,所以在之前获取_NSZombie_Student的superclass的是nil;
  7. 然后通过objc_destructInstance将该对象的关联全部解除,对象的内存其实没有回收掉;
  8. 最后调用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
跟系统就一样了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享