如何在Swift中展示一个僵尸对象?

6
我已经阅读了如何在Xcode工具中演示内存泄漏和僵尸对象?但那是针对Objective-C的。这些步骤并不适用。
这里的阅读中,我理解僵尸对象是指:
  • 已被销毁
  • 但仍有指针试图指向它们并给它们发送消息。
我并不确定这与访问一个已销毁的对象有何不同。
我的意思是,在Swift中可以这样做:
var person : Person? = Person(name: "John")
person = nil
print(person!.name)

这个人被释放了吗?是的!

我们试图指向它吗?是的!

那么有人可以分享导致创建悬空指针最常见的错误吗?


1
在Swift中,我不会担心这个问题。只要确保您不使用强制解包可选项(我仅在IBOutlets中使用它们),您就不会有问题。 - EmilioPelaez
这正是我想的。这个链接是否适用:假设你有一个缓存,其条目是从某个URL下载的NSData实例,其中URL包含URL中的会话ID,并且该会话ID + URL用作查找缓存中的内容的键。现在,假设用户注销,导致会话ID被销毁。如果缓存没有清除与该会话ID特定相关的所有条目,则所有这些NSData对象都将被遗弃。 - mfaani
3
请注意,你提供的 Swift 示例并不是一个悬垂指针的例子 - 你将引用设置为 nil,这意味着无论对象是否仍然被分配,你都不再拥有该对象的引用。也许在 Swift 中获得一个悬垂指针最简单的例子是使用 Unmanaged,例如 class C {}; var c = C(); Unmanaged.passUnretained(c).release(),此时 c就成了一个悬垂指针。但是这并不是一个“常见错误” - 在 Swift 中,你不应该使用这种不安全的结构来获得悬垂指针(因为 Swift 默认是安全的)。 - Hamish
2
话虽如此,目前存在一种没有临时指针转换的“footgun”,可以创建悬空指针,例如 let ptr = UnsafePointer([1, 2, 3]) - ptr 是一个悬空指针,因为数组到指针的转换仅在调用期间有效。希望在 https://github.com/apple/swift/pull/20070 中警告(最终错误)这样的转换。 - Hamish
糟糕:*带有临时指针转换 - Hamish
4个回答

6
这是一个不到15行代码的僵尸攻击示例:
class Parent { }

class Child {
    unowned var parent: Parent // every child needs a parent

    init(parent: Parent) {
        self.parent = parent
    }
}

var parent: Parent? = Parent()
let child = Child(parent: parent!) // let's pretend the forced unwrap didn't happen
parent = nil // let's deallocate this bad parent
print(child.parent) // BOOM!!!, crash

在这段代码中,Child 持有一个 Parent 的无主引用(unowned reference),但是一旦 Parent 被销毁后,该引用就会变得无效。引用持有一个指向已经不再存在的父级对象的指针(RIP),访问该引用会导致崩溃并显示类似于以下的错误信息:

Fatal error: Attempted to read an unowned reference but object 0x1018362d0 was already deallocated2018-10-29 20:18:39.423114+0200 MyApp[35825:611433] Fatal error: Attempted to read an unowned reference but object 0x1018362d0 was already deallocated

注意: 该代码无法在 Playground 中运行,需要在常规应用程序中运行。

4
从技术角度来说,这不是一个悬空指针,因为Swift会跟踪无主引用以确保在所有强引用都消失后尝试访问时始终陷入错误。您可以使用 unowned(unsafe) 来删除此保护机制并获得实际的悬空指针,这将导致释放后的访问行为未定义。 - Hamish
@Hamish 说得有道理,我已经删除了“悬空指针”的提法。 - Cristik
2
将其称为“僵尸”可能也会令人困惑,因为它不会出现在Cocoa生态系统中任何与僵尸相关的内容中。这是一个特定于Swift的东西,即未拥有的引用,而不是悬空指针。但我认为这对讨论增加了很多价值(指出存在一种特定于Swift的东西,可以在没有Unsafe的情况下导致与内存相关的远程恐怖崩溃),只要读者不被误导认为这与NSZombie有任何关系。 - Rob Napier
在这里,您不会遇到与同一原则产生的EXC_BAD_ACCESS相同的问题,但是它不会让您知道谁是父级,在它被释放时,谁是子级以及何时尝试访问父级。这是这个问题的戏剧性部分。但是这是一个很好的解释。 - Cublax

6
这不是悬空指针或僵尸对象。当你使用!时,意思是“如果这是nil,则崩溃”。在Swift中,你不应该将person视为指针,它是一个值。该值可能是.some(T),也可能是.none(也称为nil)。这两个值都不是悬空的,它们只是两个不同的显式值。Swift的nil与其他语言中的null指针完全不同。只有在你明确要求时,它才会像null指针一样崩溃。
要创建僵尸对象,你需要使用类似Unmanaged的东西。在Swift中,这种情况极为罕见。

1
你没有指向它。在你上面的代码中没有指针。Swift 指针的名称中都有“Unsafe”字样。 - Rob Napier
不是指针,而是强引用。请注意UnsafePointer文档中的差异。指针是Swift中的低级概念,大多数代码不需要使用它们,而是处理值和引用。 - Rob Napier
2
@Honey 引用是通过指针实现的,但同样的,所有的分支语句(函数调用、returnif/else)、数组、闭包和许多其他实体也是如此。指针支撑着许多东西,但这并不意味着这些东西等同于指针。 - Alexander
1
@Honey 在我提到的高级抽象实现中,每当指针支持其中一个实现时,都会实现一些机制来区分抽象的行为和天真指针使用的行为。例如,强引用永远不可能是悬空引用。这是因为强引用具有特殊的行为,可以确保它所指向的对象保持活动状态(通过对其非零保留计数的贡献)。 - Alexander
1
@Honey String 是一个结构体,因此它直接存储在内联中,而不是通过指针/引用。但是像 class C {}; let c = C() 这样的东西就是我所说的强引用。对于指针,你是正确的,尽管 ! 不相关。 - Alexander
显示剩余7条评论

5

僵尸对象是已经被释放但仍然接收消息的Objective-C对象

在Objective-C中,可以使用__unsafe_unretained来创建一个指向对象的额外指针,而不会增加引用计数。在Swift中,这将是unowned(unsafe)

下面是一个自包含的示例:

import Foundation

class MyClass: NSObject {
    func foo() { print("foo"); }

    deinit { print("deinit") }
}

unowned(unsafe) let obj2: MyClass
do {
    let obj1 = MyClass()
    obj2 = obj1
    print("exit scope")
}
obj2.foo()

obj1是指向一个新创建的对象的指针,而obj2是另一个指向同一对象的指针,但不会增加引用计数。当离开do { ... }块时,对象将被释放。通过obj2发送消息会导致Zombie错误:

exit scope
deinit
*** -[ZombieTest.MyClass retain]: message sent to deallocated instance 0x1005748d0

如果您使用Managed(例如将指针转换为C void指针以便将它们传递给C回调函数),但未选择正确的保留/释放组合,则也可能发生此情况。一个人为制造的例子是:
let obj2: MyClass
do {
    let obj1 = MyClass()
    obj2 = Unmanaged.passUnretained(obj1).takeRetainedValue()
    print("exit scope")
}
obj2.foo()

不增加引用计数,这与弱引用有何不同? - mfaani
如果对象被释放,弱引用将被设置为nil。但是,如果使用未拥有的(不安全)引用(指向NSObject的实例),您将收到相同的Zombie消息,因此您是正确的,不需要进行托管操作。- 因此,这与Cristik所说的非常相似,只是使用了未拥有的(不安全)而不是未拥有,并且是NSObject的子类,因此您确实会收到Zombie错误消息。 - Martin R

1

Swift Zombie Object

僵尸对象是一种已释放的对象(引用计数为零),但仍能够像正常对象一样运行(接收消息)。这是由于悬空指针所造成的。

悬空指针指向内存中某个地址,其中数据是不可预测的。

  • Objective-C有unsafe_unretained[关于]属性。[示例]

  • Swift拥有:

    • unowned(unsafe)[关于]引用
    • Unmanaged - 一个非ARC Objc-代码的包装器
unowned(unsafe) let unsafeA: A

func main() {
    let a = A() // A(ref count = 1)
    unsafeA = a 
} // A(ref count = 0), deinit() is called

func additional() {
    unsafeA.foo() //<- unexpected
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接