深入理解Swift中的引用计数

3,590 阅读6分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

在iOS开发中,OCSwift都是通过ARC进行内存管理的,本篇文章我们将一起探索Swift中的引用计数。

三种引用计数

Swift对象一文中,我们探索了Swift对象的本质,其数据结构如下,里面的InlineRefCounts为该对象的引用计数值。

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts
  
struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata; // 1
  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; // 2 
}

先来看看苹果的官方是来怎么来介绍引用计数规则的:

通常来说,一个Swift对象3种引用计数,存储在紧挨着对象isa的地方或者isa指向的side table entry

  • 1,stong RC:强引用计数,计算强引用的计数。 当 strong RC等于0的时候,对象就会deinit,使用无主引用访问该对象,就会发生错误。使用弱引用访问该对象会返回nil

  • 2,unowned RC:无主引用计数,计算无主引用计数,无主引用计数会额外+1,来代表强引用计数,在 对象deinit完成后,会自动-1。当无主引用计数等于0的时候,该对象的内存空间,已经被释放。

  • 3,weak RC:弱引用计数,计算弱引用的计数,弱引用计数会额外+1,代表无主引用计数值,当对象的内存空间被释放后,会自动-1,当弱引用计数值为0,这个对象的side table entry已经被释放。

对象在初始化的时候,是没有side table的,当出现以下情况的时候,就会有side table

  • 1,有一个弱引用指向它。
  • 2,强引用计数或无主引用计数在数值溢出的时候(小于32位).
  • 3,对象有关联对象

获取side table entry是一个单向操作,这样可以保证不会丢失,且防止了一些线程竞争的问题。

其内存结构如下

HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

没有使用side table entry,称为InlineRefCount。使用side table entry存储引用计数的称为SideTableRefCounts

这段话摘自Swift源码的注释。

截屏2022-02-13 下午1.50.03.png

接下来,我们结合源码来探索Swift的引用计数管理。

源码分析

强引用计数和无主引用计数

当初始化Swift对象的时候,在refCounts函数中,会初始化引用计数值

截屏2022-02-12 下午3.42.00.png

LLVM_ATTRIBUTE_ALWAYS_INLINE
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) <<  Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }

传入两个参数强引用计数(0)无主引用(1),通过位运算,将数值存放到对应的空间上。

对应的偏移数值如下所示

截屏2022-02-12 下午3.52.30.png

根据内存偏移量,我们可以分析出,引用计数的存储规则如图所示

截屏2022-02-12 下午4.13.55.png

  • 1,第0位,用来标记是否是纯Swift对象。
  • 2,第1~31位,存储无主引用计数
  • 3,第32位,表示是否是deiniting状态。
  • 4,第33~62位,存储强引用计数。
  • 5,第63位,标记是否有散列表,若有弱引用该位值为1。

接下来,我们使用lldb来动态查看其内存分布。

lldb调试

有如下代码

import Foundation

class Person {
    var age = 10
}

var p: Person? = Person() 
//1
var p1 = p 
// 2
var p2 = p 
// 3
unowned var p3 = p
// 4
print(p1?.age)

在断点1处,我们先输出p变量的内存地址,然后查看内存分布情况

(lldb)p Unmanaged.passUnretained(p!)
(Unmanaged<LYCSwift.Person>) $R0 = {
  _value = 0x0000000101f1d180 (age = 10)
}
(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000000000003
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e

此时refCounts的值为0x0000000000000003,通过计算器将其转化为二进制

截屏2022-02-12 下午4.11.31.png 前两位分别是1,其他位是0,就代表着此时为纯swift对象,强引用计数为0,无主引用计数为1

咦?等等......

这里和我们平常理解的不一样,OC对象初始化完成后,isa指针的extra_rc为1,其强引用计数值为1,为什么swift对象初始化完成,里面强引用计数值为0了?

翻了下苹果的注释,前面已经提到了无主引用会额外+1,代表强引用计数,所以此时存储的强引用计数值为0。

我们接着往下看: 在断点3处

(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000400000003
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e

截屏2022-02-12 下午4.26.34.png

经过两个强引用p1、p2后,此时,强引用计数值为0x102,无主引用计数值为0x11

在断点4处

(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000400000005
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e

截屏2022-02-12 下午4.36.46.png 经过unown修饰的变量引用后,会使无主引用计数值+1,此时 无主引用计数2,强引用计数为 2

弱引用计数

当我们使用 weak var p4 = p时,就会产生成一个WeakReference对象,并且此时对象的引用计数类型也会发生变化,会由InlineRefCounts转变为SideTableRefCounts。 此时HeapObject的结构为

HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        HeapObjectSideTableEntry*
      }
    }
  }
  
class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
}

class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;
}

我们通过查看运行时的汇编代码,我们发现会执行swift_weakInit函数,来处理弱引用

截屏2022-02-12 下午4.49.10.png

其内部实现如下

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

主要实现代码

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); // 1
  // Preflight failures before allocating a new side table.
  if (oldbits.hasSideTable()) {
    // Already have a side table. Return it.
    return oldbits.getSideTable(); // 2
  }else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // Already past the start of deinit. Do nothing.
    return nullptr;
  }
  // Preflight passed. Allocate a side table.
  // FIXME: custom side table allocator**
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject()); 
  auto newbits = InlineRefCountBits(side);
  do {
    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;
failIfDeiniting && oldbits.getIsDeiniting()) {
      // Already past the start of deinit. Do nothing.
      return nullptr;
    }
    side->initRefCounts(oldbits); // 3
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,std::memory_order_release,
std::memory_order_relaxed));

  return side;
}
// 获取Side Table
LLVM_ATTRIBUTE_ALWAYS_INLINE
HeapObjectSideTableEntry *getSideTable() const {
    assert(hasSideTable());
    // Stored value is a shifted pointer.
    return reinterpret_cast<HeapObjectSideTableEntry *>
      (uintptr_t(getField(SideTable)) << Offsets::SideTableUnusedLowBits);
  }
  • 1,获取 InlineRefcounts oldbit,通过标志位判断是否已存在HeapObjectSideTableEntry
  • 2,如果有SideTable,通过HeapObjectSideTableEntry *地址指针,左移3位,即可得到HeapObjectSideTableEntry对象值。
  • 3,将 InlineRefCounts中的强引用计数无主引用计数存入散列表中。

得到HeapObjectSideTableEntry之后,在进行 ide->incrementWeak();32位的弱引用计数+1。 此时HeapObjectSideTableEntryHeapObject的关系如下图所示

截屏2022-02-13 下午1.46.02.png

lldb调试

我们看下如下代码的引用计数变化

 // 强引用计数 无主引用 弱引用
var s: Person? = Person() // 0 1
var s1 = s // 1 1
var s2 = s1 // 2 1
print(2)
weak var s3 = s2 // 2 1 2
print(4)
weak var s4 = s3 // 2 1 3
print(s4?.age)

// 后面的数值分别代表: 强引用计数 无主引用计数 和 弱引用计数

print(2)函数处,我们看下引用计数

(lldb)p Unmanaged.passUnretained(s!)
(Unmanaged<LYCSwift.Person>) $R0 = {
  _value = 0x000000010384e8f0 (age = 10)
}
(lldb)x/8gx 0x000000010384e8f0
0x10384e8f0: 0x0000000100008200 0x0000000400000003
0x10384e900: 0x000000000000000a 0x0000000000000023
0x10384e910: 0x0000000080080000 0x0000000100a4f3a8
0x10384e920: 0x0000000000000000 0x0000000100a3dce8

截屏2022-02-13 下午2.03.18.png 此时,强引用计数为2,无主引用计数为1

print(4)

(lldb) x/8gx 0x000000010384e8f0
0x10384e8f0: 0x0000000100008200 0xc000000020748016
0x10384e900: 0x000000000000000a 0x0000000000000023
0x10384e910: 0x0000000080080000 0x0000000100a4f3a8
0x10384e920: 0x0000000000000000 0x0000000100a3dce8

0xc000000020748016HeapObjectSideTableEntry的指针地址,将其左移3位得到地址0x103A400B0,就得到了 HeapObjectSideTableEntry的值,我们读取该值

(lldb)x/8gx 0x103A400B0
0x103a400b0: 0x000000010384e8f0 0x0000000000000000
0x103a400c0: 0x0000000400000003 0x0000000000000002
0x103a400d0: 0x0000000000000000 0x0000000000000000
0x103a400e0: 0x00007fff84990008 0x00007fff84995200

0x000000010384e8f0HeapObject的内存指针,0x0000000400000003存储的是强引用计数弱引用计数0x0000000000000002前32位存储的是弱引用计数. 此时,强引用,无主引用和弱引用计数值分别为2 1 2弱引用计数会额外+1,代表无主引用计数值

⚠️⚠️⚠️ 注意:为什么会额外+1,可以看文章的开始部分的苹果注释。

print(s4?.age)

(lldb) x/8gx 0x103A400B0
0x103a400b0: 0x000000010384e8f0 0x0000000000000000
0x103a400c0: 0x0000000400000003 0x0000000000000003
0x103a400d0: 0x0000000100a4f2e8 0x0000000100000003
0x103a400e0: 0x08d0eef4ea1dadab 0x08d0eef4ea1dadab

此时,强引用,无主引用和弱引用计数值分别为2 1 3

总结

Swift的引用计数有三种,强引用计数、无主引用计数和弱引用计数。当对象初始化的时候强引用计数为0,无主引用为 1, 当第一次使用weak修饰的时候,弱引用计数值为2,原因在文章的开头有阐述。

如果觉得有收获请按如下方式给个 爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!