【问题标题】:Swift deinit is being called but object still not deallocatingSwift deinit 被调用,但对象仍未解除分配
【发布时间】:2015-05-15 04:31:47
【问题描述】:

在 swift 中,我让 deinit 函数打印出一行,说明对象已被取消初始化,但该对象仍在 Instruments、分配工具中报告为活动的。我什至不认为这是可能的。有没有办法找出为什么它没有被释放?或者有没有办法找出哪些子对象可能会阻止它?

【问题讨论】:

  • 你能解决吗?我有同样的问题

标签: xcode swift instruments


【解决方案1】:

确保您与其他对象没有任何牢固的关系。想象一下:

class A {
   var b: B
}

class B {
   var a: A
}

a.b = xxx
b.a = yyy

如果 A 持有对 B 的强引用,而 B 持有对 A 的强引用,则在它们之间创建一个强引用循环,并设置
a = 无 不会调用 deinit ,因为它拥有对 b 的强引用。您要么将 a.b 设置为 nil,要么使用弱引用(weak 关键字)来解决这个问题。

查看苹果文档here了解更多详情

【讨论】:

  • 线程启动器说 deinit 被调用。如果 deinit 被调用,没有人可以持有强引用。
  • 是的,保留计数必须达到 0 才能调用 deinit
【解决方案2】:

我怀疑您正在查看不同的对象。为什么您认为名为@9​​87654321@ 的对象与您在 Instruments 中看到的对象相同?实例数量超出您的想象(或只是查看错误的实例)是导致此类混淆的最常见原因之一。

您的deinit 只是打印语句,还是您在做其他事情?特别是,您是否正在做任何可能会意外再次留住您的事情? (我不记得这是否是明确定义的行为。)

【讨论】:

  • 我有同样的问题,我真的不知道为什么仪器告诉我它是一个瞬态对象,但它被调用了 deinit。有什么原因,为什么会发生这种情况?
【解决方案3】:

更新:对于 Swift 4,请参阅末尾的附加说明。

警告:这个答案详细介绍了 Swift 运行时的实现方式。此处的信息不会影响您在 Swift 中编写代码的方式,除非是在一些高级场景中。主要的一点是,从你作为程序员的角度来看,一旦调用了deinit,对象对你来说就死了,你不能再使用它了。

内存未被释放的原因是 Swift 中的对象在被取消初始化时不一定会立即被释放(释放)。对对象的弱引用将导致对象的“外壳”保持分配状态(在内存中 - 你仍然不能使用它!),直到所有弱引用都被清零。

Swift 中的弱引用在对象被取消初始化时不会立即归零,但在下次访问它们时它们会归零。也就是说,Swift 会懒惰地将弱引用归零。这是一个例子:

public class MyClass {
}
var object = MyClass()
weak var weakObject = object
print (weakObject) // Points at a MyClass instance
object = MyClass()
// A: weakObject is not yet nil
print(weakObject) // prints 'nil'
// B: now weakObject is nil

在将object 分配给一个新实例之后(第6 行),您会认为对原始对象的弱引用将为零,但它不是(还)。该对象已deinited,但仍保持已分配(在内存中),直到所有弱引用都消失为止。在A 点,弱引用仍然存在,它仅在您尝试评估弱引用时出现在下一行,Swift 检查并注意到它引用的对象已取消初始化,因此它将弱引用归零,然后将其传递给要打印的print 函数。这种机制需要对象的空壳保持分配状态,直到所有弱引用都消失为止。它被称为外壳,因为它的所有属性都已归零并在deinit 中释放,因此它不会保留其他任何东西(对象的内存量非常小,仅足以存储其内部标题和成员) .

为什么以及如何?

每个对象都有一个内部弱引用计数,而不是需要归零的引用列表。这比deinit 更快且占用更少的资源,因为以线程安全的方式清零弱引用列表需要相当长的原子/同步操作。

当强引用计数为零时,调用deinit,对象进入deallocating状态。运行时保持分配的内存,因为它需要在访问弱引用时检查对象的状态。一旦所有弱引用都被访问并归零(弱引用计数为零),内存将被释放并完成释放。

从 swift 源代码中查看swift_weakLoadStrong 的实现 - 这是在访问弱引用并使其成为强引用时插入的代码(例如,分配给强引用或传递给函数等等。)。我在下面缩写了它。查看original code on github 以了解加载弱引用的全部复杂性:

if (object == nullptr) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    return nullptr;
}
if (object->refCount.isDeallocating()) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    SWIFT_RT_ENTRY_CALL(swift_unownedRelease)(object);
    return nullptr;
}
auto result = swift_tryRetain(object);
__atomic_store_n(&ref->Value, ptr, __ATOMIC_RELAXED);
return result;

您可以看到对象外壳仍然存在于内存中,加载弱引用的机制(即当您在代码中访问它时)检查它是否正在释放,如果是,它将弱引用归零,调用@ 987654336@ 减少弱引用计数并在计数为零时释放对象,并返回nullptr (nil)。

Swift 4 更新

从 Swift 4 开始,弱引用有了改进的实现,这意味着对象外壳不再需要闲逛。取而代之的是一个小的边表,而弱引用实际上指向了它。边表包含一个指向真实对象的指针,Swift 知道在访问弱引用时要遵循这个指针。如需更详细的说明,请阅读 Mike Ash 的 this great blog post

【讨论】:

  • 我不确定其中有多少是真的,苹果文档说这个Deinitializers are called automatically, just before instance deallocation takes placedeveloper.apple.com/library/content/documentation/Swift/…
  • @Knight0fDragon 我喜欢你的怀疑态度,这对学习新事物很有好处。查看更新,我添加了更多细节。
  • "不谈太多细节(因为我不知道全部),发生的情况是对象被取消初始化但保持分配状态" 不。调用deinit 的对象是不复存在;它不会“保持分配状态”。当然,对这个对象的弱引用需要一些记录,这将花费一些有限的时间,但它会在你有机会通过悬空指针引用对象之前发生,这才是最重要的。
  • @matt 我的术语有误吗?对象的“外壳”(即为对象分配的内存)在它被取消初始化后绝对会一直存在,直到所有弱引用都被访问并归零。请参阅:a)我的更新中链接的快速实现源代码; b) Mike Ash、John McCall 和 Chris Lattner 之间的交流,Mike 修复了实现中的线程安全问题,他们讨论了这些外壳的优缺点:lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151207/…
  • OP所看到的与您在谈论的内容无关,而您在谈论的内容仅仅是实现细节。我要争辩的是,人们不应该用不可见的幕后细节来回答 OP 的问题。 Swift 的幕后工作方式可能会让一些人着迷,但它不应该被用来在普通用户的眼中撒灰尘。
猜你喜欢
  • 2016-01-17
  • 2017-01-26
  • 2015-10-21
  • 1970-01-01
  • 1970-01-01
  • 2019-12-14
  • 1970-01-01
  • 2014-08-13
  • 1970-01-01
相关资源
最近更新 更多