Friday Q&A 2015-12-11:Swift 中的弱引用
即便你已经在火星的一个洞穴里,紧闭着你的双眼并且捂住你的耳朵,也避免不了 Swift 已经开源的事实。正因为开源,我们能够更加方便地去探索 Swift 中的很多有趣的特性,其中之一便是 Swift 中弱引用是如何工作的问题。 弱引用在采用垃圾回收器或者引用计数进行内存管理的语言中,强引用可以使得特定的对象一直存活,但弱引用就无法保证。当一个对象被强引用时,它是不能够被销毁的;但是如果它是个弱引用,就可以。 当我们所提到「弱引用」时,通常的意思是指一个归零弱引用(Zeroing Weak Reference)。也就是说,当弱引用的目标对象被销毁时,弱引用就会变成 归零弱引用很方便使用,在基于引用计数进行内存管理的语言中他们是非常有用的。它们允许循环引用存在却不会产生死循环,并且不需要手动打破逆向引用。他们非常的有用,在苹果引入 ARC 和让弱引用在垃圾收集代码之外的语言层面上可用之前,我就已经实现了我自己的弱引用版本。 它是如何工作的呢?归零弱引用比较典型的实现方式是保持一个对每个对象的所有弱引用列表。当对一个对象创建了弱引用,这个引用就会被添加到这个列表中。当这个引用被重新赋值或者超出了其作用域,它就会从列表中被移除。当一个对象被销毁,这个列表中的所有引用都会被归零。在多线程的情况下,其实现必须是同步获取一个弱引用并销毁一个对象,以避免竞态条件的出现:比如当一个线程释放某个对象的最后一个强引用而同时另一个线程却试图加载一个它的一个弱引用。 在我的实现中,每一个弱引用都是一个完整的对象。弱引用列表是一个弱引用对象的集合。虽然由于额外的转换和内存使用让效率变低了,但这种方式可以很方便的让这些引用变成完整的对象。 苹果公司的 Objective-C 的实现是这样的,每一个弱引用是一个指向目标对象的普通指针。编译器并不直接读写指针,而是使用一些帮助函数。当存储一个弱指针时,存储函数会将指针的位置注册为目标对象的一个弱引用。由于读取函数被集成进了引用计数系统,这就确保了在读取一个弱指针时,不会返回一个已经被释放了的对象的指针。 归零操作让我们创建一些代码来研究一下它们究竟是怎么运行的。 我们希望写一个函数能够 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 这个对象内容的函数。在函数内部,我们给对象保存了一个 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 我们看到 现在我们让这个弱引用指向一个目标对象,看看会变成什么样。我将这段代码放入一个 do { let target = NSObject() referer.target = target print(target) print(refererDump()) } 打印结果: <NSObject: 0x7fda6a21c6a0> WeakReferer 0x00007fda6a000ad0: 00000001050a44a0 0000000200000004 1234321012343210 00007fda6a21c6a0 abcdefabcdefabcd 正如我们期望的那样,目标对象的指针直接存储在弱引用中。在目标对象被销毁之后,我们在 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 内容看到它并没有正常工作。 让我们再仔细检查一下。下面是一个经过修改的 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 对象的生命周期和之前一样,它的 WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000004 0123456789abcdef 在给 WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd
WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000400000004 0123456789abcdef 目标对象像我们期望的那样被销毁了: Weak target deinit 我们看到引用对象一直都有一个指针指向目标对象: WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd 并且目标对象本身一直存活着。和上次我们看到的相比,它的头字段减少了 2: WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000002 0123456789abcdef 访问 nil 再次 dump referer 对象的内容,从中我们看出仅仅访问 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。 回顾一下,下面这些就是目前我们所发现的:
Swift 代码既然 Swift 已经开源,我们可以通过查看源代码来继续我们的观察。 在 Swift 标准库中用 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 的 cpp #define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS StrongRefCount refCount; WeakRefCount weakRefCount 噢,快看!这就是我们的两个引用计数。 (附加问题:为什么这里强引用在前面,而在 dump 时确是弱引用在前面?) 引用计数是通过位于 stdlib/public/runtime/HeapObject.cpp 文件中的一系列函数来进行管理的。比如,下面的 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); }
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 和 (顺便说一句, 现在让我们看一看弱引用计数的实现。它有同样的枚举结构: 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_ONE = RC_FLAGS_MASK + 1 }; 这就是 2 的来源:其中有一个保留的标志位,目前尚未被使用。奇怪的是,关于这段代码的注释似乎是不正确的,这的 所有这些是如何和加载弱引用相关联的呢?它是由 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); } 从上面的代码,惰性归零是如何工作的已经一目了然了。当加载一个弱引用时,如果目标对象正在被销毁,就会对这个引用进行归零。反之,会保留目标对象并返回它。进一步深挖一点,我们可以看到 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 的弱引用又是如何工作的呢?
比起 Objective-C 中的实现,这种设计会带来一些有趣的结果:
总结Swift 的弱指针通过一种有趣的方式,既保证了速度和正确性,也保证较低的内存开销。通过追踪每个对象的弱引用计数,将对象的销毁和对象的析构过程分离开来,弱引用问题被安全而又快速的得到解决。正是由于可以查看标准库的源代码,这让我们可以在源代码级别看到究竟发生了什么,而不是像我们之前通过反编译和 dump 内存来进行研究。当然,正如你上面看到的那样,我们很难完全打破这个习惯。 今天就这样了。下次回来会带来更多的干货。由于假期的缘故,可能需要几周,但是我会在之前发布一篇稍微短一点的文章。不管怎样,给接下来的话题提更多的建议吧。周五问答是由读者们的想法驱动的,如果你有一个你希望了解的想法,请告知我!
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |