Swift进阶-refCount结构&强引用&弱引用&闭包循环引用

Swift是使用引用计数来进行内存管理,本文将从refCount结构进行深入分析,进而对强引用弱引用循环引用进行分析

refCount结构分析

  • Swift进阶-类&对象&属性 中我们分析得知refCount是用来记录引用计数,下面从一个案例中来查看refCount

    class WSPerson {
        var age: Int = 18
    }
    var ws = WSPerson()
    var ws1 = ws
    var ws2 = ws
    复制代码
    • 运行打印refCount结果如下:

    截屏2022-01-03 23.05.11.png

    • 得到的refCount并不是一个数,像是两个数的组合,具体的结构就得去 Swift源码 分析
  • Swift进阶-类&对象&属性 中,我们在源码中得知HeapObject的构造中有refCounts

    截屏2022-01-04 09.33.02.png

    • 可以看到refCounts的构造方法都是通过InlineRefCountBits来调用相关的方法进行的,所以此处可以真正起作用的是InlineRefCountBits
  • 在查看InlineRefCountBits

    typedef RefCounts<InlineRefCountBits> InlineRefCounts;
    复制代码
    • InlineRefCountBits是此处RefCounts中传入的类型,那么此时核心就变成了RefCounts
  • 继续跟踪RefCounts,得到它是一个模版类,在运行时真正起作用的是传入的类型:

    截屏2022-01-04 09.43.15.png

    • 通过对RefCounts类的阅读发现里面起作用的是RefCountBits,也就是说我们研究的核心实质是传入的InlineRefCountBits
  • 在进入InlineRefCountBits阅读发现它也是个模版类型:

    typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
    复制代码
    • 下面再来看看RefCountIsInline是什么:

      enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };
      复制代码
      • RefCountIsInlineRefCountInlinedness类型的枚举,RefCountIsInline代表trueRefCountNotInline代表false
    • 于是再来分析RefCountBitsT,它也是个模版类:

      截屏2022-01-04 10.58.36.png

      • 分析得知bits是它的成员变量,它的类型为uint64_t占用8字节
      • RefCountBitsT的初始化都是通过内存平移来实现的:

      截屏2022-01-04 11.02.47.png

      • 而内存平移的核心方法是Offsets,它是RefCountBitOffsets<sizeof(BitsType)>的别名,再继续点击进入RefCountBitOffsets可以查看所有的位移的信息:

      截屏2022-01-04 11.09.46.png

      • 根据位移信息我们可以得到64位下的refCount的内存分布:

      截屏2022-01-04 13.43.20.png

      • UnownedRefCount:是无主引用(Unowned)的引用计数
      • StrongExtraRefCount:是强引用的引用计数
  • 拿到refCount的内存分布后,我们再回到前面的案例,这明显是一个强引用,而强引用在refCount中占用的是33~62位,所以refCount的内存0x0000000600000003中,强引用个数的是3,可以通过计算器验证:

    截屏2022-01-04 14.08.09.png

强引用

  • 下面来分析下强引用,首先来看下没有强引用的案例Sil文件分析:

    class WSPerson {
        var age: Int = 18
    }
    var ws = WSPerson()
    复制代码

Sil分析

  • 生成的Sil文件主要内容如下:

    截屏2022-01-05 21.38.51.png

  • 再添加强引用

    var ws1 = ws
    复制代码
  • 然后生成Sil文件

    截屏2022-01-05 22.01.32.png

    • 通过阅读Sil文件得知,强引用是读取%3的内存,并调用copy_addr函数拷贝一份,并存储到地址%9
    • Swift Intermediate Language 文档中有对copy_addr的解释:

    截屏2022-01-05 22.26.30.png

    • 在文档中,copy_addr相当于做了一下几件事:
        1. load内存%0,并赋值给%new
        1. %new进行strong_retain
        1. %new存储到%1
  • 再去汇编查看,发现强引用核心调用的方法是swift_retain

    截屏2022-01-05 23.11.30.png

下面再去Swift源码中分析swift_retain做了什么

swift_retain

  • swift_retain在源码中的代码不多,代码如下:

    截屏2022-01-05 23.26.38.png

    • 主要是调用increment函数进行引用计数加1
  • 在查看increment代码:

    截屏2022-01-05 23.33.42.png

    • 主要是获取oldbits然后将赋值给newbits,再用newbits调用incrementStrongExtraRefCount进行增加引用计数
  • 再继续阅读incrementStrongExtraRefCount函数:

    SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool
    incrementStrongExtraRefCount(uint32_t inc) {
        // This deliberately overflows into the UseSlowRC field.
        bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
        return (SignedBitsType(bits) >= 0);
    }
    复制代码
    • 此处的核心是将传入的inc(1),进行左移StrongExtraRefCountShift (33)位,从上面分析我们知道,33位刚好是强引用位数的第一位,计算器验证1<<33结果如下:

      截屏2022-01-05 23.59.32.png

    • 1<<3316进制刚好是0x200000000,所以没多一个强引用,refCount地址都会增加0x200000000,使用案例验证如下:

    截屏2022-01-06 00.05.58.png

  • 通过案例可知,Swift在创建实例对象时的默认引用计数是1,而OCalloc创建对象时是没有引用计数的,此处是SwiftOC不同点

弱引用

  • 下面来看看弱引用:

    class WSPerson {
        var age: Int = 18
    }
    var ws = WSPerson()
    var ws1 = ws
    var ws2 = ws
    weak var ws3 = ws
    复制代码
  • 通过查看weak修饰的变量,发现weak变量是可选类型:

    截屏2022-01-06 00.25.30.png

  • 再在weak前后打印对象内存分布,发现reCount地址发生了变化:

    截屏2022-01-06 00.30.38.png

  • 再在汇编代码中查看weak修饰的变量,发现最终调用了swift_weakInit函数:

    截屏2022-01-06 00.34.06.png

下面我们再重点去分析swift_weakInit函数

swift_weakInit

  • swift_weakInit函数在源码实现如下:

    WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
      ref->nativeInit(value);
      return ref;
    }
    复制代码
    • 函数主要是WeakReference的实例ref去调用nativeInit方法
  • nativeInit的核心代码如下:

    void nativeInit(HeapObject *object) {
        auto side = object ? object->refCounts.formWeakReference() : nullptr;
        nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
    }
    复制代码
    • 主要是判断weak对象是否存在,如果存在则调用对象的refCounts.formWeakReference函数,不存在则为nullptr,然后将结果进行存储
  • 在继续查看formWeakReference函数的代码

    template <>
    HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
    {
      auto side = allocateSideTable(true);
      if (side)
        return side->incrementWeak();
      else
        return nullptr;
    }
    复制代码
    • 这里主要是创建了一个散列表,然后使用创建的散列表调用incrementWeak函数
  • 继续查看incrementWeak函数,最终找到如下代码:

    void incrementWeak() {
        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));
    }
    复制代码
    • 主要是在compare_exchange_weak条件中判断oldbitsnewbits
      • 该函数在这里传入期待值新值,它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换
    • 满足条件则将旧值赋给新值,然后使用newbits调用incrementWeakRefCount函数
  • incrementWeakRefCount函数最终调用是bits自增:

    void incrementWeakRefCount() { weakBits++; }
    复制代码
    • 这里出现了新的名词weakBits,在上面的文中我们知道RefCountBitsT中有uint64_tbits,那么这个weakBits肯定是在创建散列表时产生的,然后我们再去看看创建散列表时都做了些什么
  • allocateSideTable的代码如下:

    截屏2022-01-06 09.29.19.png

  • 现在我们分析的主线是weakBits是什么,所以我们需要关注的代码是创建处,也就是HeapObjectSideTableEntry,再来跟进HeapObjectSideTableEntry发现它是一个类:

    截屏2022-01-06 10.16.58.png

    • HeapObjectSideTableEntry中的成员变量refCountsSideTableRefCounts类型,它是模版函数RefCounts<SideTableRefCountBits>的别名,实际的内容是根据SideTableRefCountBits来确定的

    • 再去查看SideTableRefCountBits

      截屏2022-01-06 10.22.56.png

      • 这里可以看出SideTableRefCountBits是继承RefCountBitsT,而且有自己的成员变量weakBits,也就是说SideTableRefCountBits有继承过来uint64_t位的成员变量bitsweakBits
      • SideTableRefCountBits初始化时,weakBits默认值为1
  • 此时我们找到了weakBits,但它的结构我们不清楚,然后再回到allocateSideTable函数查看创建newbits的函数InlineRefCountBits

    截屏2022-01-06 10.36.42.png

    • 该函数的主要作用是将bits进行右移3位,然后将第63位与62位置为1,此时我们可以得到散列表在bits中的位置:

    截屏2022-01-06 11.06.23.png

    • 那么我们想要拿到原来的引用计数,只需要先将第63位与62位置为0,再左移3位,就可以拿到原来的引用计数。
  • 下面去验证:

    截屏2022-01-06 11.28.29.png

    • 在打印的分块内存中,我们可以看到继承过来的强引用的引用计数,也就是3,而由于weakBits初始值是1,所以此时显示的弱引用值为2,得以验证

闭包的循环引用

  • 闭包的循环引用有如下案例:

    class WSPerson {
        var age: Int = 18
        var birthday: (() ->Void)?
      
        deinit {
            print("WSPerson deinit ~~~")
        }
    }
    func test() {
        let p = WSPerson()
        p.birthday = {
            p.age += 1
        }
        p.birthday!()
    }
    test()
    复制代码
    • test()函数执行完,WSPerson中的deinit(反初始化,相当于dealloc)并不会调用,因为p->birthday->p导致循环引用,可以使用weakunowned来解决循环引用的问题
  • 使用weak

    截屏2022-01-06 13.44.03.png

    • 使用weak后,发现deinit函数得以执行,所以解决了循环引用
    • 使用weak修饰后,变量变成可选类型,使用时 需要解包,写法上稍微有些麻烦

unowned(无主引用)

  • 使用unowned

    截屏2022-01-06 13.47.42.png

    • 执行结果,deinit函数可以执行
    • unowned不允许被设置为nil,它是假定有值的,这一点与weak不同,它也不是强引用
    • unowned由于总是假定有值,所以当对象释放后再调用的话会产生野指针
  • 在上面refCount分析中得知unownedRefCount类型的引用计数在1~31位,例子如下

    截屏2022-01-06 14.43.16.png

    • 由于unownedRefCount是从第一位开始,所以每增加一个,在16进制上增加0x2,在二进制上是0x10
    • 当没有unowned时,发现在第一位默认是1

    截屏2022-01-06 14.49.17.png

    • 所以无主类型引用计数的个数,是UnownedRefCount减1

捕获列表

  • 在上面解决闭包循环引用时[xxx]的写法,叫做捕获列表,先来看看案例:

    var age : Int = 18
    var height: CGFloat = 180
    
    let clouse = { [age] in
        print(age)
        print(height)
    }
    
    age = 19
    height = 190
    
    clouse()
    复制代码
    • 打印结果如下:

    截屏2022-01-06 14.59.30.png

    • 结果捕获列表中的age在闭包中打印的是原始值,而height打印是最新值
      1. 对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。
      1. 捕获列表中的变量是值拷贝,且不可修改

猜你喜欢

转载自juejin.im/post/7049988418700312590
今日推荐