Swift源码解析之弱引用

序言:

各个社区有关 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 引用会怎样。

  1. 调用 SIL 接口 swift::swift_weakAssign(暂时省略这块的逻辑,它属于引用者的逻辑,我们现在先分析被引用者)
  2. 调用 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,本文仅供参考,如果存在错误,欢迎大家勘正。感谢!

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Swift里的值类型与引用类型区别和使用

    Swift里面的类型分为两种: ●值类型(Value Types):每个实例都保留了一分独有的数据拷贝,一般以结构体 (struct).枚举(enum) 或者元组(tuple)的形式出现. ●引用类型(Reference Type):每个实例共享同一份数据来源,一般以类(class)的形式出现. 在这篇博文里面,我们会介绍两种类型各自的优点,以及应该怎么选择使用. 值类型与引用类型的区别 值类型和引用类型最基本的分别在复制之后的结果.当一个值类型被复制的时候,相当于创造了一个完全独立的实例,这个

  • Swift 4.0中如何引用3.0的第三方库

    前言 第三方库是所有工程师在开发中都会经常用到的,熟练的掌握多个第三方库能把我们的生产力提升一大截,Swift 已经发布了 4.0 版本,在 Xcode9 中新建项目后,默认是使用 4.0 语法的.项目中的引用的第三方库,虽然有很多已经发不了 4.0 版本,但是还是有一些未及时更新的,那在作者未更新之前我们是否有更好的办法来使用这些第三方库呢? 答案当然是 肯定 的, Xcode9 中是同时支持 3.2 和 4.0 语法的. 具体的设置可以看下图. 那么下面就说说如何设置同时支持 3.2 和 4

  • Swift中如何避免循环引用的方法

    内存管理中经常会遇到的一个问题便是循环引用.首先,我们来了解一下iOS是如何进行内存管理的. 和OC一样,swift也是使用自动引用计数ARC(Auto Reference Counteting)来自动管理内存的,所以我们不需要过多考虑内存管理.当某个类实例不需要用到的时候,ARC会自动释放其占用的内存. ARC ARC(Automatic Reference Counting) 是苹果的自动内存管理机制.正如其名:自动引用计数,根据引用计数来决定内存块是否应该被释放. 当一个对象被创建的时候,

  • Swift源码解析之弱引用

    序言: 各个社区有关 Objective-C weak 机制的实现分析文章有很多,然而 Swift 发布这么长时间以来,有关 ABI 的分析文章一直非常少,似乎也是很多 iOS 开发者未涉及的领域- 本文就从源码层面分析一下 Swift 是如何实现 weak 机制的. 下面话不多说了,来一起看看详细的介绍吧 准备工作 由于 Swift 源码量较大,强烈建议大家把 repo clone 下来,结合源码一起来看这篇文章. $ git clone https://github.com/apple/sw

  • Spring循环引用失败问题源码解析

    目录 前言: 例子 启动容器 加载circulationa AbstractBeanFactory 最终调用BeanDefinitionValueResolver circulationb加载分析 前言: 之前我们有分析过Spring是怎么解决循环引用的问题,主要思路就是三级缓存: Spring在加载beanA的时候会先调用默认的空构造函数(在没有指定构造函数实例化的前提下)得到一个空的实例引用对象,这个时候没有设置任何值,但是Spring会用缓存把它给提前暴露出来,让其他依赖beanA的bea

  • C++11中的智能指针shared_ptr、weak_ptr源码解析

    目录 1.前言 2.源码准备 3.智能指针概念 4.源码解析 4.1.shared_ptr解析 4.1.1.shared_ptr 4.1.2.__shared_ptr 4.1.3.__shared_count 4.1.4._Sp_counted_base 4.1.5._Sp_counted_ptr 4.1.6.shared_ptr总结 4.2.weak_ptr解析 4.2.1.weak_ptr 4.2.2.__weak_ptr 4.2.3.__weak_count 4.2.4.回过头看weak_

  • Java 线程池ThreadPoolExecutor源码解析

    目录 引导语 1.整体架构图 1.1.类结构 1.2.类注释 1.3.ThreadPoolExecutor重要属性 2.线程池的任务提交 3.线程执行完任务之后都在干啥 4.总结 引导语 线程池我们在工作中经常会用到.在请求量大时,使用线程池,可以充分利用机器资源,增加请求的处理速度,本章节我们就和大家一起来学习线程池. 本章的顺序,先说源码,弄懂原理,接着看一看面试题,最后看看实际工作中是如何运用线程池的. 1.整体架构图 我们画了线程池的整体图,如下: 本小节主要就按照这个图来进行 Thre

  • JetCache 缓存框架的使用及源码解析(推荐)

    目录 一.简介 为什么使用缓存? 使用场景 使用规范 二.如何使用 引入maven依赖 添加配置 配置说明 注解说明 @EnableCreateCacheAnnotation @EnableMethodCache @CacheInvalidate @CacheUpdate @CacheRefresh @CachePenetrationProtect @CreateCache 三.源码解析 项目的各个子模块 常用注解与变量 缓存API Cache接口 AbstractCache抽象类 Abstra

  • 内存泄漏检测工具LeakCanary源码解析

    目录 前言 使用 源码解析 LeakCanary自动初始化 如何关闭自动初始化 LeakCanary初始化做了什么 ActivityWatcher FragmentAndViewModelWatcher RootViewWatcher ServiceWatcher Leakcanary对象泄漏检查 总结 前言 LeakCanary是一个简单方便的内存泄漏检测工具,它是由大名鼎鼎的Square公司出品并开源的出来的.目前大部分APP在开发阶段都会接入此工具用来检测内存泄漏问题.它让我们开发者可以在

  • Java源码解析之object类

    在源码的阅读过程中,可以了解别人实现某个功能的涉及思路,看看他们是怎么想,怎么做的.接下来,我们看看这篇Java源码解析之object的详细内容. Java基类Object java.lang.Object,Java所有类的父类,在你编写一个类的时候,若无指定父类(没有显式extends一个父类)编译器(一般编译器完成该步骤)会默认的添加Object为该类的父类(可以将该类反编译看其字节码,不过貌似Java7自带的反编译javap现在看不到了). 再说的详细点:假如类A,没有显式继承其他类,编译

  • java.lang.Void类源码解析

    在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerException if the parent argument is {@code null} * @throws SecurityException if the current thread cannot create a * thread in the specified thread group. */ private static Void checkParentAcc

  • java TreeMap源码解析详解

    java TreeMap源码解析详解 在介绍TreeMap之前,我们来了解一种数据结构:排序二叉树.相信学过数据结构的同学知道,这种结构的数据存储形式在查找的时候效率非常高. 如图所示,这种数据结构是以二叉树为基础的,所有的左孩子的value值都是小于根结点的value值的,所有右孩子的value值都是大于根结点的.这样做的好处在于:如果需要按照键值查找数据元素,只要比较当前结点的value值即可(小于当前结点value值的,往左走,否则往右走),这种方式,每次可以减少一半的操作,所以效率比较高

  • Spring SpringMVC在启动完成后执行方法源码解析

    关键字:spring容器加载完毕做一件事情(利用ContextRefreshedEvent事件) 应用场景:很多时候我们想要在某个类加载完毕时干某件事情,但是使用了spring管理对象,我们这个类引用了其他类(可能是更复杂的关联),所以当我们去使用这个类做事情时发现包空指针错误,这是因为我们这个类有可能已经初始化完成,但是引用的其他类不一定初始化完成,所以发生了空指针错误,解决方案如下: 1.写一个类继承spring的ApplicationListener监听,并监控ContextRefresh

随机推荐