序言: 各个社区有关 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, 本文仅供参考, 如果存在错误, 欢迎大家勘正. 感谢!
来源: https://juejin.im/post/5c7b835af265da2d881b4457