Swift 内存管理

1,305 阅读6分钟

类与结构体(上)中我们得知,对象的前的本质是HeapObject结构,前16个字节存放的是 metadata 和 refcount,在Swift中就是使用refcount来追踪和管理内存。

1、强引用

当实例对象被一个强指针指向的时候就会产生一个强引用,会使对象引用计数加1,接下来通过下面的例子来窥探引用计数的本质

截屏2022-01-27 16.39.58.png 通过x/4gx 打印出sg指针的内存结构,每次赋值打印一次,分别打印三次,对应赋值sg1之前,sg2之前,sg2之后的结果

如图,在引用到sg2的过程中,refCounts 值的变化为 0x0000000000000003 -> 0x0000000200000003 -> 0x0000000400000003,在第一次初始化的时候引用计数并不是1,被强引用的时候单纯的在refcount的那8个字节中累加,那么这是为什么呢?让我们通过源码来探究一下。

1.1 引用计数的本质

通过 HeapObject 找到 refcount成员是InlineRefCounts类型

1。1.jpeg 进入 InlineRefCounts,是RefCounts的别名 1.2.jpegRefCounts 中定义了一个RefCountBits的泛型 ,这里也就是上面别名中传入的InlineRefCountBits类型 1.3.jpeg InlineRefCountBits 这个类型又是 RefCountBitsT<RefCountIsInline> 的别名 1.4.jpeg 进入RefCountBitsT 类,这里只有一个属性,就是 BitsType bits ,并且它是用 RefCountBitsIntType属性来定义,所以refcount的本质其实就是 RefCountBitsIntType 的结构 1.5.jpeg

1.6.jpeg

可以看到RefCountBitsInt是一个UInt64位的位域信息,对于Swift的引用计数还是OC里面的引用计数都是一个64位的位域信息。 从结构上还是没办法知道为什么初始化的时候的值不是 0x0000000000000002 而是 0x0000000000000003,那么就从实例对象的初始化过程来分析

1.2 引用计数的增加

我们可以通过源码看一下,全局搜索 _swift_retain_,在 HeapObject.cpp 文件中找到它的实现,如下:

截屏2022-01-27 16.59.10.png 在进行强引用的时候,本质上是调用 refCounts 的 increment 方法,也就是引用计数 +1。我们来看一下 increment 的实现:

截屏2022-01-27 17.00.39.png 看到关键的代码,在 increment 中调用了 incrementStrongExtraRefCount,我们再去看看 incrementStrongExtraRefCount 的实现: 截屏2022-01-27 17.00.56.png 注意看,在前面我们已经知道 StrongExtraRefCountShift = 33,外部传进来的 inc = 1。假设此时 bits = 0,那此时就是 1 << 33 = 0x2。如果还有变量对其进行强引用,就是 1 += 1 << 33 --> 2 << 33 = 0x4。

到这里, incrementStrongExtraRefCount 的实现就对应了前面讲的赋值 sg1,sg2 后,refCounts 高 32 位开始的变化。

2、弱引用

弱引用不会对实例保持强引用,因此ARC可以释放被引用的实例。声明属性或者变量时,在前面加上weak关键字即可生成一个弱引用。 由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC会在被释放实例时自动地设置为nil,所以若引用的变量一定得是可选类型。

截屏2022-01-27 17.16.31.png

由上面的结果看出,在弱引用后,引用计数出现了很大的变化,这是怎么产生的呢?

截屏2022-01-27 18.25.00.png 通过汇编,我们可以看到,用 weak 修饰之后,sg 变成了一个可选项,并且,之后会调用一个 swift_weakInit 函数,紧接着调用 swift_release 函数,将 sg 的实例释放掉了。

我们来看一下 swift_weakInit 函数在源码中是怎么实现的,在 HeapObject.cpp 文件中,swift_weakInit 的实现如下

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

通过源码,可以知道用 weak 修饰之后,在内部会生成 WeakReference 类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。nativeInit 的实现如下:

void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

在这里,它调用了 refCounts 的 formWeakReference 函数,形成了弱引用,我们再来看一下 formWeakReference 的实现:

// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
    auto side = allocateSideTable(true);
    if (side)
    return side->incrementWeak();
    else
    return nullptr;
}

可以发现,它本质上就是创建了一个散列表,我们接下来看一下散列表的创建:

// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
    // 1. 取出原来的 refCounts-引用计数的信息
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);

    // Preflight failures before allocating a new side table.
    // 2. 判断原来的 refCounts 是否有当前的引用计数
    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.
        // 如果没有并且正在析构直接返回 nil
        return nullptr;
    }

    // Preflight passed. Allocate a side table.

    // FIXME: custom side table allocator
    // 3. 创建一个散列表
    HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());

    auto newbits = InlineRefCountBits(side);

    // 4. 对原来的散列表以及正在析构的一些处理
    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;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
        // Already past the start of deinit. Do nothing.
        return nullptr;
    }

    side->initRefCounts(oldbits);

    } while (! refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_release, std::memory_order_relaxed));
    return side;
}

散列表的创建可分为 4 步:

  1. 取出原来的 refCounts 引用计数的信息
  2. 判断原来的 refCounts 是否有散列表,如果有直接返回,如果没有并且正在析构直接返回 nil
  3. 创建一个散列表
  4. 对原来的散列表以及正在析构的一些处理

3、无主引用

无主引用和弱引用类似,区别是无主引用会牢牢保持对实例地址的引用,实例被销毁时无主引用不会被置为nil,所以就会造成野指针,总之,无主引用假定是永远有值的。

根据苹果的官方文档的建议。当我们知道两个对象的生命周期并不相关,但是不会去管当前引用的地址是否有值,那么我们必须使用weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用unowned

...

4、闭包的循环引用

如果我们在class的内部定义一个闭包,当前闭包访问属性的过程中,就会对我们当前的实例对象进行捕获:

class Swagger {
    var a = 18
  
    deinit{
        print("deinit")
    }
}

var sg: Swagger? = Swagger()
var closure = {
    sg!.a = 20
}

这样就是造成循环引用,程序结束并不会打印deinit

如何解决循环引用

  • 使用weak修饰闭包传入的参数,其中参数的类型是optional
var closure = { [weak sg] in
    sg!.a = 20
}
  • 使用unowned修饰闭包参数,与weak的区别在于unowned不允许被设置为nil,即总是假定有值的
var closure = { [unowned sg] in
    sg!.a = 20
}

捕获列表

默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。你可以使用捕获列表来显示控制如何在闭包中捕获值。在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用in关键词。

对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量。有以下几点说明:

  • 捕获列表中的常量是值拷贝,而不是引用
  • 捕获列表中的常量的相当于复制了变量age的值
  • 捕获列表中的常量是只读的,即不可修改

创建闭包时,内部作用域中的age会用外部作用域中age的值进行初始化,但它们的值未以任何特殊方式连接。这意味着更改外部作用域中的a的值不会影响内部作用域中age的值,也不会更改封闭内部的值,也不会更改封闭外部的值。相比之下,只有一个名为height的变量,外部作用域中的height,在闭包内部或者外部进行的更改在两个地方均可见。

func test(){
    var age = 10
    var height = 100
    
    var closure = { [age] in
        print(age)
        print(height)
    }
    age = 18
    height = 180
    closure()
}
test()

//10
//180