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