Swift源码解析之弱引用
作者:Cyandev
序言:
各个社区有关 Objective-C weak 机制的实现分析文章有很多,然而 Swift 发布这么长时间以来,有关 ABI 的分析文章一直非常少,似乎也是很多 iOS 开发者未涉及的领域… 本文就从源码层面分析一下 Swift 是如何实现 weak 机制的。
下面话不多说了,来一起看看详细的介绍吧
准备工作
由于 Swift 源码量较大,强烈建议大家把 repo clone 下来,结合源码一起来看这篇文章。
$ git clone https://github.com/apple/swift.git
Swift 整个工程采用了 CMake 作为构建工具,如果你想用 Xcode 来打开的话需要先安装 LLVM,然后用 cmake -G 生成 Xcode 项目。
我们这里只是进行源码分析,我就直接用 Visual Studio Code 配合 C/C++ 插件了,同样支持符号跳转、查找引用。另外提醒一下大家,Swift stdlib 里 C++ 代码的类型层次比较复杂,不使用 IDE 辅助阅读起来会相当费劲。
正文
下面我们就正式进入源码分析阶段,首先我们来看一下 Swift 中的对象(class 实例)它的内存布局是怎样的。
HeapObject
我们知道 Objective-C 在 runtime 中通过 objc_object 来表示一个对象,这些类型定义了对象在内存中头部的结构。同样的,在 Swift 中也有类似的结构,那就是 HeapObject,我们来看一下它的定义:
struct HeapObject { /// This is always a valid pointer to a metadata object. HeapMetadata const *metadata; SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; HeapObject() = default; // Initialize a HeapObject header as appropriate for a newly-allocated object. constexpr HeapObject(HeapMetadata const *newMetadata) : metadata(newMetadata) , refCounts(InlineRefCounts::Initialized) { } // Initialize a HeapObject header for an immortal object constexpr HeapObject(HeapMetadata const *newMetadata, InlineRefCounts::Immortal_t immortal) : metadata(newMetadata) , refCounts(InlineRefCounts::Immortal) { } };
可以看到,HeapObject 的第一个字段是一个 HeapMetadata 对象,这个对象有着与 isa_t 类似的作用,就是用来描述对象类型的(等价于 type(of:) 取得的结果),只不过 Swift 在很多情况下并不会用到它,比如静态方法派发等等。
接下来是 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS,这是一个宏定义,展开后即:
RefCounts<InlineRefCountBits> refCounts;
这是一个相当重要东西,引用计数、弱引用、unowned 引用都与它有关,同时它也是 Swift 对象(文中后续的 Swift 对象均指引用类型,即 class 的实例)中较为复杂的一个结构。
其实说复杂也并不是很复杂,我们知道 Objective-C runtime 里就有很多 union 结构的应用,例如 isa_t 有 pointer 类型也有 nonpointer 类型,它们都占用了相同的内存空间,这样做的好处就是能更高效地使用内存,尤其是这些大量使用到的东西,可以大大减少运行期的开销。类似的技术在 JVM 里也有,就如对象头的 mark word。当然,Swift ABI 中也大量采用这种技术。
RefCounts 类型和 Side Table
上面说到 RefCounts 类型,这里我们就来看看它到底是个什么东西。
先看一下定义:
template <typename RefCountBits> class RefCounts { std::atomic<RefCountBits> refCounts; // ... };
这就是 RefCounts 的内存布局,我这里省略了所有的方法和类型定义。你可以把 RefCounts 想象成一个线程安全的 wrapper,模板参数 RefCountBits 指定了真实的内部类型,在 Swift ABI 里总共有两种:
typedef RefCounts<InlineRefCountBits> InlineRefCounts; typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
前者是用在 HeapObject 中的,而后者是用在 HeapObjectSideTableEntry(Side Table)中的,这两种类型后文我会一一讲到。
一般来讲,Swift 对象并不会用到 Side Table,一旦对象被 weak 或 unowned 引用,该对象就会分配一个 Side Table。
InlineRefCountBits
定义:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits; template <RefCountInlinedness refcountIsInline> class RefCountBitsT { friend class RefCountBitsT<RefCountIsInline>; friend class RefCountBitsT<RefCountNotInline>; static const RefCountInlinedness Inlinedness = refcountIsInline; typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type BitsType; typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType SignedBitsType; typedef RefCountBitOffsets<sizeof(BitsType)> Offsets; BitsType bits; // ... };
通过模板替换之后,InlineRefCountBits 实际上就是一个 uint64_t,相关的一堆类型就是为了通过模板元编程让代码可读性更高(或者更低,哈哈哈)。
下面我们来模拟一下对象引用计数 +1:
调用 SIL 接口 swift::swift_retain:
HeapObject *swift::swift_retain(HeapObject *object) { return _swift_retain(object); } static HeapObject *_swift_retain_(HeapObject *object) { SWIFT_RT_TRACK_INVOCATION(object, swift_retain); if (isValidPointerForNativeRetain(object)) object->refCounts.increment(1); return object; } auto swift::_swift_retain = _swift_retain_;
调用 RefCounts 的 increment 方法:
void increment(uint32_t inc = 1) { // 3. 原子地读出 InlineRefCountBits 对象(即一个 uint64_t)。 auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); RefCountBits newbits; do { newbits = oldbits; // 4. 调用 InlineRefCountBits 的 incrementStrongExtraRefCount 方法 // 对这个 uint64_t 进行一系列运算。 bool fast = newbits.incrementStrongExtraRefCount(inc); // 无 weak、unowned 引用时一般不会进入。 if (SWIFT_UNLIKELY(!fast)) { if (oldbits.isImmortal()) return; return incrementSlow(oldbits, inc); } // 5. 通过 CAS 将运算后的 uint64_t 设置回去。 } while (!refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_relaxed)); }
到这里就完成了一次 retain 操作。
SideTableRefCountBits
上面是不存在 weak、unowned 引用的情况,现在我们来看看增加一个 weak 引用会怎样。
- 调用 SIL 接口 swift::swift_weakAssign(暂时省略这块的逻辑,它属于引用者的逻辑,我们现在先分析被引用者)
- 调用 RefCounts<InlineRefCountBits>::formWeakReference 增加一个弱引用:
template <> HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference() { // 分配一个 Side Table。 auto side = allocateSideTable(true); if (side) // 增加一个弱引用。 return side->incrementWeak(); else return nullptr; }
重点来看一下 allocateSideTable 的实现:
template <> HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting) { auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); // 已有 Side Table 或正在析构就直接返回。 if (oldbits.hasSideTable()) { return oldbits.getSideTable(); } else if (failIfDeiniting && oldbits.getIsDeiniting()) { return nullptr; } // 分配 Side Table 对象。 HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject()); auto newbits = InlineRefCountBits(side); do { if (oldbits.hasSideTable()) { // 此时可能其他线程创建了 Side Table,删除该线程分配的,然后返回。 auto result = oldbits.getSideTable(); delete side; return result; } else if (failIfDeiniting && oldbits.getIsDeiniting()) { return nullptr; } // 用当前的 InlineRefCountBits 初始化 Side Table。 side->initRefCounts(oldbits); // 进行 CAS。 } while (! refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_release, std::memory_order_relaxed)); return side; }
还记得 HeapObject 里的 RefCounts 实际上是 InlineRefCountBits 的一个 wrapper 吗?上面构造完 Side Table 以后,对象中的 InlineRefCountBits 就不是原来的引用计数了,而是一个指向 Side Table 的指针,然而由于它们实际都是 uint64_t,因此需要一个方法来区分。区分的方法我们可以来看 InlineRefCountBits 的构造函数:
LLVM_ATTRIBUTE_ALWAYS_INLINE RefCountBitsT(HeapObjectSideTableEntry* side) : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits) | (BitsType(1) << Offsets::UseSlowRCShift) | (BitsType(1) << Offsets::SideTableMarkShift)) { assert(refcountIsInline); }
其实还是最常见的方法,把指针地址无用的位替换成标识位。
顺便,看一下 Side Table 的结构:
class HeapObjectSideTableEntry { // FIXME: does object need to be atomic? std::atomic<HeapObject*> object; SideTableRefCounts refCounts; public: HeapObjectSideTableEntry(HeapObject *newObject) : object(newObject), refCounts() { } // ... };
此时再增加引用计数会怎样呢?来看下之前的 RefCounts::increment 方法:
void increment(uint32_t inc = 1) { auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); RefCountBits newbits; do { newbits = oldbits; bool fast = newbits.incrementStrongExtraRefCount(inc); // ---> 这次进入这个分支。 if (SWIFT_UNLIKELY(!fast)) { if (oldbits.isImmortal()) return; return incrementSlow(oldbits, inc); } } while (!refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_relaxed)); } template <typename RefCountBits> void RefCounts<RefCountBits>::incrementSlow(RefCountBits oldbits, uint32_t n) { if (oldbits.isImmortal()) { return; } else if (oldbits.hasSideTable()) { auto side = oldbits.getSideTable(); // ---> 然后调用到这里。 side->incrementStrong(n); } else { swift::swift_abortRetainOverflow(); } } void HeapObjectSideTableEntry::incrementStrong(uint32_t inc) { // 最终到这里,refCounts 是一个 RefCounts<SideTableRefCountBits> 对象。 refCounts.increment(inc); }
到这里我们就需要引出 SideTableRefCountBits 了,它与前面的 InlineRefCountBits 很像,只不过又多了一个字段,看一下定义:
class SideTableRefCountBits : public RefCountBitsT<RefCountNotInline> { uint32_t weakBits; // ... };
小结一下
不知道上面的内容大家看晕了没有,反正我一开始分析的时候费了点时间。
上面我们讲了两种 RefCounts,一种是 inline 的,用在 HeapObject 中,它其实是一个 uint64_t,可以当引用计数也可以当 Side Table 的指针。
Side Table 是一种类名为 HeapObjectSideTableEntry 的结构,里面也有 RefCounts 成员,是内部是 SideTableRefCountBits,其实就是原来的 uint64_t 加上一个存储弱引用数的 uint32_t。
WeakReference
上面说的都是被引用的对象所涉及的逻辑,而引用者这边的逻辑就稍微简单一些了,主要就是通过 WeakReference 这个类来实现的,比较简单,我们简单过一下就行。
Swift 中的 weak 变量经过 silgen 之后都会变成 swift::swift_weakAssign 调用,然后派发给 WeakReference::nativeAssign:
void nativeAssign(HeapObject *newObject) { if (newObject) { assert(objectUsesNativeSwiftReferenceCounting(newObject) && "weak assign native with non-native new object"); } // 让被引用者构造 Side Table。 auto newSide = newObject ? newObject->refCounts.formWeakReference() : nullptr; auto newBits = WeakReferenceBits(newSide); // 喜闻乐见的 CAS。 auto oldBits = nativeValue.load(std::memory_order_relaxed); nativeValue.store(newBits, std::memory_order_relaxed); assert(oldBits.isNativeOrNull() && "weak assign native with non-native old object"); // 销毁原来对象的弱引用。 destroyOldNativeBits(oldBits); }
弱引用的访问就更简单了:
HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) { auto side = bits.getNativeOrNull(); return side ? side->tryRetain() : nullptr; }
到这里大家发现一个问题没有,被引用对象释放了为什么还能直接访问 Side Table?其实 Swift ABI 中 Side Table 的生命周期与对象是分离的,当强引用计数为 0 时,只有 HeapObject 被释放了。
只有所有的 weak 引用者都被释放了或相关变量被置 nil 后,Side Table 才能得以释放,相见:
void HeapObjectSideTableEntry::decrementWeak() { // FIXME: assertions // FIXME: optimize barriers bool cleanup = refCounts.decrementWeakShouldCleanUp(); if (!cleanup) return; // Weak ref count is now zero. Delete the side table entry. // FREED -> DEAD assert(refCounts.getUnownedCount() == 0); delete this; }
所以即便使用了弱引用,也不能保证相关内存全部被释放,因为只要 weak 变量不被显式置 nil,Side Table 就会存在。而 ABI 中也有可以提升的地方,那就是如果访问弱引用变量时发现被引用对象已经释放,就将自己的弱引用销毁掉,避免之后重复无意义的 CAS 操作。当然 ABI 不做这个优化,我们也可以在 Swift 代码里做。:)
总结
以上就是 Swift 弱引用机制实现方式的一个简单的分析,可见思路与 Objective-C runtime 还是很类似的,都采用与对象匹配的 Side Table 来维护引用计数。不同的地方就是 Objective-C 对象在内存布局中没有 Side Table 指针,而是通过一个全局的 StripedMap 来维护对象和 Side Table 之间的关系,效率没有 Swift 这么高。另外 Objective-C runtime 在对象释放时会将所有的 __weak 变量都 zero-out,而 Swift 并没有。
总的来说,Swift 的实现方式会稍微简单一些(虽然代码更复杂,Swift 团队追求更高的抽象)。第一次分析 Swift ABI,本文仅供参考,如果存在错误,欢迎大家勘正。感谢!
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。