从源码解析 Swift 弱引用

552 阅读5分钟

这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

各个社区有关 Objective-C weak 机制的实现分析文章有很多,然而 Swift weak的实现分析文章很少,本文就从源码层面分析一下 Swift 是如何实现 weak 机制的。

在之前文章中知道了Swift类的本质是 HeapObject 结构体指针。而HeapObject结构体中有两个成员变量:metadatarefCountsmetadata 是指向元数据对象的指针,里面存储着类的信息,比如属性信息,虚函数表等。refCounts 是指引用计数相关。要想搞明白弱引用,首先要搞明白引用计数

refCounts - 引用计数

在 Swift 源码(源码下载地址)中找到 HeapObject.h 文件,并在 HeapObject.h 文件中找到 refCounts 的具体定义(这是一个宏定义)

截屏2022-02-14 上午11.30.18.png

可以看到 refCounts 的类型为 InlineRefCounts,在 RefCount.h 文件中找到 InlineRefCounts 的定义:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

发现这是一个模版类:RefCountsRefCounts 接收一个泛型参数,我们来看一下 RefCounts 的结构:

截屏2022-02-14 上午11.36.31.png

RefCounts 是什么呢?RefCounts 其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数。那这个泛型参数 InlineRefCountBits 是什么?

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

看到这也是一个模版类,接下来看一下 RefCountBitsT 的结构:

截屏2022-02-14 上午11.42.25.png

RefCountBitsT 类中只有一个属性信息 bits,如图,bits 的类型为 BitsType,并且由 RefCountBitsInt 中的 Type 来定义,我们来看一下 RefCountBitsInt 的结构:

截屏2022-02-14 上午11.47.25.png

可以看到,Type 的类型是一个 uint64_t 的位域信息,在这个 uint64_t 的位域信息中存储着运行生命周期的相关引用计数。

跟踪到这里,我们仍然没搞明白是如何设置的,我们先来看⼀下,当我们创建⼀个实例对象的时候,当前的引⽤计数是多少?我们找到 HeapObject 的定义,在 HeapObject 的初始化方法中我们看到了 refCounts 的初始化赋值,如下:

截屏2022-02-14 上午11.49.22.png

我们看到 refCounts 传入一个 Initialized,接下来全局搜索 Initialized_t ,在RefCount.h 文件找到了 Initialized 为 Initialized_t 枚举的一个值。

截屏2022-02-14 上午11.50.49.png

接下来看到了 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {} ,通过注释得知,一个新的对象的引用计数为 1,并且我们可以看到 refCounts 函数的参数传的不就是前面找到的 RefCountBitsT 么。我们回到 RefCountBitsT 类中找到它的初始化方法,如下:

截屏2022-02-14 下午1.39.14.png

如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount 传 0,unownedCount 传 1。那么 Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1,这三个的值又是怎么来的呢。

我们来看下 RefCountBitOffsets 在 64 位的实现:

截屏2022-02-14 下午1.40.50.png

由此可知,PureSwiftDeallocShift = 0,毫无疑问,那么 StrongExtraRefCountShift 和 UnownedRefCountShift 呢,我们发现它们都调用同一个方法 shiftAfterField,找到它的实现,如下:

# define shiftAfterField(name) (name##Shift + name##BitCount)

这是一个宏定义实现,传一个参数 name,内部做相加的操作。注意看,## 运算符在 C++ 中是用来粘合的,比如一个名字,或者一个值,所以:

UnownedRefCountShift 传的是 PureSwiftDealloc,那么内部的实现为:

PureSwiftDeallocShift + PureSwiftDeallocBitCount)-> (0 + 1= 1

这样就知道了这三个值的由来了,我们开始计算 RefCountBitsT 的初始化方法调用 bits 的值:

0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;

我们最终算出来的结果为 3,所以在创建一个对象并第一次引用对象的时候,refCounts = 3。来验证一下

class Person {
   var name = "Candy"
}

var person = Person()

// 打印当前 person 实例的内存指针地址
print(Unmanaged.passUnretained(person as AnyObject).toOpaque())

print("end")
0x0000000108cc4e30

(lldb) x/8g 0x0000000108cc4e30
0x108cc4e30: 0x0000000100008150 0x0000000000000003
0x108cc4e40: 0x00000079646e6143 0xe500000000000000
0x108cc4e50: 0x00000009a0080001 0x00007fff8160ab10
0x108cc4e60: 0x0000000000000000 0x00007fff806f7390

弱引用

当前对象的生命周期不由其他对象引用限制,它本该什么时候销毁就什么时候被销毁。即使它的引用没断,但是当它的生存周期到了时就会被销毁。

接下来我们看一下用 weak 修饰的本质是什么,代码如下:

class Student {
    var name = "Candy"
    var age  = 10
}

weak var student = Student()

打个断点,查看下对应的汇编代码:

截屏2022-02-17 下午6.16.57.png

通过汇编代码分析,用 weak 修饰之后,student 变成了一个可选项,并且之后会调用一个 swift_weakInit 函数,紧接着又调用 swift_release 函数,将 student 的实例释放掉了。

下面来在源码中查看下 swift_weakInit 的实现,在 HeapObject.cpp 文件中,swift_weakInit 的实现如下:

截屏2022-02-17 下午6.21.27.png

通过源码,看到用 weak 修饰之后,在内部会生成 WeakReference 类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。nativeInit 的实现如下:

截屏2022-02-17 下午6.22.40.png

在这里,它调用了 refCounts.formWeakReference 函数,形成了弱引用,看一下 formWeakReference 的实现:

截屏2022-02-17 下午6.24.01.png

可以发现,它本质上就是创建了一个散列表,我们接下来看一下散列表的创建:

截屏2022-02-17 下午6.25.32.png

接下来我们来看看这个散列表 - HeapObjectSideTableEntry

截屏2022-02-17 下午7.18.20.png

其实在这里,官方已经告诉我们强引用和弱引用内部实现的区别了,弱引用比强引用多了加上 weak RC 。接下来看一下 HeapObjectSideTableEntry 的结构。

截屏2022-02-17 下午7.20.29.png

可以看到,HeapObjectSideTableEntry 中存着对象的指针refCounts,而 refCounts 的类型为 SideTableRefCounts,那这个 SideTableRefCounts 又是什么呢?其实 SideTableRefCounts 就是继承自我们前面看到的 RefCountBitsT 的模版类。

typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

截屏2022-02-17 下午7.24.04.png

并且,它还多了一个 weakBits。到这里,当我们用 weak 修饰之后,这个散列表就会存储对象的指针和引用计数信息相关的东西。我们来验证一下是否存储了对象的指针,代码如下:

class Student {
    var name = "Candy"
    var age  = 10
}

var student = Student()
print(Unmanaged.passUnretained(student **as** AnyObject).toOpaque())
weak var student1 = student
print("end")

截屏2022-02-17 下午7.27.05.png

用 weak 修饰后,refCounts 从原来的 0x0000000000000003 变成了 0xc000000021164cee

截屏2022-02-17 下午7.29.40.png

在用 weak 修饰之后变成的 0xc000000021164cee ,在 62 位 和 63 位会变成 1,此时将 1 还原成 0,还原之后的内存地址变成了 0x21164CEE

截屏2022-02-17 下午7.29.21.png

我们接下来看一下这个散列表的生成 InlineRefCountBits

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

可以看到,InlineRefCountBits 也是一个 RefCountBitsT 的模版类。它对应的初始化方法如下:

RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits) 
       | (BitsType(1) << Offsets::UseSlowRCShift) 
       | (BitsType(1) << Offsets::SideTableMarkShift))
{
    assert(refcountIsInline);
}

SideTableUnusedLowBits = 3,所以,在这个过程中,传进来的散列表往右移了 3 位,下面的两个是 62 位和 63 位标记成 1。所以,我们回到计算器,它既然是右移 3 位,那么我左移 3 位把它还原。

0x21164CEE 左移 3 位的结果等于 0x108B26770。接下来在 Xcode 中我们格式化输出 0x108B26770

截屏2022-02-17 下午7.34.35.png

如图,验证的结果与分析一致。所以,当用 weak 修饰的时候,本质上是创建了一个散列表。

无主引用

在Swift中可以通过 unowned 定义无主引用,unowned 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的 unsafe_unretained)。

区别在于 weak 是弱引用,被引用对象释放时候,引用值会自动置为 nil;unowned 是无主引用,需保证被引用对象的生命周期大于等于当前引用者,如果被引用对象提前释放,会导致崩溃。相比 unowned,weak 更加安全,但效率会低一些,类似于可选类型,使用时还需要考虑解包操作,我们还是要根据对象之间的关系来选择合适的处理。

总结

以上就是 Swift 弱引用机制实现方式的一个简单的分析,可见思路与 Objective-C runtime 还是很类似的,都采用与对象匹配的 Side Table 来维护引用计数。不同的地方就是 Objective-C 对象在内存布局中没有 Side Table 指针,而是通过一个全局的 StripedMap 来维护对象和 Side Table 之间的关系,效率没有 Swift 这么高。

参考文章: juejin.cn/post/705421…