「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」
弱引用
在Swift中我们通过关键字weak来表明一个弱引用;weak关键字的作用是在使用这个实例的时候并不保有此实例的引用。使用weak关键字修饰的引用类型数据在传递时不会使引用计数加1,不会对其引用的实例保持强引用,因此不会阻止ARC释放被引用的实例。
由于弱引用不会保持对实例的引用,所以当实例被释放的时候,弱引用仍旧引用着这个实例也是有可能。因此,ARC会在被引用的实例释放时,自动地将弱引用设置为nil。由于弱引用需要允许设置为nil,因此它一定是可选类型;
我们将上述代码进行修改,为ClassB中的ClassA属性添加weak关键字:
可以看到,内存被正确的释放;
那么weak纠结对我们的代码做了什么处理呢?
我们定义如下代码:
weak var obj3 = ClassA()
打上断点,当运行至此代码时,我们查看汇编代码会发现调用了swift_weakInit:
我们从Swift源码中找到swift_weakInit的实现:
从源码中可以看到,声明一个weak关键字实质上是定义了一个WeakReference对象;那么nativeInit方法做了什么事情呢?
我们看到,在nativeInit方法中调用了formWeakReference()方法,也就意味着形成了弱引用(形成一个散列表):
可以看到在formWeakReference中其实是创建了一个散列表,其创建如下:
- 取出原有的
64位信息,也就是refCounts- 如果存在引用计数,则返回引用计数;
- 如果不存在引用计数或者正在被析构,则返回null;
- 之后会创建散列表
HeapObjectSideTableEntry;
根据如下源码:
可以分析出在Swift中本质上存在两种引用计数:
InlineRefCounts里边存储strong RC,unowned RC,flags标志位;如果当前存在引用计数,那么将会存储HeapObjectSideTableEntry;- 如果是
强引用,那么是strong RC + unowned RC + flags; - 如果是
弱引用,那么是HeapObjectSideTableEntry;
- 如果是
HeapObjectSideTableEntry里边存储的是64位原有的strong RC + unowned RC + flags,再加上32位的weak RC;InlineRefCounts和SideTableRefCounts都是公用的RefCountBitsT;
我们可以通过代码验证一下,查看HeapObjectSideTableEntry的实现:
可以看到其refCounts是SideTableRefCounts类型的:
而最终都指向了RefCountBitsT;
SideTableRefCounts在继承RefCountBitsT的同时也会继承其64位的位域信息,同时又多出了32位的weakBits;
我们通过代码来看一下:
通过代码发现,被weak修饰之后,其内存数据发生了变化,我们查看十六进制发现:
首先使用weak之后其62和63位被标记为了1,我们将其还原为0:
我们在分析引用计数的时候,强引用是通过左移计数的,那么弱引用应该也是类似的原理,我们在源码中能够找到散列表的偏移代码:
static const size_t SideTableUnusedLowBits = 3;
也就是在源码中,将我们的散列表的内存地址右移了3位,那么我们将地址0x20E80810左移3位就能找到散列表地址:
我们分析其内存数据如下:
我们已经知道了弱引用只能修饰可选类型,其引用的实例被释放后,属性会被自动置为nil;那么问题来了,如果我们使用的属性非可选类型,又恰好出现了循环引用,那么应该如何处理呢?在Swift中为我们提供了另外一个关键字unowned(无主引用);
无主引用
无主引用与弱引用最大的区别在于,无主引用总是假定属性是不为nil的,如果属性所引用的实例被销毁释放了,再次使用这个实例程序就会崩溃。而弱引用则允许属性值为nil,因此不会崩溃。两者相比,弱引用更加兼容,无主引用不太安全;
我们将上述代码中的weak修改为unowned:
那么对于p1此时其被声明为了无主引用;
那么针对弱引用和无主引用如何选择呢?
- 如果强引用的双方生命周期没有任何关系,使用
weak,如delegate; - 如果其中一个对象销毁,另一个对象也跟着销毁,则使用
unowned;
weak相对于unowned更兼容,更安全,而unowned性能更高;这是因为weak需要操作散列表,而unowned只需要操作64位位域信息;在使用unowned的时候,要确保其修饰的属性一定有值;