内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

Swift4弱引用实现

2018-08-02 21:04 出处:清屏网 人气: 评论(0

Swift 开源不久我就写了篇关于弱引用实现的 文章 。时移势易,Swift 4 中的弱引用实现已经与旧文不一致了。应 Guillaume Lessard 建议,今天我将会介绍新版实现,并比较其与老版的区别。

旧实现

考虑到有些人可能已经忘记了旧实现并且不愿重看前面的文章,下面我们就一起简要的回顾下之前的实现方式。

在旧实现中,Swift 对象有两个引用计数:强引用计数和弱引用计数。当强引用计数为 0 而弱引用计数不为 0 时,对象会被销毁,但是内存并不会被立即释放。内存中会保留弱引用指向的僵尸对象。

在加载弱引用时,运行时会对引用对象进行检查。如果是僵尸对象,则会对弱引用计数进行递减操作。一旦弱引用计数为 0,对象内存将会被释放。换句话说,僵尸对象的所有弱引用被加载访问后僵尸对象才会真正被清空。

虽然我喜欢该实现的简单性,但它有一些缺陷。其中一个就是,僵尸对象可能会长时间停留在内存中。对于那些拥有很多实例的类(因为它们包含许多属性,或使用类似 ManagedBuffer 分配了内联的额外内存),这会造成严重的内存浪费。

另外,在写完旧文后我还发现:对于并发读取,该实现是非线程安全的。虽然已经有补丁修复了这个问题,但从相关讨论可以看出,开发者希望找到一个更好的实现方式,避免出现类似问题。

对象数据

Swift 中的 “对象” 其实是由一组数据构成。

首先,最容易想到的就是源码中声明的那些可直接访问的存储属性。

其次就是对象的类信息。该信息主要被用于动态派发和 type(of: ) 内置函数。虽然动态派发和 type(of: ) 内置函数从侧面暗示了它的存在,但是实际上该信息大多是被隐藏的。

第三种就是各种引用计数信息。除非你进行一些非常规操作,例如,读取对象的原始内存或说服编译器让你调用 CFGetRetainCount,否则这些信息对你来说是完全透明不可见的。

第四种就是 Objective-C 运行时存储的辅助信息,例如 Objective-C 弱引用列表(Objective-C 的弱引用实现是通过单独追踪每个弱引用)和关联对象。

那么这些信息最终都存储在哪里呢?

在 Objective-C 中,类信息和存储属性(例如,实例变量)内联在对象内存中。其中类信息位于指针所在第一块内存,其后才是实例变量。辅助类信息则保存在外部表中。当你需要操作关联对象时,运行时机制会使用内存地址去一个大的哈希表中查找它。为了实现多线程安全,该表在操作时会加锁,所以存在一定程度访问速度问题。引用计数的保存位置,则取决于具体操作系统版本和 CPU 架构,它有时位于对象内存中,而有时又存储在外部表中。

在 Swift 旧有实现中,类信息,引用计数和存储属性全部内联在对象内存中。而辅助信息则依旧存储在单独的外部表中。

下面我们不妨将具体实现代码先放一边,仔细思考下:理论上应该如何存储这些信息呢?

每种存储方案都有利弊。将数据存储在对象内存中虽然能提高访问速度,但是会让内存空间变得吃紧。与之相对,外部存储方案则是通过牺牲速度来换空间。

Objective-C 传统存储方案不将对象引用计数保存在内存中,部分原因正是基于此。因为在 Objective-C 引入引用计数概念时,设备的性能远不如现在,而且内存容量也极为有限。Objective-C 程序中大多数对象只有一个所有者,即引用计数为 1 。此时在对象内存中腾出 4 个字节空间存储该引用计数 1 是很浪费的。而外部表方案中,数值 1 可以通过缺省默认值方式表示从而减少内存消耗。

每次进行动态方法派发时都需要对象的类信息,所有作为最常用信息,类信息应该直接保存在内存中,存在外部表中是不合适的。

而实例变量这类存储属性在编译期就确定了,而且有现实的访问速度需求,所以存在对象内存中也是最合理的设计。另外,当对象没有存储属性时,系统不会为其分配内存空间也就不存在浪费问题。

每个对象都需要保留引用计数。虽然不是每个对象的引用计数都为 1,但它依旧是一个相对常见的情形,加上现在内存足够,它可以直接保存在内存中。

大多数对象都不会有弱引用或关联对象数据,所有它们应该保存在外部以期节约内存空间。

对于那些有弱引用或关联对象数据的对象来说,访问速度确实不够快但这是合理的权衡结果。那么问题来了,该旧实现有没有改进空间和可行方法呢?

Side Tables

在 Swift 弱引用的新版实现代码中,引入了 side tables 概念来改进上诉缺陷。

Side table 本质就是用于保存额外信息的单独内存块,并且它还是可选的。也就是说,对于那些无需保存额外信息的对象来说并没有多余开销。

每个对象都有一个指向其对应 side table 的指针,而 side table 也有一个指针指向该对象。另外,side table 可以存储关联的对象数据等其他信息。

为了避免 side table 带来的 8 字节空间开销,Swift 做了一个漂亮的优化。通常内存中的第一个字(Word)是类信息,第二个字则是引用计数。当对象存在 side table 需求时,第二个字将保存指向 side table 的指针。因为引用计数是必要信息,所以此时会将引用计数保存到 side table 中。至于程序运行时到底是哪种情形,则由该块内存中的一个标志位进行区分。

通过将弱引用从指向对象本身改为指向 side table ,Swift 得以在保留原有引用计数设计的同时修复了旧设计中的缺陷。

因为 side table 比较小并且弱引用不再指向对象本身,这样之前大型僵尸对象的内存空间将能立即释放从而降低了内存浪费。同时该实现也让线程安全问题变得更易解决:不再需要提前将弱引用置空。因为 side table 比较小,指向它的弱引用可以持续保留,直到这些引用自身被覆写或销毁。

这里需要提醒一下,当前 side table 实现中只保存引用计数和指向原始对象的指针。类似保存关联对象等用途只是一个猜想和假设。因为 Swift 还没有内建关联对象功能,而 Objective-C API 仍在使用全局表。

该技术还有不少潜力可挖,也许在不久的将来能看到其应用在关联对象等内容上。我希望它能为类拓展中的存储属性和其他有趣的功能打开一扇新窗。

代码

因为 Swift 已经开源,所有相关代码都能直接访问。

关于 side table 的大部分代码都在 stdlib/public/SwiftShims/RefCount.h

高层级的弱引用 API 以及相关注释都在 swift/stdlib/public/runtime/WeakReference.h

更多关于堆对象的实现和注释在 stdlib/public/runtime/HeapObject.cpp

上述链接其实带着版本信息,以便后面的读者也能找到本文内容当时的上下文。如果你想看最新的实现代码,你在点击链接后切换到 master 分支即可。

总结

弱引用是一个重要的语言特性。Swift 最初的实现方式非常聪明,也有一些不错的特性,但是同时也存在一些问题。通过引入 side table,Swift 开发工程师在保留原有特点的同时还解决了这些缺陷。Side table 的实现也为将来更多新特性创造了更多可能性。

分享给小伙伴们:
本文标签: Swift弱引用

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号