「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战。 」
各个社区有关 Objective-C weak 机制的实现分析文章有很多,然而 Swift weak的实现分析文章很少,本文就从源码层面分析一下 Swift 是如何实现 weak 机制的。
在之前文章中知道了Swift类的本质是 HeapObject 结构体指针。而HeapObject结构体中有两个成员变量:metadata 和 refCounts,metadata 是指向元数据对象的指针,里面存储着类的信息,比如属性信息,虚函数表等。refCounts 是指引用计数相关。要想搞明白弱引用,首先要搞明白引用计数。
refCounts - 引用计数
在 Swift 源码(源码下载地址)中找到 HeapObject.h 文件,并在 HeapObject.h 文件中找到 refCounts 的具体定义(这是一个宏定义)
可以看到 refCounts 的类型为 InlineRefCounts,在 RefCount.h 文件中找到 InlineRefCounts 的定义:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
发现这是一个模版类:RefCounts,RefCounts 接收一个泛型参数,我们来看一下 RefCounts 的结构:
RefCounts 是什么呢?RefCounts 其实是对引用计数的一个包装,而引用计数的具体类型取决于外部传进来的泛型参数。那这个泛型参数 InlineRefCountBits 是什么?
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
看到这也是一个模版类,接下来看一下 RefCountBitsT 的结构:
RefCountBitsT 类中只有一个属性信息 bits,如图,bits 的类型为 BitsType,并且由 RefCountBitsInt 中的 Type 来定义,我们来看一下 RefCountBitsInt 的结构:
可以看到,Type 的类型是一个 uint64_t 的位域信息,在这个 uint64_t 的位域信息中存储着运行生命周期的相关引用计数。
跟踪到这里,我们仍然没搞明白是如何设置的,我们先来看⼀下,当我们创建⼀个实例对象的时候,当前的引⽤计数是多少?我们找到 HeapObject 的定义,在 HeapObject 的初始化方法中我们看到了 refCounts 的初始化赋值,如下:
我们看到 refCounts 传入一个 Initialized,接下来全局搜索 Initialized_t ,在RefCount.h 文件找到了 Initialized 为 Initialized_t 枚举的一个值。
接下来看到了 constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {} ,通过注释得知,一个新的对象的引用计数为 1,并且我们可以看到 refCounts 函数的参数传的不就是前面找到的 RefCountBitsT 么。我们回到 RefCountBitsT 类中找到它的初始化方法,如下:
如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount 传 0,unownedCount 传 1。那么 Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1,这三个的值又是怎么来的呢。
我们来看下 RefCountBitOffsets 在 64 位的实现:
由此可知,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()
打个断点,查看下对应的汇编代码:
通过汇编代码分析,用 weak 修饰之后,student 变成了一个可选项,并且之后会调用一个 swift_weakInit 函数,紧接着又调用 swift_release 函数,将 student 的实例释放掉了。
下面来在源码中查看下 swift_weakInit 的实现,在 HeapObject.cpp 文件中,swift_weakInit 的实现如下:
通过源码,看到用 weak 修饰之后,在内部会生成 WeakReference 类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。nativeInit 的实现如下:
在这里,它调用了 refCounts.formWeakReference 函数,形成了弱引用,看一下 formWeakReference 的实现:
可以发现,它本质上就是创建了一个散列表,我们接下来看一下散列表的创建:
接下来我们来看看这个散列表 - HeapObjectSideTableEntry
其实在这里,官方已经告诉我们强引用和弱引用内部实现的区别了,弱引用比强引用多了加上 weak RC 。接下来看一下 HeapObjectSideTableEntry 的结构。
可以看到,HeapObjectSideTableEntry 中存着对象的指针和 refCounts,而 refCounts 的类型为 SideTableRefCounts,那这个 SideTableRefCounts 又是什么呢?其实 SideTableRefCounts 就是继承自我们前面看到的 RefCountBitsT 的模版类。
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
并且,它还多了一个 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")
用 weak 修饰后,refCounts 从原来的 0x0000000000000003 变成了 0xc000000021164cee
在用 weak 修饰之后变成的 0xc000000021164cee ,在 62 位 和 63 位会变成 1,此时将 1 还原成 0,还原之后的内存地址变成了 0x21164CEE。
我们接下来看一下这个散列表的生成 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,
如图,验证的结果与分析一致。所以,当用 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…