Swift引用计数的底层分析

3,210 阅读11分钟

引用计数的变化

学过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的位置打上断点,每走一步,查看下引用计数的变化,具体的结果如下截图

  1. person初始化的引用计数是0x0000000200000003
  2. person1强引用后的引用计数是0x0000000400000003
  3. person2无主引用的引用计数是0x0000000400000005
  4. person3弱引用的引用计数是0xc000000020b8488e

从1到2到3,可能还有点规律可循,但是第四步就毫无头绪了,不用怕,我们去看分析源码。

我们先查下汇编,把相关引用计数调用的方法先记下,看源码的时候能找到入口

InlineRefCounts

我们先简单探索下InlineRefCounts,看看到底是什么?

回到我们源码的HeapObject.cpp文件,上一篇文章我们分析到这 点进去,我们可以看到:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

看到InlineRefCountsRefCounts的别名,我们点进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的操作,所以我们看下BitsTypeBitsType又是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的意思,而strongExtraCountunownedCount也能从左侧看到是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地址
SideTableUnusedLowBitsSideTable没有用到的低字节位数
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,但不会影响本文对引用计数的理解,所以计数上的错误就不在修改了。