为什么object只有继承NSObject才会变成NSZombie?

Why does object become NSZombie only when inherit from NSObject?

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

向 NSZombie 的转换由 ObjC 运行时处理,并且需要 ISA swizzle(即对象的 class 在运行时更改)。纯 Swift classes 不支持。

在某些情况下,纯 Swift 对象可能仍会成为 NSZombie,因为纯 Swift 对象有时会桥接到 ObjC 运行时,但这些是不能依赖的实现细节上。

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


初始化时(__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

启用僵尸时,__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;
}

这意味着 NSObject 的所有子 class 及其后代在解除分配后将调用 __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;
}

用更易读的术语来说,它:

  1. 查找[self class]
  2. 如果还没有名为 _NSZombie_<our class name> 的 class,它会通过复制 _NSZombie_ class 来创建一个,并为副本赋予一个新名称(这会创建class 的副本及其所有方法实现,或缺少)
  3. 它拆掉了 self,并用新的 class 替换了它的 class,这样如果你以后给它发消息,你就会发送到 _NSZombie_<whatever>

_NSZombie_ 是一个 class,它没有实现任何方法,因此向它发送任何消息(方法调用)最终都会落入消息转发中的代码路径,打印出 "message sent to deallocated instance"留言。


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

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

所以,在很大程度上,以这种方式为 Obj-C 实现真的很容易,而且花时间为纯 Swift classes 这样做的好处也有限。