前言
在Swift Runtime-初探一文里,我们初步研究了对象的内存结构.有metadata
及Refcount
.接下来我们要研究Refcount
,.为什么不是metadata
呢?因为Refcount
相对于metadata
比较简单,让我们的研究由浅入深.
正文
首先在HeapObject结构体里定义这么一个属性:RefCounts<InlineRefCountBits> refCounts;
表示引用计数.
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
......
};
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS
为宏定义,搜索源码可以得到:InlineRefCounts refCounts
.而InlineRefCounts
又是RefCounts<InlineRefCountBits>
类型,
所以可以最终替换为:
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;
RefCounts<InlineRefCountBits> refCounts;
......
};
而RefCounts<InlineRefCountBits>
又是什么呢?我们继续搜索RefCounts
的定义:
template <typename RefCountBits> //RefCountBits代表一种类型
class RefCounts {
std::atomic<RefCountBits> refCounts; //可以通过RefCounts<类型>来传入类型
......略
}
查看源码可知,RefCounts<InlineRefCountBits>即声明一个RefCounts
类,其属性refCount
为InlineRefCountBits
类型.接下来需要查找InlineRefCountBits
的定义:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits
RefCountBitsT<RefCountIsInline>
又是接受模板参数的定义,查阅RefCountBitsT
的定义,发现RefCountBitsT
就是承载引用计数的最终数据结构了.那为什么以上一系列定义都采用了模板参数,其实阅读源码的时候就发现,一共有两种RefCounts,只是通过RefCounts<T>
,RefCountsBits<T>
共享了部分对引用数据操作的实现.缺点是代码阅读起来非常难受. 优点是可以减少重复代码.
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
strong RC + unowned RC + flags
或者
HeapObjectSideTableEntry*
}
}
}
HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}
InlineRefCounts
让我们先从相对简单的InlineRefCountBits
,即RefCountBitsT<RefCountIsInline>
入手.
仔细阅读源码,RefCountBitsT代表的是64位长的一段位域,通过field value = (bits & mask) >> shift
的方式来得到想要的值.举一个例子,比如针对这个位域,源码里定义了
很多mask,其中一个PureSwiftDeallocMask
定义如下:
static const uint64_t PureSwiftDeallocMask = maskForField(PureSwiftDealloc);
maskForField
是一个宏定义# define maskForField(name) (((uint64_t(1)<<name##BitCount)-1) << name##Shift)
,那么上面的代码就可以转换成:
((uint64_t(1)<<PureSwiftDeallocBitCount)-1) << PureSwiftDeallocShift
再用相应的值替换:
( (uint64_t(1) << 1) -1 ) << 0
// 用白话文对这段代码解释一下
//首先将uint64_t(1)用二进制表示就是:
//0000000000000000000000000000000000000000000000000000000000000001, 左移1位之后:变成
//0000000000000000000000000000000000000000000000000000000000000010,再减1,又变成
//0000000000000000000000000000000000000000000000000000000000000001,这个结果就是PureSwiftDeallocMask
我再举一个例子,看是如何计算UnownedRefCountMask
UnownedRefCountMask = maskForField(UnownedRefCount);
UnownedRefCountMask = (((uint64_t(1)<<UnownedRefCountBitCount)-1) << UnownedRefCountShift) //宏替换后
UnownedRefCountMask = (((uint64_t(1)<<31)-1) << 1)
//0000000000000000000000000000000000000000000000000000000000000001, 左移31位之后:
//0000000000000000000000000000000010000000000000000000000000000000,再减1:
//0000000000000000000000000000000001111111111111111111111111111111,再往左移动1位:
//0000000000000000000000000000000011111111111111111111111111111110,这就是UnownedRefCountMask的值了.
最后计算出所有mask可以得到这样的结论,在InlineRefCountBits类型下,引用计数是这样的位域:
接下来让我打开之前的项目把main.swift修改成如下:
class Person {
}
var a = Person()
var b = a
var c = a
print("Hello, World!")
断点停住之后我们打印相应数据,但是这次使用二进制格式输出.这下对于第二段内容就一目了然了:
输出的内容都变成了二进制,我们可以通过
(bits & mask) >> shift
来计算出对应位域的值.而持有这段位域的RefCountBitsT
正是通过这种方式来提供非常多便利的函数来控制相应的值.
SideTableRefCounts
看完InlineRefCounts
,再来看一下SideTableRefCounts
,那什么时候refcounts
会是SideTableRefCounts
类型的呢?其实swift源码里已经说了,其中一个比较常见的原因就是有弱引用的形成.上文已经提到SideTableRefCounts
的结构:
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
HeapObjectSideTableEntry*
}
}
}
HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}
当变成SideTableRefCounts
类型的时候,refcounts不再是位域,而是一个指针,指向HeapObjectSideTableEntry
,HeapObjectSideTableEntry
里面有类似之前InlineRefCountBits
的SideTableRefCountBits
,SideTableRefCountBits
比InlineRefCountBits
多了弱引用计数.还有一个pointer
属性,指向sidetable归属的对象.其实HeapObjectSideTableEntry
是一个独立的内存区域,对象指向这个sidetable
,sidetable
也会指向对象.因为side table
已知很小,这样就不会有弱引用指向大对象导致的内存浪费,所以问题自然就消失了。这也指明了线程安全问题的简单解决方案:不用提前清零弱引用。既然已知 side table
比较小,指向它的弱引用可以持续保留,直到这些引用自身被覆盖或销毁。
以上讨论的是被弱引用对象的处理,那么弱引用的对象是如何处理的呢?我们可以通过指令swiftc -emit-ir main.swift
编译swift文件生成IR文件,然后阅读IR文件发现:对于弱引用变量的赋值操作,编译器都会帮你加上对应的操作函数:
%24 = call %swift.weak* bitcast (%swift.weak* (%swift.weak*, %swift.refcounted*)* @swift_weakAssign to %swift.weak* (%swift.weak*, %T4main6personC*)*)(%swift.weak* returned @"$s4main9bbbbbbbbbAA6personCSgvp", %T4main6personC* %23) #3
%13 = call %swift.weak* bitcast (%swift.weak* (%swift.weak*, %swift.refcounted*)* @swift_weakInit to %swift.weak* (%swift.weak*, %T4main6personC*)*)(%swift.weak* returned @"$s4main9bbbbbbbbbAA6personCSgvp", %T4main6personC* %12) #3
//如上就出现了两个函数调用:swift_weakAssign和swift_weakInit,将weak变量和被引用的对象作为参数.
阅读源码发现swift_weakAssign
和swift_weakInit
一类的函数由HeapObject
提供支持,一路跟踪发现,weak
变量并没有持有被引用对象的指针,而是持有了被引用对象的sidetable
,通过访问持有的sidetable
的pointer
指针间接访问引用的对象.这跟Objective-C的weak实现是有区别的,Objective-C的sidetable
是存储在一个全局数组里,而Swift则是每个对象都有自己的sidetable
.
再来说下swiftsidetable
具体操作逻辑:
var a = Object()
weak var b = a //因为a被b弱引用所以生成一个sidetable,同时弱引用计数加一,b也获得这个sidetable
var c = Object()
b = c //b被重新赋值,因为c被弱引用所以生成c的sidetable,同时弱引用计数加一,再被b持有. 因为b有之前a的sidetable,所以对a的sidetable弱引用计数做减一操作,又因为减一之后弱引用计数正好到零,所以a的sidetable会自动销毁.
对象强引用计数归零时,并不会处理自己的sidetable
,所以weak
变量不会自动置为nil.此时如果weak
变量去访问指向的对象,因为sidetable
被标记为对象已经销毁,所以代码会返回nil
.这里与Objective-C的自动将weak
变量的值变成nil有很大区别.
参考
https://juejin.im/post/5c7b835af265da2d881b4457
https://alvinzhu.me/2017/11/15/ios-weak-references.html