Swift数组扩容原理
#结论
首先把结论写在文章开头,因为接下来的分析会有些啰嗦、复杂,如果不愿意深究的话只要记住Swift中数组扩容的原理是:
Swift2中创建一个空数组,默认容量为2,当长度和容量相等,且还需要再添加元素时,创建一个 复制代码
两倍长度于旧数组的新数组,把旧数组的元素拷贝过来,再把元素插入到新数组中。 复制代码
#引子
下面这段代码希望通过多线程技术,加速向数组中添加数字这个过程,我们来看看它有什么问题:
var array: [Int] = []
let concurrentQueue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)for i in 1...10000 {
dispatch_async(concurrentQueue, { () -> Void in
array.append(i)
})
}
复制代码
代码很简短,看上去问题不大。不过如果你运行完这段代码而且程序没有崩溃,我强烈建议买一份彩票,因为你的运气已经好到逆天了。
通常情况下,你会遇到这样的报错
fatal error: UnsafeMutablePointer.destroy with negative count
#Append方法实现
程序会断在array.append(i)
这一行。也就是append
方法出了问题。我们知道Swift里的数组不像C语言,不需要提前定义好长度,更像是C++的vector
和OC的NSMutableArray
。
所以,会不会是数组的可变性,导致了append
方法是线程不安全的呢,带着这样的猜想,我们来研究一下Swift是如何实现Append方法的。
Swift已经开源了,github上相关源码已经可以下载。
虽然明知道有些文件夹不会包含append方法的实现源码,但真想找到也不容易。如果你试着搜索"append"的话,相关文件非常多,因为"append"本身就是一个非常常用的单词。我采取的办法是搜索完整的函数定义,而函数定义是我们很容易看到的。
当我们搜索"mutating func append(newElement: Element)"后,就只有六个相关文件了。如图所示:
前三个文件无法直接打开,暂时先不管。其实第三个一看也知道是单元测试文件。第六个是字符串,也不是我们感兴趣的。所以我们依次打开"ArrayType.swift"和"RangeReplaceableCollectionType.swift"这两个文件。
提示:这两个文件的目录都是"/swift/stdlib/public/core" 复制代码
遗憾的是,ArrayType.swift文件中没有找到相关函数,RangeReplaceableCollectionType.swift文件倒是有一个append
方法,不过参数类型对不上。于是我想到第一个文件——"Arrays.swift.gyb",去掉gyb后缀后果然可以打开了。并且成功的找到了我们感兴趣的append
方法:
public mutating func append(newElement: Element) {
_makeUniqueAndReserveCapacityIfNotUnique()
let oldCount = _getCount()
_reserveCapacityAssumingUniqueBuffer(oldCount)
_appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement)
}
复制代码
#源码分析
代码不长,我们逐行看一下
- 第一行的函数,看名字表示,如果这个数组不是惟一的,就把它变成惟一的,而且保留数组容量。在Xcode里面搜索一下这个函数:
internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() {
if _slowPath(!_buffer.isMutableAndUniquelyReferenced()) {
_copyToNewBuffer(_buffer.count)
}
}
复制代码
这个函数会进行一个判断判断——如果数组不是被唯一引用的,就会复制一份新的。这其实就是“写时赋值(copy-on-write)”技术。如果你想了解它的具体使用,可以参考我的这篇文章——《第二章——集合(数组与可变性)》
不过对于文章开头那个例子的数组来说,它肯定是唯一引用的。所以_copyToNewBuffer
函数不会调用,我们先记下这个方法。然后回到append
方法继续研究。
第二行用一个变量
oldCount
保存数组当前长度。第三行的函数表示,在假设当前数组是唯一引用的前提下,保留数组容量。之所以做出这样的假设,是因为此前已经调用过
_makeUniqueAndReserveCapacityIfNotUnique
方法,即使这个数组不是唯一引用,也被复制了一份新的。我们来看看_reserveCapacityAssumingUniqueBuffer
方法的实现:
internal mutating func _reserveCapacityAssumingUniqueBuffer(oldCount : Int) {
_sanityCheck(_buffer.isMutableAndUniquelyReferenced())if _slowPath(oldCount + 1 > _buffer.capacity) {
_copyToNewBuffer(oldCount)
}
}
复制代码
第一行有一个_sanityCheck
来判断数组是否可变且唯一引用。"sanity"说明这个判断是符合常理的,虽然它很有可能并没有效果,但也是为了确保万无一失。
下面还有一个判断,检查当前数组长度加一后是否大于数组容量。如果判断成立,说明oldCount == _buffer.capacity
,在实际编程中,就意味着数组需要扩容了。可以看到又会执行刚刚提到过的_copyToNewBuffer
函数。我们还是先把这个函数放一放,接着往后看。
- 最后一行的函数表示,假设数组是唯一引用的,且数组容量也设置正确,把新的元素添加到数组中。这其实是真正执行了“append”操作的地方。它的实现如下:
internal mutating func _appendElementAssumeUniqueAndCapacity(
oldCount : Int,
newElement : Element) {_sanityCheck(_buffer.isMutableAndUniquelyReferenced())
_sanityCheck(_buffer.capacity >= _buffer.count + 1)_buffer.count = oldCount + 1
(_buffer.firstElementAddress + oldCount).initialize(newElement)
}
复制代码
首先是两个基本判断,然后把count
属性加一,最后获取到将要添加的位置的地址,用一个新的值初始化它。
OK,append
方法的结构基本上了解了,首先会保证数组是唯一引用的,然后处理数组的容量问题,最后把待插入的元素放到指定位置上。其中最关键,也是目前还没有彻底明白的一步,就是之前所说的_copyToNewBuffer
函数了
#copyToNewBuffer
先来看看copyToNewBuffer
函数的实现:
internal mutating func _copyToNewBuffer(oldCount: Int) {
let newCount = oldCount + 1
var newBuffer = Optional(
_forceCreateUniqueMutableBuffer(
&_buffer, countForNewBuffer: oldCount, minNewCapacity: newCount))
_arrayOutOfPlaceUpdate(
&_buffer, &newBuffer, oldCount, 0, _IgnorePointer())
}
复制代码
这个方法又分为两步,_forceCreateUniqueMutableBuffer
和_arrayOutOfPlaceUpdate
。前者实现了新存储区域的创建,而后者完成了数据的复制工作。
为了简单起见,我们先看看_arrayOutOfPlaceUpdate
函数,这个函数的实现太长了,不过好在有注释:
/// Initialize the elements of dest by copying the first headCount
/// items from source, calling initializeNewElements on the next
/// uninitialized element, and finally by copying the last N items
/// from source into the N remaining uninitialized elements of dest.
///
/// As an optimization, may move elements out of source rather than
/// copying when it isUniquelyReferenced.
复制代码
大意是说,源数组中已存在的元素会被复制到目标数组中,如果新数组比较长,空缺部分会调用initializeNewElements
方法来初始化。为了优化性能,被唯一引用的元素可能会直接从源数组移到新数组而不是复制。其实就是换了一个指针指向那个元素,从而避免了复制。
接下来我们再研究一下比较关键的_forceCreateUniqueMutableBuffer
部分,也就是数组是怎样扩容的:
@inline(never)
func _forceCreateUniqueMutableBuffer<_Buffer : _ArrayBufferType>(
inout source: _Buffer, countForNewBuffer: Int, minNewCapacity: Int
) -> _ContiguousArrayBuffer<_Buffer.Element> {//其实什么也没干,多加了一个参数就转发给 _forceCreateUniqueMutableBufferImpl了
return _forceCreateUniqueMutableBufferImpl(
&source, countForBuffer: countForNewBuffer, minNewCapacity: minNewCapacity,
requiredCapacity: minNewCapacity)
}internal func _forceCreateUniqueMutableBufferImpl<_Buffer : _ArrayBufferType>(
inout source: _Buffer, countForBuffer: Int, minNewCapacity: Int,
requiredCapacity: Int
) -> _ContiguousArrayBuffer<_Buffer.Element> {
_sanityCheck(countForBuffer >= 0)
_sanityCheck(requiredCapacity >= countForBuffer)
_sanityCheck(minNewCapacity >= countForBuffer)let minimumCapacity = max(
requiredCapacity, minNewCapacity > source.capacity
? _growArrayCapacity(source.capacity) : source.capacity)return _ContiguousArrayBuffer(
count: countForBuffer, minimumCapacity: minimumCapacity)
}
复制代码
_forceCreateUniqueMutableBufferImpl
函数刚开始的三个检查读者可以自行理解,关键部分在于minimumCapacity
的计算。因为它会作为容量参数被传到用于创建新的Buffer的函数中。
这个函数有四个参数,第一个参数buffer可以理解为数组中真正用于数据存放的那个部分。对于最后两个参数,意思有点像,我们不妨考虑一个实际的、需要进行数组扩容的情况,向一个容量为3,长度为3的数组新增一个元素1,此时函数的调用顺序如下:
append(1)
_reserveCapacityAssumingUniqueBuffer(3)
_copyToNewBuffer(3)
_forceCreateUniqueMutableBuffer(&_buffer, countForNewBuffer: 3, minNewCapacity: 4)
_forceCreateUniqueMutableBufferImpl(&_buffer, countForBuffer: 3, minNewCapacity: 4, requiredCapacity: 4)
还记得_copyToNewBuffer()
方法里的let newCount = oldCount + 1
么,所以oldCount(=3)
作为minNewCapacity
,而newCount(=4)
作为requiredCapacity
参数被传入_forceCreateUniqueMutableBufferImpl
方法。
此时,minimumCapacity
的计算,其实就是以下表达式的值:
max(4, 4 > 3 ? _growArrayCapacity(3) : 3)
复制代码
我们知道,如果数组需要扩容,source.capacity
总是等于minNewCapacity
的。也就是说上式可以写为:
max(length+1, length+1 > length ? _growArrayCapacity(length) : length)//等价于
max(length+1, _growArrayCapacity(length))
复制代码
可以看到_growArrayCapacity
返回值是传入参数的两倍:
@warn_unused_result
internal func _growArrayCapacity(capacity: Int) -> Int {
return capacity * 2
}
复制代码
所以minimumCapacity
= max(length+1, 2 * length)
= 2 * length
。也就是新扩容的数组长度其实翻倍了。
#线程安全
现在我们可以理解为什么append
方法不是线程安全的了。如果在某一个线程中插入新元素,导致了数组扩容,那么Swift会创建一个新的数组(意味着地址完全不同)。然后ARC会为我们自动释放旧的数组,但这时候可能另一个线程还在访问旧的数组对象。
#验证
说了这么多,我们来证明一下Swift数组扩容的工作原理:
let semaphore = dispatch_semaphore_create(1)
var array: [Int] = []
for i in 1...100000 {
array.append(i)
let arrayPtr = UnsafeMutableBufferPointer<Int>(start: &array, count: array.count)
print(arrayPtr)
}
复制代码
运行结果如下,可以验证文章开头的结论:“初始容量为2,扩容时容量翻倍”
Swift数组扩容原理相关推荐
- java数组可扩展_[转载]Java数组扩容算法及Java对它的应用
Java数组扩容的原理 1)Java数组对象的大小是固定不变的,数组对象是不可扩容的. 2)利用数组复制方法可以变通的实现数组扩容. 3)System.arraycopy()可以复制数组. 4)Arr ...
- arraylist扩容是创建新数组吗 java_Java编程之数组扩容
一.背景 数组在实际的系统开发中用的越来越少了,我们只有在阅读某些开源项目时才会看到数组的使用.在Java中,数组与List.Set.Map等集合类相比,后者使用起来方便,但是在基本数据类型处理方面, ...
- Java数组扩容算法及Java对它的应用
1)Java数组对象的大小是固定不变的,数组对象是不可扩容的.利用数组复制方法可以变通的实现数组扩容.System.arraycopy()可以复制数组.Arrays.copyOf()可以简便的创建数组 ...
- [转载]Java数组扩容算法及Java对它的应用
原文链接:http://www.cnblogs.com/gw811/archive/2012/10/07/2714252.html Java数组扩容的原理 1)Java数组对象的大小是固定不变的,数组 ...
- HashMap面试题 头插法、尾插法、hash冲突、数组扩容、ConcurrentHashMap
文章目录 HashMap 的数据结构? HashMap 的工作原理? HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题? 数组扩容的 ...
- ArrayList扩容原理
今天带来的下饭菜是ArrayList的扩容源码解读. 相信大家对这盘菜都不陌生,我们经常使用它来定义一个集合,无论日常开发还是自己学习使用的频率是相当的高. 而且大家也都一定知道ArrayList集合 ...
- HashMap扩容原理
本篇文章分别讲解JDK1.7和JDK1.8下的HashMap底层实现原理 文章目录 一.什么是HashMap? 二.为什么要使用HashMap? 三.HashMap扩容为什么总是2的次幂? 四.JDk ...
- HashMap之扩容原理
一.什么是HashMap? HashMap 数据结构为 数组+链表(JDk1.7),JDK1.8中增加了红黑树,其中:链表的节点存储的是一个 Entry 对象,每个Entry 对象存储四个属性(has ...
- ArrayList 扩容详解,扩容原理
ArrayList 扩容详解,扩容原理 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长. ArrayList不是线程安全的,只能用在单线程环境下. 实现了Serializable ...
最新文章
- 【计算机网络】应用层 : 总结 ( 网络应用模型 C/S P2P | 域名解析 DNS | 文件传输协议 FTP | 电子邮件 | 万维网 与 HTTP ) ★★★
- 暗时间(一)设计你自己的进度条
- JavaScript No Overloading 函数无重载之说
- java通过url抓取网页数据-----正则表达式
- matlab %3c handle,volume browser (updated).htm 源代码在线查看 - Matlab显式三维地震数据的源代码 资源下载 虫虫电子下载站...
- Linux下 -bash: php: command not found 命令找不到
- 金明的预算方案(洛谷-P1064)
- 【docker系列】docker基本常用命令
- Mysql插入中文的字段内容时乱码的解决方法
- 概率论在实际生活的例子_「收藏」不确定度是什么?两个例子让你清清楚楚
- 二十一个心理学效应 笔记
- 笔记本连接android手机屏幕,实现手机、电脑屏幕共享的7个步骤
- 计算机如何认硬盘,电脑怎样识别大容量的硬盘?
- phpStudy配置站点解决各种不能访问问题(本地可www.xx.com访问)
- OGG抽取进程异常一例
- 南京湖南路学计算机哪家好,南京“最好吃餐厅排行榜”,去过8个,你就是超级美食达人......
- 2000-2020上市公司全要素生产率LP方法含原始数据和Stata代码
- 入门嵌入式,开发板应该怎么选?
- C语言可以敲哪些小游戏,C语言可以写哪些小游戏?
- 【capture2hls】
热门文章
- 效率 qt_Qt开发之Go篇(三)
- oracle空格太多,Oracle Sql字符串多余空格处理方法初记
- python列表做参数传值_python不定参数传值怎么做-问答-阿里云开发者社区-阿里云...
- mysql导入数据权限_mysql5.7导入数据的权限问题
- jdbc mysql 实例名_JDBC连接自定义sqlserver数据库实例名(多个实例)
- 智能车竞赛技术报告 | 单车拉力组-大连海事大学-同舟拾队
- 基于MEGA8的声音CLICK模块
- 第十五届全国大学生智能汽车竞赛确定各分赛区总决赛名单数量分配草案
- 2020春季学期作业提交统计处理
- c语言排班系统设计报告,C语言课程设计关于排班系统的一些问题