Friday Q&A 2015-12-11:Swift 中的弱引用

做者:Mike Ash,原文连接,原文日期:2015-12-11
译者:riven;校对:Cee;定稿:千叶知风html

即使你已经在火星的一个洞穴里,紧闭着你的双眼而且捂住你的耳朵,也避免不了 Swift 已经开源的事实。正由于开源,咱们可以更加方便地去探索 Swift 中的不少有趣的特性,其中之一即是 Swift 中弱引用是如何工做的问题。git

弱引用

在采用垃圾回收器或者引用计数进行内存管理的语言中,强引用可使得特定的对象一直存活,但弱引用就没法保证。当一个对象被强引用时,它是不可以被销毁的;可是若是它是个弱引用,就能够。github

当咱们所提到「弱引用」时,一般的意思是指一个归零弱引用(Zeroing Weak Reference)。也就是说,当弱引用的目标对象被销毁时,弱引用就会变成 nil(校者注:看这篇文章了解更多)。非归零弱引用也是存在的,它会致使一些陷阱(Trap)、崩溃(Crash)或者未定义行为的调用。好比你在 Objective-C 中使用 unsafe_unretained,或者在 Swift 中使用 unowned(Objective-C 未定义行为处理方式,而 Swift 却很可靠地处理这些崩溃)。objective-c

归零弱引用很方便使用,在基于引用计数进行内存管理的语言中他们是很是有用的。它们容许循环引用存在却不会产生死循环,而且不须要手动打破逆向引用。他们很是的有用,在苹果引入 ARC 和让弱引用在垃圾收集代码以外的语言层面上可用以前,我就已经实现了我本身的弱引用版本swift

它是如何工做的呢?

归零弱引用比较典型的实现方式是保持一个对每一个对象的全部弱引用列表。当对一个对象建立了弱引用,这个引用就会被添加到这个列表中。当这个引用被从新赋值或者超出了其做用域,它就会从列表中被移除。当一个对象被销毁,这个列表中的全部引用都会被归零。在多线程的状况下,其实现必须是同步获取一个弱引用并销毁一个对象,以免竞态条件的出现:好比当一个线程释放某个对象的最后一个强引用而同时另外一个线程却试图加载一个它的一个弱引用。安全

在个人实现中,每个弱引用都是一个完整的对象。弱引用列表是一个弱引用对象的集合。虽然因为额外的转换和内存使用让效率变低了,但这种方式能够很方便的让这些引用变成完整的对象。多线程

苹果公司的 Objective-C 的实现是这样的,每个弱引用是一个指向目标对象的普通指针。编译器并不直接读写指针,而是使用一些帮助函数。当存储一个弱指针时,存储函数会将指针的位置注册为目标对象的一个弱引用。因为读取函数被集成进了引用计数系统,这就确保了在读取一个弱指针时,不会返回一个已经被释放了的对象的指针。app

归零操做

让咱们建立一些代码来研究一下它们到底是怎么运行的。函数

咱们但愿写一个函数可以 dump 一个对象的内存内容。这个函数接受一块内存区域,将其按指针大小进行分块,而且将最终的结果转换成一个易于查看的十六进制字符串:性能

func contents(ptr: UnsafePointer<Void>, _ length: Int) -> String {
    let wordPtr = UnsafePointer<UInt>(ptr)
    let words = length / sizeof(UInt.self)
    let wordChars = sizeof(UInt.self) * 2

    let buffer = UnsafeBufferPointer<UInt>(start: wordPtr, count: words)
    let wordStrings = buffer.map({ word -> String in
        var wordString = String(word, radix: 16)
        while wordString.characters.count < wordChars {
            wordString = "0" + wordString
        }
        return wordString
    })
    return wordStrings.joinWithSeparator(" ")
}

下一个函数会为一个对象建立一个 dump 函数。调用时传入一个对象,它会返回一个 dump 这个对象内容的函数。在函数内部,咱们给对象保存了一个 UnsafePointer,而不是普通的引用。这样能够确保它不会和语言的引用计数系统发生交互。它容许咱们能够在这个对象被销毁以后 dump 出它的内存,后面咱们会介绍。

func dumperFunc(obj: AnyObject) -> (Void -> String) {
    let objString = String(obj)
    let ptr = unsafeBitCast(obj, UnsafePointer<Void>.self)
    let length = class_getInstanceSize(obj.dynamicType)
    return {
        let bytes = contents(ptr, length)
        return "\(objString) \(ptr): \(bytes)"
    }
}

下面是一个包含弱引用变量的类,后面我会观察这个弱引用。我在弱引用变量的先后分别添加了一个 dummy 变量,以便于咱们区分弱引用在 dump 出来的内存结构中的位置:

class WeakReferer {
    var dummy1 = 0x1234321012343210
    weak var target: WeakTarget?
    var dummy2: UInt = 0xabcdefabcdefabcd
}

让咱们试一下! 咱们先建立一个引用,而后 dump 它:

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())

打印结果:

WeakReferer 0x00007f8a3861b920: 0000000107ab24a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

咱们看到 isa 指针位于最开始的位置,紧随其后的是其它一些内部字段。dummy1 变量占据了第四块,dummy2 变量占据了第六块。正如咱们所指望的那样,在他们之间的弱引用正好是零。

如今咱们让这个弱引用指向一个目标对象,看看会变成什么样。我将这段代码放入一个 do语句中,以便于当目标对象超出做用域和被销毁时咱们能够进行控制:

do {
    let target = NSObject()
    referer.target = target
    print(target)
    print(refererDump())
}

打印结果:

<NSObject: 0x7fda6a21c6a0>
WeakReferer 0x00007fda6a000ad0: 00000001050a44a0 0000000200000004 1234321012343210 00007fda6a21c6a0 abcdefabcdefabcd

正如咱们指望的那样,目标对象的指针直接存储在弱引用中。在目标对象被销毁以后,咱们在 do 代码块以后再次调用 dump 函数:

print(refererDump())

WeakReferer 0x00007ffe32300060: 000000010cfb44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

它被归零了。点个赞!

仅仅为了好玩,咱们用一个纯 Swift 对象做为对象来重复这个实验。没必要要时,我并非很想使用 Objective-C 中的东西。下面是一个纯 Swift 对象:

class WeakTarget {}

让咱们试一下:

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
    class WeakTarget {}
    let target = WeakTarget()
    referer.target = target
    print(refererDump())
}
print(refererDump())

目标对象像咱们指望的那样被归零了,而后被从新赋值:

WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 00007fbe95121ce0 abcdefabcdefabcd

而后当目标对象被销毁,引用应该被归零:

WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 00007fbe95121ce0 abcdefabcdefabcd

不幸的是它并无被归零。多是目标对象没有被销毁。必定是有某些东西让它继续活着!让咱们再检查一下:

class WeakTarget {
    deinit { print("WeakTarget deinit") }
}

再次运行代码,结果以下:

WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 00007fd29a42a920 abcdefabcdefabcd
WeakTarget deinit
WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 00007fd29a42a920 abcdefabcdefabcd

它消失了,可是弱引用并无归零。怎么回事呢,咱们发现了 Swift 的一个 bug!很神奇,这个 bug 一直没有被解决。你会想以前确定已经有人已经注意到了这个问题。接下来,咱们经过访问弱引用来产生一个崩溃,而后咱们能够用这个 Swift 工程提交这个 bug :

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
    class WeakTarget {
        deinit { print("WeakTarget deinit") }
    }
    let target = WeakTarget()
    referer.target = target
    print(refererDump())
}
print(refererDump())
print(referer.target)

下面就是崩溃信息:

WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
WeakTarget deinit
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
nil

哦,个人天呐!大爆炸在哪呢?应该有一个惊天动地的大爆炸呀!输出的内容代表一切工做正常,但咱们能够清楚地从 dump 内容看到它并无正常工做。

让咱们再仔细检查一下。下面是一个通过修改的 WeakTarget 类,咱们添加了一个 dummy 变量以便于区分 dump 的内容:

class WeakTarget {
    var dummy = 0x0123456789abcdef

    deinit {
        print("Weak target deinit")
    }
}

下面是一段新的代码,运行的程序和以前的基本相同,只不过每次 dump 都会输出两个对象(校者注:Target 和 Referer):

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
let targetDump: Void -> String
do {
    let target = WeakTarget()
    targetDump = dumperFunc(target)
    print(targetDump())

    referer.target = target

    print(refererDump())
    print(targetDump())
}
print(refererDump())
print(targetDump())
print(referer.target)
print(refererDump())
print(targetDump())

让咱们检查一下输出内容。referer 对象的生命周期和以前同样,它的 target 字段被顺利的归零了:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

target 首先做为一个普通对象,在各类头字段以后紧跟着咱们的 dummy 字段:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000004 0123456789abcdef

在给 target 字段赋值后,咱们能够看到被填充的指针的值:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd

target 对象仍是和以前同样,可是它其中一个头字段增长了 2:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000400000004 0123456789abcdef

目标对象像咱们指望的那样被销毁了:

Weak target deinit

咱们看到引用对象一直都有一个指针指向目标对象:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd

而且目标对象自己一直存活着。和上次咱们看到的相比,它的头字段减小了 2:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000002 0123456789abcdef

访问 target 字段会产生 nil ,即使它没有被归零:

nil

再次 dump referer 对象的内容,从中咱们看出仅仅访问 target 字段的行为已经改变了它。如今它被归零了:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

目标对象如今被彻底抹掉了:

WeakTarget 0x00007fe17341d270: 200007fe17342a04 300007fe17342811 ffffffffffff0002

如今变的愈来愈有趣了。咱们看到头字段会一下子增长,一下子减小;让咱们看看是否能有重现出更多的信息:

let target = WeakTarget()
let targetDump = dumperFunc(target)
do {
    print(targetDump())
    weak var a = target
    print(targetDump())
    weak var b = target
    print(targetDump())
    weak var c = target
    print(targetDump())
    weak var d = target
    print(targetDump())
    weak var e = target
    print(targetDump())

    var f = target
    print(targetDump())
    var g = target
    print(targetDump())
    var h = target
    print(targetDump())
    var i = target
    print(targetDump())
    var j = target
    print(targetDump())
    var k = target
    print(targetDump())
}
print(targetDump())

打印结果:

WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000200000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000400000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000600000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000800000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000a00000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000008 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c0000000c 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000010 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000014 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000018 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c0000001c 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000200000004 0123456789abcdef

咱们看到每个新的弱引用会让头字段中的第一个数增长 2。每个新的强引用会让头字段中的第二个数增长 4。

回顾一下,下面这些就是目前咱们所发现的:

  • 在内存中弱指针和普通指针是同样的.

  • 当一个弱目标对象(WeakTarget)的 deinit 方法调用时,目标对象是不会被释放的,而且弱指针也不会被归零。

  • 当目标对象的 deinit 方法执行以后,访问弱指针,它就会被归零而且弱目标对象也会被释放。

  • 弱目标对象包含一个弱引用的引用计数,与强引用计数分离开。

Swift 代码

既然 Swift 已经开源,咱们能够经过查看源代码来继续咱们的观察。

在 Swift 标准库中用 HeapObject 类型来表示一个分配在堆上的对象,其实现可参考 stdlib/public/SwiftShims/HeapObject.h。看起来是这样的:

cpp
struct HeapObject {
/// 这始终是一个有效的元数据对象的指针。
struct HeapMetadata const *metadata;

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
// FIXME: 在 32 位的平台上分配了两个字大小的元数据。

#ifdef __cplusplus
HeapObject() = default;

// 给新分配的堆内存初始化空间(对象alloc,是分配的堆内存)。
constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCount(StrongRefCount::Initialized)
    , weakRefCount(WeakRefCount::Initialized)
{ }
#endif
};

Swift 的 metadata 字段就至关于 Objective-C 的 isa 字段,而且它们是兼容的。还有一些像 NON_OBJC_MEMBERS 这样的宏定义:

cpp
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  StrongRefCount refCount;                      \
  WeakRefCount weakRefCount

噢,快看!这就是咱们的两个引用计数。

(附加问题:为何这里强引用在前面,而在 dump 时确是弱引用在前面?)

引用计数是经过位于 stdlib/public/runtime/HeapObject.cpp 文件中的一系列函数来进行管理的。好比,下面的 swift_retain

cpp
void swift::swift_retain(HeapObject *object) {
SWIFT_RETAIN();
    _swift_retain(object);
}
static void _swift_retain_(HeapObject *object) {
    _swift_retain_inlined(object);
}
auto swift::_swift_retain = _swift_retain_;

这里面拐了几个弯,但它最终是调用头文件中的内联函数:

cpp
static inline void _swift_retain_inlined(HeapObject *object) {
  if (object) {
    object->refCount.increment();
  }
}

如你所见,它会增长引用计数。下面是 increment 函数的实现:

cpp
void increment() {
  __atomic_fetch_add(&refCount, RC_ONE, __ATOMIC_RELAXED);
}

RC_ONE 来自于一个枚举类型:

cpp
enum : uint32_t {
  RC_PINNED_FLAG = 0x1,
  RC_DEALLOCATING_FLAG = 0x2,

  RC_FLAGS_COUNT = 2,
  RC_FLAGS_MASK = 3,
  RC_COUNT_MASK = ~RC_FLAGS_MASK,

  RC_ONE = RC_FLAGS_MASK + 1
};

相信你已经明白为何每个新的强引用会让头字段增长 4 了吧。这个枚举类型的前两位用来做为标志位。回想一下以前的 dump 结果,咱们能够看到这些标志位。下面是一个弱目标对象在最后一个强引用消失以前和以后的结果:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000400000004 0123456789abcdef
Weak target deinit
WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000002 0123456789abcdef

其中第二个字段开始是 4,表示引用计数为 1 而且没有标志位,以后变成了 2,表示引用计数为 0 和 RC_DEALLOCATING_FLAG 标志位被设定了。这个被析构的对象被放在了处于 DEALLOCATING 状态的位置。

(顺便说一句,RC_PINNED_FLAG 究竟是用来干什么的呢?我查找了相关代码,除了可以代表一个「固定的对象(pinned object)」外,其它对于这个标记一无所知。若是你弄清楚了或者有一些相关的猜想,请给我留言。)

如今让咱们看一看弱引用计数的实现。它有一样的枚举结构:

cpp
enum : uint32_t {
  // There isn't really a flag here.
  // Making weak RC_ONE == strong RC_ONE saves an
  // instruction in allocation on arm64.
  RC_UNUSED_FLAG = 1,

  RC_FLAGS_COUNT = 1,
  RC_FLAGS_MASK = 1,
  RC_COUNT_MASK = ~RC_FLAGS_MASK,

  RC_ONE = RC_FLAGS_MASK + 1
};

这就是 2 的来源:其中有一个保留的标志位,目前还没有被使用。奇怪的是,关于这段代码的注释彷佛是不正确的,这的 RC_ONE 等于 2,而强引用的 RC_ONE 等于 4。我猜它们曾经是相等的,但后来它被修改了而注释却没有更新。我只是想代表若是注释是无用的,那你为何还要写它呢。

全部这些是如何和加载弱引用相关联的呢?它是由 swift_weakLoadStrong 函数来处理的:

cpp
HeapObject *swift::swift_weakLoadStrong(WeakReference *ref) {
  auto object = ref->Value;
  if (object == nullptr) return nullptr;
  if (object->refCount.isDeallocating()) {
    swift_weakRelease(object);
    ref->Value = nullptr;
    return nullptr;
  }
  return swift_tryRetain(object);
}

从上面的代码,惰性归零是如何工做的已经一目了然了。当加载一个弱引用时,若是目标对象正在被销毁,就会对这个引用进行归零。反之,会保留目标对象并返回它。进一步深挖一点,咱们能够看到 swift_weakRelease 如何释放对象的内存,前提是它是最后一个引用:

cpp
void swift::swift_weakRelease(HeapObject *object) {
  if (!object) return;

  if (object->weakRefCount.decrementShouldDeallocate()) {
    // 只有对象能够 weak-retained 和 weak-released
    auto metadata = object->metadata;
    assert(metadata->isClassObject());
    auto classMetadata = static_cast<const ClassMetadata*>(metadata);
    assert(classMetadata->isTypeMetadata());
    swift_slowDealloc(object, classMetadata->getInstanceSize(),
                      classMetadata->getInstanceAlignMask());
  }
}

(注意:若是你正在查看版本库中的代码,使用「weak」命名的地方大多数都改为了「unowned」。上面的命名是截至撰写本文时最新的快照,但开发仍在继续。你能够查看和我这对应的版本库中的 2.2 版本的快照,或者获取最新的版本可是要注意命名的变化,而且实现也有可能发生了改变。)

整合

咱们已经在层级上自上往下地看到了 Swift 中的弱引用是如何实现的。那么在高层观察 Swift 的弱引用又是如何工做的呢?

  1. 弱引用只是指向目标对象的指针。

  2. 在 Objective-C 中是没有办法单独追踪弱引用的。

  3. 相反,每个 Swift 对象都有一个弱引用计数,和它的强引用计数相邻。

  4. Swift 将对象的析构过程(deinit)和对象的释放(dealloc)解耦。一个对象能够被析构并释放它的外部资源,但没必要释放对象自己所占用的内存。

  5. 当一个 Swift 对象的强引用计数变成零而弱引用计数仍大于零时,那么这个对象会被析构,可是不会被释放。

  6. 这意味着一个被释放对象的弱指针仍然是一个有效的指针,它能够被反向引用而不会崩溃或者加载垃圾数据。它们只是指向一个处于僵尸状态的对象。

  7. 当一个弱引用被加载时,运行时会检查目标对象的状态。若是目标对象是一个僵尸对象,而后它会对弱引用进行归零,也就是减小弱引用计数并返回 nil

  8. 当僵尸对象的全部弱引用都被归零,那么这个僵尸对象就会被释放。

比起 Objective-C 中的实现,这种设计会带来一些有趣的结果:

  • 不须要维护一个弱引用列表。这样既简化代码也提升了性能。

  • 在一个线程归零一个弱引用和另一个线程加载一个弱引用之间就不会存在竞态条件了。这也意味着加载一个弱引用和销毁一个弱引用对象不须要加锁。这也提升了性能。

  • 一个对象即使没有了强引用,可是弱引用任然会致使该对象被分配的内存被占用,直到全部弱引用被加载或者被丢弃。这种作法临时增长了内存使用。可是要注意的是这个影响很小,当目标对象没有被释放时,它所占的内存大小只是实例自己。当最后一个强引用变成零时,全部的外部资源(包括用于存储的 ArrayDictionary 属性)都会被释放。弱引用会致使被分配的单个实例不会被释放,而不是整个对象树。

  • 每个对象都须要额外的内存来存储弱引用计数。但在实际的 64 位系统中,这彷佛是可有可无的。头字段要占据全部指针大小的块的数量,而且强和弱引用计数共享一个头字段。若是没有弱引用计数,强引用计数就会占据整个 64 位。经过使用非指针(non-pointer) isa 能够将强引用移到 isa 中,但我不肯定那是否是很重要或者它将来会如何发展。 对于 32 位系统,弱引用计数会将对象的大小增长四个字节。然而,32 位系统现在已经没有那么重要了.

  • 由于访问一个弱指针是如此的方便,因此 unowned 的语义也采用了相同的机制来实现。unownedweak 工做方式是同样的,只是当目标对象被释放,unowned 会给你一个大大的失败,而不是给你返回一个 nil 。在 Objective-C 中,__unsafe_unretained 是做为一个带有未定义行为的原始指针来实现的,你能够快速的访问它,毕竟加载一个弱指针仍是有点慢。

总结

Swift 的弱指针经过一种有趣的方式,既保证了速度和正确性,也保证较低的内存开销。经过追踪每一个对象的弱引用计数,将对象的销毁和对象的析构过程分离开来,弱引用问题被安全而又快速的获得解决。正是因为能够查看标准库的源代码,这让咱们能够在源代码级别看到究竟发生了什么,而不是像咱们以前经过反编译和 dump 内存来进行研究。固然,正如你上面看到的那样,咱们很难彻底打破这个习惯。

今天就这样了。下次回来会带来更多的干货。因为假期的缘故,可能须要几周,可是我会在以前发布一篇稍微短一点的文章。无论怎样,给接下来的话题提更多的建议吧。周五问答是由读者们的想法驱动的,若是你有一个你但愿了解的想法,请告知我!

本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg

相关文章
相关标签/搜索