swift底层探索 07 -内存管理(refCount&weak&unowned)swift底层探索 07 -内存管理(refCount&weak&unowned)

2021-08-09 11:17:04 浏览数 (1)

提到内存管理在iOS开发中,就不得不提ARC(自动引用技术)。本文主要讨论的就是ARC在swift中是如何存储、计算,以及循环引用是如何解决的。

toc

一, refCount引用计数(强引用 无主引用)

先看一段简单的代码

代码语言:javascript复制
class classModel{
    var age : Int = 18
}
func test() {
    let c = classModel()
    var c1 = c
    var c2 = c
}
test()

通过LLDB添加断点查看当前c对象的内存情况

图一

  • 通过经验该对象的引用计数应该是:3
  • 可是图一中对象内存中refCopunt:0x0000000600000002,以及通过cfGetRetainCount(AnyObject)获取到的引用计算看起来都是不正确的。
1. cfGetRetainCount - sil解析
代码语言:javascript复制
class classModel{
    var age : Int = 18
}
let temp = classModel()
CFGetRetainCount(temp)

编译后的Sil文件:

图二

  • 通过图二sil文件很简单的看出CFGetRetainCount在调用之前对temp这个变量进行了一次强引用,也就是引用计数加1。所以通过CFGetRetainCount获得的引用计数需要-1才是正确的。这也印证了之前的经验推论。
2. refCount - 类型的源码

swift底层探索 01 - 类初始化&类结构一文中有对swift类的源码进行过简单的解释。

相信你一定会有疑惑:0x0000000600000002是什么?它为什么被叫做refCount,探索方法依旧是翻开源码!

  • 由于源码中涉及多层嵌套 模板类 泛型,所以阅读起来还是有点困难的,建议自己动手试试。swift-5.3.1源码地址
(1) 该方法是swift对象初始化方法
代码语言:javascript复制
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • 其中refCounts(InlineRefCounts::Initialized)就是refCounts的初始化方法.
  • InlineRefCountsrefCounts的类型.
(2) InlineRefCounts类型
代码语言:javascript复制
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
  • InlineRefCounts是重命名
  • InlineRefCounts = RefCounts
(3) RefCounts类
代码语言:javascript复制
template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
  //省略方法
}
  • RefCounts是依赖泛型:RefCountBits的模板类。同时发现refCounts的类型也是泛型:RefCountBits
  • 通过第2步,第3步: RefCounts = RefCountBits = InlineRefCountBits
(4) InlineRefCountBits类型
代码语言:javascript复制
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • InlineRefCountBits也是重命名
  • InlineRefCountBits = RefCountBitsT;
(5) RefCountIsInline枚举
代码语言:javascript复制
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };
  • 传入枚举值:RefCountIsInline = true
(6) RefCountBitsT 核心类
代码语言:javascript复制
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
    //内部变量
    BitsType bits;
    //内部变量类型
    typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

    ...
    //省略无关代码
}
  • 内部只有一个变量bits,类型为BitsType
(7) RefCountBitsInt 结构体
代码语言:javascript复制
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
  typedef uint64_t Type;
  typedef int64_t SignedType;
};
  • 根据第6步的传参得到RefCountBitsInt结构,以及Type == uint64_t
(8) 【总结】
  • 通过第1步,第2步,第3步,第4步: InlineRefCounts = RefCounts = RefCountBits = InlineRefCountBits = RefCountBitsT;(该关系并不严谨只是为了解释简单)
  • 通过第6步,第7步: RefCountBitsTbits类型是:uint64_t;
  • refCounts的类型为RefCountBitsT,内部只有一个变量bits类型为uint64_t;
  • RefCountBitsT是模板类,首地址指向唯一内部变量bits;
  • 结论为:**uint64_t : refCounts**.
3. refCount - 初始化的源码

现在再看0x0000000600000002知道它是一个uint64_t的值,可是内部存储了哪些值还需要查看初始化方法,观察初始化方法做了什么?

(1) 该方法是swift对象初始化方法
代码语言:javascript复制
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • Initialized初始化
(2) RefCounts初始化方法
代码语言:javascript复制
template <typename RefCountBits>
class RefCounts {
    std::atomic<RefCountBits> refCounts;
    
    enum Initialized_t { Initialized };
    
 constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}
    ...
    //省略无关代码
}
  • 调用了RefCountBits的初始化方法,根据上一步中的关系对应:RefCountBits = InlineRefCountBits = RefCountBitsT
(3) RefCountBitsT初始化方法
代码语言:javascript复制
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }
(4)Offsets对应关系

Offsets的关系图:undefined

简书-月月

(5)【总结】
  • 0x0000000600000002就可以拆分为: 5部分。强引用的引用计数位于:33-62
代码语言:javascript复制
0x0000000600000002 >> 33 // 引用计数 = 3
  • 同样满足之前的论证。
补充1:
  • 初始化并且没有赋值时,引用计数为0,无主引用数为:1。源码中的确也是这样的RefCountBits(0, 1)
补充2:
代码语言:javascript复制
class PersonModel{
    var age : Int = 18
}
func test() {
    let c = PersonModel()
    var c1 = c
    var c2 = c
    var c3 = c
    //增加了一个无主引用
    unowned var c4 = c
}
test()

图三-输出结果

  • unowned在本文的解决循环引用中会解释。
  • StrongExtraRefCountShift(33-63位) : 0x0000000800000004右移33位 = 4
  • UnownedRefCountShift(1-31位) : 0x0000000800000004左移32位,右移33位。 = 2
4. 引用计数增加、减少

知道了引用计数的数据结构初始化值,现在就需要知道引用计数是如何增加减少,本文中以增加为例;

通过打开汇编,查看调用堆栈:

图三

  • 发现会执行swift_retain这个函数
swift_retain源码
代码语言:javascript复制
//入口函数
HeapObject *swift::swift_retain(HeapObject *object) {
  CALL_IMPL(swift_retain, (object));
}

static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    //引用计数在该函数进行 1操作
    object->refCounts.increment(1);
  return object;
}
  • 后面源码的阅读会进行断点调试的方式。
increment

图四

通过可执行源码进行调试可执行源码。

  • 根据断点证实的确是执行到increment函数,并且新增值是1
具体计算的方法

图五

  • 计算都是从33位开始计算的

二, refCount 循环引用

代码语言:javascript复制
class PersonModel{
    var teach : TeachModel?
}
class TeachModel{
    var person : PersonModel?
}

面对这样的相互包含的两个类,使用时一定会出现相互引用(循环引用)

图六

  • deinit方法没有调用,造成了循环引用。
1. weak关键字

通过OC的经验,可以将其中一个值改为weak,就可以打破循环引用.

代码语言:javascript复制
class PersonModel{
    weak var teach : TeachModel?
}
class TeachModel{
    weak var person : PersonModel?
}

图六

  • 很显然weak是可以的。问题是:weak做了什么呢?
2. weak 实现源码
代码语言:javascript复制
weak var weakP = PersonModel()

依旧是打开汇编断点.

图七

  • 从图七能看出到weak是调用了swift_weak
swift_weak源码
代码语言:javascript复制
//weak入口函数
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

void nativeInit(HeapObject *object) {
//做一个非空判断
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
  • 没有找到WeakReference对象的创建,猜测是编译器自动创建的用来管理weak动作.
通过formWeakReference创建HeapObjectSideTableEntry
代码语言:javascript复制
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
调用allocateSideTable进行创建
代码语言:javascript复制
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
    //获取当前对象的原本的引用计数(uInt64_t)
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  ...
  
  // FIXME: custom side table allocator
  
  //创建HeapObjectSideTableEntry对象
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
    //RefCountBitsT对象进行初始化
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      return nullptr;
    }
    side->initRefCounts(oldbits);
    //通过地址交换完成赋值
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
  • 最终将RefCountBitsT对象(class)的地址和旧值uint_64进行交换。
HeapObjectSideTableEntry对象
代码语言:javascript复制
class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
    ...
}

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

class RefCountBitsT {
    //Uint64_t就是strong_count | unowned_count
    BitsType bits;
}

通过源码分析得出HeapObjectSideTableEntry对象的内存分布

RefCountBitsT初始化

最终保存到实例对象的refcount字段的内容(RefCountBitsT)创建

代码语言:javascript复制
    //Offsets::SideTableUnusedLowBits = 3
    //SideTableMarkShift 高位 62位
    //UseSlowRCShift 高位 63位
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }
  • 62位,63位改为0 -> 整体左移3位: 就可以得到sideTable对象的地址。
lldb验证

现在知道了refcount字段获取规律,以及sideTable对象的内部结构,现在通过lldb验证一下。

图八

  • 发现被weak修饰之后,refcount变化成sideTable对象地址 高位标识符

图九

  • 将高位62,63变为0后,在左移3位.

图十

  • 0x10325D870这就是sideTable对象地址
weak_count 增加

weakcount是从第二位开始计算的。

formWeakReference函数中出现了side->incrementWeak();sideTable对象创建完成后调用了该函数.

代码语言:javascript复制
  HeapObjectSideTableEntry* incrementWeak() {
    if (refCounts.isDeiniting())
      return nullptr;
      //没有销毁就调用
    refCounts.incrementWeak();
    return this;
  }
  
  void incrementWeak() {
    //获取当前的sideTable对象
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;
    do {
      newbits = oldbits;
      assert(newbits.getWeakRefCount() != 0);
      //调用核心自增函数
      newbits.incrementWeakRefCount();
      
      if (newbits.getWeakRefCount() < oldbits.getWeakRefCount())
        swift_abortWeakRetainOverflow();
        //通过值交换完成赋值
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));
  }
  
  void incrementWeakRefCount() {
  //就是一个简单  
    weakBits  ;
  }
  1. 在声明weak后,调用了incrementWeak自增方法;
  2. incrementWeak方法中获取了sideTable对象;
  3. incrementWeakRefCount完成了weakBits的自增;

注:在weak引用之后,在进行strong强引用后,refCount该如何计算呢?篇幅问题就不展开了,各位可以自己试试。

三, 捕获列表

  • [weak t] / [unowned t] 在swift中被称为捕获列表
  • 作用:
    1. 解决closure的循环引用;
    2. 进行外部变量的值捕获

本次换个例子。

代码语言:javascript复制
class TeachModel{
    var age = 18
    var closure : (() -> Void)?
    deinit {
        print("deinit")
    }
}
func test() {
    let b = TeachModel()
    b.closure = {
        b.age  = 1
    }
    print("end")
}
  • 看到这段代码,deinit会不会执行呢?答案是很显然的,实例对象的闭包和实例对象相互持有,一定是不会释放的。
作用1-解决循环引用
代码语言:javascript复制
func test() {
    let b = TeachModel()
    b.closure = {[weak b] in
        b?.age  = 1
    }
    print("end")
}

func test() {
    let b = TeachModel()
    b.closure = {[unowned b] in
        b?.age  = 1
    }
    print("end")
}

执行效果,都可以解决循环引用:

  • weak修饰之后对象会变为
作用2-捕获外部变量

例如这样的代码:

代码语言:javascript复制
func test() {
    var age = 18
    var height = 1.8
    var name = "Henry"
    
    height = 2.0
    //age,height被闭包进行了捕获
    let closure = {[age, height] in
        print(age)
        print(height)
        print(name)
    }
    
    age = 20
    height = 1.85
    name = "Wan"
    
    //猜猜会输出什么?    
    closure()
}
  • age,height被捕获之后,值虽然被外部修改但不会影响闭包内的值
  • 闭包捕获的值时机为闭包声明之前
闭包捕获之后值发生了什么?

通过打开汇编调试,并查看寄存器堆栈信息.

  • 猜测rdx-0x0000000100507e00,存在堆区。而闭包外的age是存在栈区的。
几种基本汇编指令详解

0 人点赞