为什么对象只有从 NSObject 继承才会变成 NSZombie?

Hen*_*bat 4 xcode memory-management nsobject nszombie swift

我创建了一个新项目,启用了僵尸对象(编辑方案 - > 诊断)。我初始化了两个对象:ZombieTest 和 ZombieTest2(从 NSObject 继承)。运行应用程序后,我打开调试内存图,只有从 NSObject 继承的对象显示为 NSZombie。

调试内存图

Ita*_*ber 6

tl;dr:因为NSZombies被实现只影响NSObject及其子类。(这也与 Swift 无关:不是 Swift 的子类的 Obj-C 对象NSObject也不会变成僵尸。)


在初始化时(__CFInitialize,在框架加载时调用),CoreFoundation 框架设置了很多底层的 Foundation 和 CoreFoundation 行为;除其他外,它会查找NSZombieEnabled环境变量,如果存在,则通过调用__CFZombifyNSObject函数启用僵尸:

; Disassembly from Hopper on /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation on macOS Catalina
0000000000001cc0         lea        rdi, qword [aNszombieenable]                ; argument #1 for method ___CFgetenv, "NSZombieEnabled"
0000000000001cc7         call       ___CFgetenv                                 ; ___CFgetenv
0000000000001ccc         test       rax, rax
0000000000001ccf         je         loc_1cee

0000000000001cd1         mov        al, byte [rax]                              ; DATA XREF=sub_1bb8ec
0000000000001cd3         or         al, 0x20
0000000000001cd5         cmp        al, 0x79
0000000000001cd7         jne        loc_1cee

0000000000001cd9         cmp        byte [___CFZombieEnabled], 0x0              ; ___CFZombieEnabled
0000000000001ce0         jne        loc_1cee

0000000000001ce2         mov        byte [___CFZombieEnabled], 0xff             ; ___CFZombieEnabled
0000000000001ce9         call       ___CFZombifyNSObject                        ; ___CFZombifyNSObject
Run Code Online (Sandbox Code Playgroud)

启用僵尸时,__CFZombifyNSObject将 的实现替换-[NSObject dealloc]为不同的实现 ( __dealloc_zombie):

// Hopper-generated pseudo-code:
void ___CFZombifyNSObject() {
    rax = objc_lookUpClass("NSObject");
    method_exchangeImplementations(class_getInstanceMethod(rax, @selector(dealloc)), class_getInstanceMethod(rax, @selector(__dealloc_zombie)));
    return;
}
Run Code Online (Sandbox Code Playgroud)

这意味着 的所有子类NSObject及其后代,在解除分配时,将调用到__dealloc_zombie。那么有什么作用__dealloc_zombie呢?

// Hopper-generated pseudo-code:
/* @class NSObject */
-(void)__dealloc_zombie {
    rbx = self;
    if ((rbx & 0x1) != 0x0) goto loc_175ed5;

loc_175e3f:
    if (*(int8_t *)___CFZombieEnabled == 0x0) goto loc_175eee;

loc_175e4c:
    rax = object_getClass(rbx);
    var_20 = 0x0;
    rax = asprintf(&var_20, "_NSZombie_%s", class_getName(rax));
    rax = objc_lookUpClass(var_20);
    r14 = rax;
    if (rax == 0x0) {
            r14 = objc_duplicateClass(objc_lookUpClass("_NSZombie_"), var_20, 0x0);
    }
    free(var_20);
    objc_destructInstance(rbx);
    object_setClass(rbx, r14);
    if (*(int8_t *)___CFDeallocateZombies != 0x0) {
            free(rbx);
    }
    goto loc_175ed5;

loc_175ed5:
    if (**___stack_chk_guard != **___stack_chk_guard) {
            __stack_chk_fail();
    }
    return;

loc_175eee:
    if (**___stack_chk_guard == **___stack_chk_guard) {
            _objc_rootDealloc(rbx);
    }
    else {
            __stack_chk_fail();
    }
    return;
}
Run Code Online (Sandbox Code Playgroud)

用更易于理解的术语来说,它:

  1. 仰望;查询 [self class]
  2. 如果还没有名为 的类_NSZombie_<our class name>,它会通过复制_NSZombie_该类来创建一个类,并为副本提供一个新名称(这会创建该类的副本及其所有方法实现,或缺少该类)
  3. 它拆除了self,并用新类替换了它的类,这样如果你将来给它发消息,你就可以分派给_NSZombie_<whatever>

_NSZombie_ 是一个不实现任何方法的类,因此向它发送任何消息(方法调用)最终会落入消息转发中的代码路径,该代码路径会打印出“发送到已释放实例的消息”消息。


实际上,这种实现僵尸的方法取决于继承自NSObject(因为所有NSObject子类都应该调用[super dealloc]释放,最终到达[NSObject dealloc]);不继承自的东西NSObject不继承这个实现。(您也可以通过实现一个在其实现中不调用的NSObject子类来实际看到这一点——该对象在发布时不会被僵尸化。)[super dealloc]-dealloc

难道NSZombies 必须要实现这种方式?不,当然可以想象其他允许纯 Swift 对象参与的方案(Swift 运行时初始化也可以查找NSZombieEnabled环境变量并执行类似的操作),但是付出努力的好处要少一些。正如 Rob 在他的回答中提到的,这主要是因为我们能够调出已释放实例的类(这实际上在 Swift 运行时是可能的,但不会暴露在外部),但至关重要的是,即使我们这样做了,它对静态方法调度的情况没有帮助,这在 Swift 中的对象类型上是可能的(例如对于final类)。[亚历山大在他的评论中提到了这一点。]

因此,在很大程度上,为 Obj-C 实现这种方式真的很容易,而且花时间为纯 Swift 类执行此操作也有一些有限的好处。