引用计数的变化
学过swift的基础语法,我们知道类对象有3种引用方式:强引用(strong
)、弱引用(weak
)、无主引用(unowned
)。
我们从上篇文章探索Swift中Class的大致结构,了解了引用计数存放的位置。我们现在写一个小Demo,来看下引用计数的变化:
class Teacher {
var age: Int = 18
var name: String = "Tom"
}
var person = Teacher()
var person1 = person
unowned var person2 = person
weak var person3 = person
print("---end---")
我们可以在person1
的位置打上断点,每走一步,查看下引用计数的变化,具体的结果如下截图
person
初始化的引用计数是0x0000000200000003
person1
强引用后的引用计数是0x0000000400000003
person2
无主引用的引用计数是0x0000000400000005
person3
弱引用的引用计数是0xc000000020b8488e
从1到2到3,可能还有点规律可循,但是第四步就毫无头绪了,不用怕,我们去看分析源码。
我们先查下汇编,把相关引用计数调用的方法先记下,看源码的时候能找到入口
InlineRefCounts
我们先简单探索下InlineRefCounts
,看看到底是什么?
回到我们源码的HeapObject.cpp
文件,上一篇文章我们分析到这
点进去,我们可以看到:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
看到InlineRefCounts
是RefCounts
的别名,我们点进RefCounts
看下:
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
....
}
我们发现RefCounts
是一个抽象出来的模板类,所以得看传进来的InlineRefCountBits
,顺便说下SideTableRefCountBits
会在弱引用中会用到。我们在点开InlineRefCountBits
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
又是一个别名,和上面操作一样,RefCountBitsT
是一个模版类,但RefCountIsInline
只是一个枚举,所以我们深入看下RefCountBitsT
// Basic encoding of refcount and flag data into the object's header.
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;
...
我们发现了一个属性bits
,后面省略的方法里基本上都是关于bits
的操作,所以我们看下BitsType
,BitsType
又是RefCountBitsInt
的别名,点击RefCountBitsInt
的时候,我们发现有多个选项,我们看下这些选项上门面的说明:
Raw storage of refcount bits, depending on pointer size and inlinedness. 32-bit inline refcount is 32-bits. All others are 64-bits.
我们的是64位哈,所以选这段:
// 64-bit inline
// 64-bit out of line
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
typedef uint64_t Type;
typedef int64_t SignedType;
};
看了这么多,InlineRefCounts
目前就是一个uint64_t
,被RefCountBitsT
操作着。
初始化的引用计数
其实这么看源码有点累哈,而且看的也不一定对,全靠编辑器的跳转,直接运行源码吧,方便快捷有保证(= =)。
我们在swift_allocObject
中打上断点,继续从上次的分析在往下走
上次分析了swift_slowAlloc
方法,并没有发现有关于引用计数的操作,我们走进HeapObject()
去看下
我们可以看到,最终跳到了RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
这个函数,bits
就是引用计数,BitsType
就是uint64_t
的意思,而strongExtraCount
和unownedCount
也能从左侧看到是0和1,那么最后我们只要搞清了右侧红框里的值,便能得到类在初始化的时候,引用计数是如何初始化的。
我们点击Offsets
后看到:
typedef RefCountBitOffsets<sizeof(BitsType)>
Offsets;
在点开别名RefCountBitOffsets
,我们可以看到:
template <>
struct RefCountBitOffsets<8> {
static const size_t PureSwiftDeallocShift = 0;
static const size_t PureSwiftDeallocBitCount = 1;
static const uint64_t PureSwiftDeallocMask = maskForField(PureSwiftDealloc);
static const size_t UnownedRefCountShift = shiftAfterField(PureSwiftDealloc);
static const size_t UnownedRefCountBitCount = 31;
static const uint64_t UnownedRefCountMask = maskForField(UnownedRefCount);
static const size_t IsImmortalShift = 0; // overlaps PureSwiftDealloc and UnownedRefCount
static const size_t IsImmortalBitCount = 32;
static const uint64_t IsImmortalMask = maskForField(IsImmortal);
static const size_t IsDeinitingShift = shiftAfterField(UnownedRefCount);
static const size_t IsDeinitingBitCount = 1;
static const uint64_t IsDeinitingMask = maskForField(IsDeiniting);
static const size_t StrongExtraRefCountShift = shiftAfterField(IsDeiniting);
static const size_t StrongExtraRefCountBitCount = 30;
static const uint64_t StrongExtraRefCountMask = maskForField(StrongExtraRefCount);
static const size_t UseSlowRCShift = shiftAfterField(StrongExtraRefCount);
static const size_t UseSlowRCBitCount = 1;
static const uint64_t UseSlowRCMask = maskForField(UseSlowRC);
static const size_t SideTableShift = 0;
static const size_t SideTableBitCount = 62;
static const uint64_t SideTableMask = maskForField(SideTable);
static const size_t SideTableUnusedLowBits = 3;
static const size_t SideTableMarkShift = SideTableBitCount;
static const size_t SideTableMarkBitCount = 1;
static const uint64_t SideTableMarkMask = maskForField(SideTableMark);
};
这里已经能看到所有的偏移值了,我们整理成一个表格:
名字 | 范围(起始位置,长度 ) | 作用 |
---|---|---|
PureSwiftDeallocMask | (0, 1) | 对象是否需要调用ObjC运行时来解除分配 |
UnownedRefCountMask | (1, 31) | 无主引用计数 |
IsImmortalMask | (0, 32) | 所有bit设置后,对象不会释放或具有refcount |
IsDeinitingMask | (32, 1) | 是否在进行反初始化 |
StrongExtraRefCountMask | (33, 30) | 强引用计数 |
UseSlowRCMask | (63, 1) | 使用慢RC |
SideTableMask | (0, 62) | 存放SideTable地址 |
SideTableUnusedLowBits | SideTable没有用到的低字节位数 | |
SideTableMarkMask | (62, 1) | 是否存放是SideTable |
我们可以画一图看下
分析完位置后,我们回头看下代码,发现UnownedRefCountMask
值为1,StrongExtraRefCountMask
为0,PureSwiftDeallocMask
默认值为1,所以位移下来的二进制结果为11
,转成16进制就是0x3
所以,从初始化源码中看,初始化完后,对象的引用计数为0x3
,我们继续跑断点,到最后return的时候,右侧的value值为3,也验证了正确性。这边我们也记一下metadata
值,后面会有比较。
swift_retain
我们把上面的结果和Xcode打印台结果0x0000000200000003
比对下,差了0x0000000200000000
,我们放到计算器里看下:
正好第33位为1,也就是强引用计数加1,那我们是不是可以猜测alloc完后,系统会调用swift_retain
方法,我们去swift_retain
打上断点(引用计数的方法都还在HeapObject.cpp
文件里),发现多次调用swift_retain
,这时候需要比对metadata
值是否一样:
我们继续运行,终于等到了我们的对象,然后断点一路往下跑
我们最后走到了incrementStrongExtraRefCount
,我们可以看到里面1位移到了强引用的位置,然后加给了引用计数。
函数返回后,我们看下,老值是3
,新值是8589934595
我们把新值放到计算器里看下:
转成16进制就是0x0000000200000003
,bit位上也能对应上,nice。
整个swift_retain
走完后,refCounts果然变成了新值,然后返回了出去
swift_unownedRetain
我们按照上面流程在来一遍,对比一下值,暂时还没变,然后断点一步步走
这里先取出了老的值1,然后和新加的1相加得到2后,赋值给了UnownedRefCount
整个函数返回出去的时候,引用计数为8589934597
,转成16进制是0x200000005
我们可以看到无主引用的位置加了一,二进制从1
变成了10
。
swift_weakInit
接下来啃最难的弱引用,还是老样子,在swift_weakInit
打断点
我们可以看到,用weak的时候,会创建一张SideTable
,我们详细看一下allocateSideTable
的实现:
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
//获取当前的引用计数存给oldbits
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// oldbits中存放的可能已经是生成好的SideTable地址,所以判断一下,如果能取到SideTable,那么直接返回出去,因为已经创建过了
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
return oldbits.getSideTable();
}
//如果正在销毁中,那么直接返回空指针
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
// Preflight passed. Allocate a side table.
// 生成了SideTable存给变量side
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
// 这个比较关键,这个方法把变量side的地址做了一定的操作,形成新的newbits,这个方法下面会详细讲
auto newbits = InlineRefCountBits(side);
do {
//这里又判断oldbits里存的是否是SideTable,是的话,返回老的SideTable,删除刚才我们创建的SideTable,这个好像和开头重叠了,猜测可能是多线程的原因吧。
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
// 把当前的引用计数存入SideTable中
side->initRefCounts(oldbits);
//CAS
} while (! refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}
LLVM_ATTRIBUTE_ALWAYS_INLINE
RefCountBitsT(HeapObjectSideTableEntry* side)
//把SideTable的右移3位
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
//把第64位bit置为1
| (BitsType(1) << Offsets::UseSlowRCShift)
//把第63位bit置为1
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
断点跑到最后,看下新老的对比
我们重新回到刚创建完SideTable那边,继续往下走会来到side->incrementWeak()
,断点继续
到此,整个SideTable
操作完了,回到函数最外层,继续往下走
把操作过的SideTable的地址值赋值给引用计数 等断点在出来的时候,我们发现引用计数的值已经改了(很尴尬,变之前的值没截图哈。。)
这样,我们整个weak的操作流程已经完成了,我们总结下
当使用弱引用的时候,我们会查看当前对象的SideTable
是否已经创建了,如果创建了,SideTable
中弱引用计数加一,如果没有创建,那么先创建,把当前对象的引用计数存在SideTable
中,在把弱引用计数加一。操作完后,我们把SideTable
处理过的地址赋给当前对象的引用计数。
换句话说,一旦我们使用了weak修复词,那么对象引用计数的内存里存放的不在是强引用和无主引用的个数,而是对应SideTable
的地址,真正的强引用和无主引用的个数存在了SideTable
中。
HeapObjectSideTableEntry
我们刚才从代码可以看到SideTable
用的是HeapObjectSideTableEntry
,这样可以看下SideTable
的大概结构
class HeapObjectSideTableEntry {
// FIXME: does object need to be atomic?
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
...
}
我们可以看到里面有个HeapObject
的指针,然后还有个SideTableRefCounts
,我们点击SideTableRefCounts
,又会来到这边
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
这次我们看的是SideTableRefCountBits
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
uint32_t weakBits;
...
}
SideTableRefCountBits
继承与RefCountBitsT
,所以也会有一个bits
属性,存放的就是对应对象的强引用和无主引用的个数,这个weakBits
存放的就是弱引用的个数,我们等会到项目里验证一下。
验证结论
先总结下结论:
- 初始化(init):在第1、2位的bit上置为1,相当于初始化完
0x3
- 无主引用(
unowned
):每次使用,在第2位的bit位上加1,相当于每次加0x2
- 强引用(
strong
):每次使用,在第33位的bit位上加1,相当于每次加0x200000000
- 弱引用(
weak
):每次使用,会生成一张SideTable
,然后把SideTable
的地址右移3位,将63、64位的bit置为1,最后存入引用计数,因为最高位的两个都是1,所以显示成16进制的时候,最高位大概率位c
。
再来个简单的demo
class Teacher {
}
var person = Teacher()
unowned var person1 = person
unowned var person2 = person
unowned var person3 = person
var person4 = person
var person5 = person
var person6 = person
weak var person7 = person
weak var person8 = person
weak var person9 = person
print("---end---")
我们打断点一步步走,查看内存 无主引用和强引用符合预期,接下来看下弱引用
我重新打了个断点,比较下弱引用使用的前后 上面分别为弱引用前,第一次弱引用,第二次弱引用,第三次弱引用。
我们观察出,
- 引用计数变成了存放
SideTable
的地址,首位是c
,符合预期。 - 多次弱引用,
SideTable
的地址没有发生变化,也符合源码逻辑。 SideTable
的首地址存放了person对象的地址- 对象的原有的引用计数存放在了
SideTable
中 SideTable
中弱引用计数从2变到了4,符合用了3次的weak逻辑(相当于从1开始,加了3次。为什么初始化为1,这个。。。源码中我没在意。。感兴趣的朋友可以在源码中翻翻)
弱引用之后的强引用变化
用完弱引用之后,原对象中的引用计数已经变成了SideTable
的地址,那如果再次强引用或者无主引用呢?会这么样,我们直接上代码看下
再来个简单的demo
//在刚刚的代码往后面加
unowned var person10 = person
unowned var person11 = person
var person12 = person
var person13 = person
我们可以看到,对象的引用计数还是SideTable
的地址,强引用或者无主引用的计数增加在SideTable
中实现,具体的源码就不带大家看了,有兴趣的可以自己断点看下。
后记
po
命令本身也会造成强引用计数加1,但不会影响本文对引用计数的理解,所以计数上的错误就不在修改了。