Swift3之Weak引用
由于不同的
Swift
版本引用计数实现会有不同,该文讨论的引用计数原理都基于Swift3
弱引用
做iOS
开发时经常会遇到循环引用,如果处理不当会导致内存泄露,我们通常会使用weak reference
弱引用来解决该问题,因为弱引用不会retain
对象,当对象引用计数变为0时,弱引用指针将会被赋nil
。
实现过程
通常如果实现弱引用,可以让每一个对象维护所有指向该对象的一个弱引用列表,当一个弱引用指向一个对象时,该引用被添加进列表,当弱引用重新赋值或生命期结束,则将其从列表中移除,当一个对象dealloced
后,列表中的所有引用会被赋nil
。在多线程环境中,需要对获得弱引用和释放对象的操作进行同步,以避免竞态条件,既当一个线程在释放最后一个强引用对象的同时,另一个线程正尝试加载该对象的弱引用。
Objective-C
实现的过程为,每一个弱引用是一个指向目标对象的指针,编译器会使用helper
函数,来避免直接读写指针,确保读取弱引用对象时不会返回正在被释放的对象指针。
实战
接下来,我们将创建几个方法来观察弱引用的过程。
首先我们想要能够dump
出一个对象的内存,如下方法将获取一块内存,将其分成指针大小的块,再将其内容转成16进制,以便于观察:
1 | // Swift version: Swift3 |
接下来,我们将创建一个dumper
函数来打印一个对象实例的内容,参数为一个对象实例,函数返回一个闭包。在函数内部,会创建一个UnsafeRawPointer
指针来指向对象,这样能确保不会进行引用计数的操作,且当对象被释放后,我们仍可以dump
出指针所指向内存的内容。
1 | // Swift version: Swift3 |
如下有一个类,它有一个弱引用的属性target
,同时,创建两个dummy
属性,当dump
内存内容时可以更清晰的识别:
1 | class WeakReferer { |
接下来,创建一个该对象的实例,并dump
出内存的内容:
1 | let referer = WeakReferer() |
结果为:
1 | SwiftLearn.WeakReferer 0x000060000004eb50: 000000010ebb0c50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
我们可以看到,dummy1
位于第4块,dummy2
位于第6块,弱引用位于他们中间,正如我们期待的,其内容为0.
现在我们给它赋一个值看看,我将通过一个do
块来控制target
的生命周期:
1 | // 因为target是NSObject对象,所以需要改一下WeakReferer的target属性的类型 |
打印结果为:
1 | <NSObject: 0x7fda6a21c6a0> |
正如我们所看到的,target
对象的指针直接存放在弱引用中。接下来,我们在do
块结束之后再打印一下看看target
释放后的情况:
1 | print(refererDump()) |
1 | WeakReferer 0x00007ffe32300060: 000000010cfb44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
被赋值为了nil
.
接下来,我们再测试一下将target
赋值为一个纯Swift
对象,看是不是和Objective-C
的NSObject
一样,如下为纯Swift``target
:
1 | class WeakTarget {} |
再试一下,看看结果怎样:
1 | let referer = WeakReferer() |
target
开始为nil
,然后赋给它一个值:
1 | SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
接下来,当target
离开作用域,我们看看弱引用是否被赋nil
:
1 | SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 000060800002a9c2 abcdefabcdefabcd |
咦,怎么没被赋nil
,难道是target
没有被释放,产生了内存泄露?我们给target
对象加上析构函数看看:
1 | class WeakTarget { |
运行之前的代码,看看结果:
1 | SwiftLearn.WeakReferer 0x00006000002423a0: 000000010538bc50 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
析构函数被调用了,但是弱引用并没有被赋nil
,这跟我们印象中的weak
运行过程有出入,我们接着访问一下该值,看是否会产生crash
:
1 | let referer = WeakReferer() |
1 | WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
并没有产生crash
,打印结果为nil
。
让我们再仔细的分析一下,首先我们先给WeakTarget
对象加上一个dummy
属性,dump
的时候能更方便的查看内存内容:
1 | class WeakTarget { |
接下来,我们将使用新的代码执行相同的过程并dump
出每一步的对象内容:
1 | let referer = WeakReferer() |
我们一个一个看一下输出的内容。一开始,target
属性为nil
:
1 | SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
给target
属性设置一个对象实例,target
对象实例的内容为:
1 | SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000200000004 0123456789abcdef |
将对象实例赋给target
属性,我们能够看到weak
属性已经被赋值了,赋的值为target
对象地址+2字节,既对于weak
指针,它并不直接指向对象的地址,而是指向对象的side table
(下文会讲到side table
的概念),unowned``strong
引用会直接指向对象:
1 | SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 00006080000357e2 abcdefabcdefabcd |
target
对象的内容块中有一个字段自增了2:
1 | SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000400000004 0123456789abcdef |
target
被析构:
1 | WeakTarget deinit |
我们看到引用的对象依然保持着target
的指针:
1 | SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 00006080000357e2 abcdefabcdefabcd |
看上去好像target
依然还存活着,我们看到target
对象的有一个字段减了2:
1 | SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec4d48 0000000200000002 0123456789abcdef |
访问一下target
属性,此时会产生nil
,尽管内容中的指针并没有被赋nil
:
1 | nil |
我们再打印一下referer
对象的内容,发现访问完target
属性后,target
字段被修改了,赋为了nil
:
1 | SwiftLearn.WeakReferer 0x0000608000243450: 000000010cec4c58 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd |
target
对象现在完全被清除了:
1 | SwiftLearn.WeakTarget 0x00006080000357e0: 000000010cec3370 0000000200000008 000000010cebe200 |
从上面的运行过程,我们发现某些字段会被增/减,我们进行一些测试,来看一下有没有规律:
1 | let target = WeakTarget() |
打印结果为:
1 | SwiftLearn.WeakTarget 0x00006080000395a0: 000000010e8aad40 0000000200000004 0123456789abcdef |
从结果,我们能够看到,对于每一个weak
引用,第2个块中的第一个数会自增2;而每一个strong
引用,第二个数会自增4.
总结一下:
- 弱指针看起来就是普通的指针。
- 当一个弱引用对象的
deinit
执行后,对象并没有被释放,且弱引用指针也没有被赋nil
。 - 当弱引用执行完
deinit
后,访问弱引用对象,则对象指针会被赋nil
,且目标对象被释放。 - 弱引用对象对于每一个弱引用会包含一个引用计数(
unowned
计数和weak
计数为同一个),且与强引用计数分开统计。
Swift代码
接下来,我们看一下Swift
实现的源代码。Swift
标准库表示一个在堆上的对象的结构体为:
1 | /// The Swift3 heap-object header. |
Swift
的metadata
字段等同于Objective-C
的isa
字段,事实上,他们是兼容的。接着,使用了一个宏来定义字段,该字段用来管理引用计数:
1 | ///Swift默认为InlineRefCounts,当有弱引用指向该对象时,InlineRefCounts会变为SideTableRefCounts |
Swift增加引用计数的方法如下:
1 | // Increment the reference count. |
由于引用计数的管理有两种类型InlineRefCounts
、SideTableRefCounts
,当对象只包含strong
或unowned
引用时,使用InlineRefCounts
进行计数管理,如果对象拥有了weak
引用,则会使用SideTableRefCounts
来管理计数。所以如上增加引用计数的函数,会考虑两种情况,fast
对应InlineRefCounts
,slow
对应SideTableRefCounts
,为了避免竞态条件,使用了compare_exchange_weak
来进行赋值。
1 | HeapObject { |
接下来,看一下weak
引用自减计数的函数操作过程,函数内调用decrementWeakShouldCleanUp
来进行位数的操作,其返回一个bool
值,既当weak
、strong
、unowned
计数都变为0时,bool
值返回true
,说明可以收回内存了,既调用delete
释放内存。
1 | void decrementWeak() { |
此时,我们应该就比较清楚了,即使strong
或unowned
的计数为0,如果还存在weak
弱引用,
那么对象也不会被释放。
接下来,我们可以看一下加载弱引用的过程,Swift
通过HeapObject *swift::swift_weakTakeStrong(WeakReference *ref)
函数来实现,该函数通过间接调用,最终调用nativeTakeStrongFromBits
函数,该函数内部首先会调用getNativeOrNull
方法,该方法会从对象的side table
中查询对象的计数,当没有strong
引用时,说明该对象已经处于DEINITING
状态,函数会返回nullptr
,否则将调用tryRetain
函数来尝试strong
对象。
1 | HeapObject *nativeTakeStrongFromBits(WeakReferenceBits bits) { |
当对象实例的deinit()
方法被调用时,内部会调用swift_deallocObject
函数,而它会通过调用canBeFreedNow
函数来判断是否需要释放内存,既满足没有side table
,unowned
引用为1,strong
引用计数为0。
1 | bool canBeFreedNow() const { |
综上,如果还存在weak
弱引用,那么肯定还有side table
表,即使没有strong
引用,也不会被释放。
总结
- 弱引用指向对象实例的
side table
地址。 - 与
Objective-C
管理引用计数的方式不同,Swift
的弱引用计数与strong
计数一起管理。 Swift
针对对象的析构和对象的释放进行了解耦,一个对象被析构后,会释放它的外部资源,但是有可能不会释放对象本身的内存。- 当
Swift
对象的strong
引用计数变为0但是weak
计数大于0时,对象会被析构但是不会被释放内存。 - 当加载一个弱引用时,运行时会检查
target
的状态
,如果target
已经是僵尸对象,那么会赋空weak
引用,weak
计数减一,并返回nil
,这个过程是安全的,当weak
引用计数变为0时,僵尸对象内存将被释放。
最后总结一下Swift
与Objective-C
的区别:
Swift
不需要维护weak
列表,这可以简化代码和提升性能。- 对于
Swift
的weak
引用,实例对象会在strong
引用计数变为0时,内存依然保留,直到所有的weak
引用离开作用域。不过这个影响是很小的,因为虽然对象分配的内存依然保留,但是它所有的外部资源(如Array
、Dictionary
属性)会在strong
引用计数变为0时被释放。 - 由于
Swift
的weak
创建后,对象的引用计数管理会从InlineRefCounts
替换为SideTableRefCounts
,这也会带来一定的开销,所以如果可以,尽量使用unowned
,unowned
有点类似于Objective-C
的__unsafe_unretained
,如果unowned
指向僵尸对象后再访问,会产生未定义行为。
附录
- https://github.com/apple/swift/blob/7913e9821b814956d243e4e03cfe9ddc0e325bc2/stdlib/public/SwiftShims/HeapObject.h
- https://github.com/apple/swift/blob/860252fab41392b7de3218e58f7542cb1dc1ce16/stdlib/public/runtime/WeakReference.h
- https://github.com/apple/swift/blob/b7d78853112c1279fc7bc5b85853779040f13703/stdlib/public/SwiftShims/RefCount.h
- Swift对象生命周期状态机
- https://www.mikeash.com/pyblog/friday-qa-2015-12-11-swift-weak-references.html