【问题标题】:How can I demonstrate a zombie object in Swift?如何在 Swift 中演示僵尸对象?
【发布时间】:2021-10-05 02:51:54
【问题描述】:

我读过How to demonstrate memory leak and zombie objects in Xcode Instruments? 但那是针对objective-c 的。这些步骤不适用。

通过阅读here,我了解到僵尸是以下对象:

  • 已解除分配
  • 但某些指针仍在尝试指向它们并向它们发送消息。

不完全确定这与访问已释放的对象有何不同。

我的意思是在 Swift 中你可以这样做:

var person : Person? = Person(name: "John")
person = nil
print(person!.name)

人被释放了吗?是的!

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

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

【问题讨论】:

  • 在 Swift 中我不会担心这一点。只要确保你不使用强制解包选项(我只将它们用于IBOutlets),你不会有问题。
  • 这正是我的想法。这here 是否适用:假设您有一个缓存,其条目是从某个 URL 下载的 NSData 实例,其中 URL 在 URL 中包含会话 ID,并且会话 ID + URL 用作查看的键缓存中的东西。现在,假设用户注销,导致会话 ID 被破坏。如果缓存中没有删除特定于该会话 ID 的所有条目,那么所有这些 NSData 对象都将被丢弃
  • 请注意,您提供的 Swift 示例不是悬空指针的示例——您将引用设置为 nil,这意味着您不再拥有对该对象的引用,无论是否仍然分配。在 Swift 中获取悬空指针的最简单示例可能是使用 Unmanaged,例如 class C {}; var c = C(); Unmanaged.passUnretained(c).release()c 现在是一个悬空指针。不过,这不是一个“常见错误”——而且如果不使用这种不安全的结构,你永远无法在 Swift 中获得悬空指针(因为 Swift 默认情况下是安全的)。
  • 也就是说,目前有一个不带临时指针转换的footgun,可以创建悬空指针,例如let ptr = UnsafePointer([1, 2, 3])ptr 是一个悬空指针,因为数组到指针的转换只产生一个指针在通话期间有效。希望在github.com/apple/swift/pull/20070 中对此类转换发出警告(并最终出错)。
  • 糟糕:*带有临时指针转换

标签: swift memory-management nszombie dangling-pointer


【解决方案1】:

Zombie Object 是一个被释放的对象,它的行为就像活着一样。有可能是因为dangling pointer

dangling pointer 指向内存中数据不可预测的某个地址

  • Objective-C 具有unsafe_unretained[About] 属性属性。 [Example]

  • 斯威夫特有

    • unowned(unsafe)[About]参考
    • Unmanaged - non ARC Objc-code 的包装器
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
}

【讨论】:

    【解决方案2】:

    Zombie objectsObjective-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 向其发送消息会导致僵尸错误:

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

    如果您使用Managed(例如convert pointers to Swift objects to C void pointers in order to pass them to C callback functions)并且您没有选择正确的保留/未保留组合,也会发生这种情况。一个人为的例子是:

    let obj2: MyClass
    do {
        let obj1 = MyClass()
        obj2 = Unmanaged.passUnretained(obj1).takeRetainedValue()
        print("exit scope")
    }
    obj2.foo()
    

    【讨论】:

    • “不增加引用计数器”与弱引用有何不同?
    • 如果对象被释放,弱引用将被设置为 nil。但是您会收到带有无主(不安全)引用(到 NSObject 的实例)的相同 Zombie 消息,因此您认为不需要托管杂耍是正确的。 – 所以这实际上与 Cristik 所说的非常相似,只是用 unowned(unsafe) 而不是 unowned,以及 NSObject 的子类,这样你才能真正得到 Zombie 错误消息。
    【解决方案3】:

    这是不到 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 的无主引用,一旦Parent 被释放,该引用就变得无效。该引用包含一个指向不再存在的父 (RIP) 的指针,访问该指针会导致崩溃并显示类似于以下的消息:

    致命错误:尝试读取无主引用但对象 0x1018362d0 已被释放2018-10-29 20:18:39.423114+0200 MyApp[35825:611433] 致命错误:尝试读取无主引用但对象 0x1018362d0 已被释放

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

    【讨论】:

    • 从技术上讲,这不是一个悬空指针,因为 Swift 会跟踪无主引用,以确保在所有强引用消失后,它总是会陷入尝试访问它的陷阱。您可以使用unowned(unsafe) 来移除此安全网并获得一个实际的悬空指针,该指针会在释放后为访问产生未定义的行为。
    • @Hamish 公平点,删除了“悬空指针”引用。
    • 将其称为“僵尸”也可能令人困惑,因为它不会出现在 Cocoa 生态系统中与僵尸相关的任何东西中。这是一个 Swift 特有的东西,一个无主引用,而不是一个悬空指针。但我认为这为讨论增添了很多内容(指出有一个 Swift 特有的东西可能会导致与内存相关的 spooky-crash-at-a-distance without Unsafe),只要不引导读者相信这和 NSZombie 有什么关系。
    • 同样在这里,您不会得到 EXC_BAD_ACCESS,它来自相同的原则,但不会让您知道谁是父级、何时释放、谁是子级以及何时尝试访问父级。这是这个问题的戏剧性部分。但很好的解释
    【解决方案4】:

    这不是一个悬空指针或僵尸。当您使用 ! 时,您是在说“如果这是 nil,则崩溃”。您不应该将person 视为 Swift 中的指针。这是一个价值。该值可能是.some(T),也可能是.none(也称为nil)。这些都不是悬空的。它们只是两个不同的显式值。 Swift 的nil 与其他语言中的空指针完全不同。当你明确要求它时,它只会像空指针一样崩溃。

    要创建僵尸,您需要使用 Unmanaged 之类的东西。这在 Swift 中极为罕见。

    【讨论】:

    • 根据经验我知道它不是僵尸。但根据我正在阅读的内容,它符合描述,即。 1. 它被释放 2. 我指着它。那我错过了什么?
    • 你没有指向它。上面的代码中没有指针。 Swift 指针的名称中都有Unsafe
    • @Honey 引用由下面的指针实现,但话又说回来,所有分支语句(函数调用,returnif/else)、数组、闭包和许多其他实体也是如此.指针支持许多事物,但这并不意味着这些事物等同于指针。
    • @Honey 在每种情况下,当指针支持我提到的更高级别抽象之一的实现时,都会实现一些机制来区分抽象行为与幼稚指针使用的行为。例如,强引用永远不会是悬空引用。这是因为强引用具有特殊行为,可确保它指向的对象保持活动状态(通过贡献其非零保留计数)。
    • @Honey String 是一个结构,所以它直接内联存储,而不是通过指针/引用。但是像class C {}; let c = C() 这样的东西就是我所说的强参考。你是正确的指针,虽然! 不相关。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-14
    • 1970-01-01
    相关资源
    最近更新 更多