Swift - Array
[TOC]
Array
是我们日常打交道非常多的一个集合,下面我们就来研究一下它。
1. Array 的创建
1.1 初始化一个数组
Swift 中有很多创建Array
的方式:
// 通过字面量初始化一个Int类型的Array
var numbers = [1, 2, 3, 4, 5]
// 通过字面量初始化一个String类型的Array
var strArray = ["element1", "element2", "element3", "element4",]
// 初始化一个Any类型的空Array,此处必须指定类型,否则会报编译错误
var emptyArray = Array<Any>()
var emptyArray1: [Any] = Array()
var emptyArray2: Array<Any> = Array()
var emptyArray3 = [Any]()
// 当然为了防止访问越界我们也可以这样初始化指定长度和初始值的数组
var array = Array(repeating: 0, count: 10)
print(array[0])
以上代码提供了数组的基本初始化方法。
- 可以通过字面量初始化
- 可以初始化空数组
- 可以初始化指定大小和初始值的数组
1.2 初始化数组的底层实现(SIL)
那么数组在底层是如何创建的呢?下面我们通过sil
代码进行查看:
var numbers = [1, 2, 3, 4, 5]
![](https://img.haomeiwen.com/i3275978/3bbeee07f46d94ca.jpg)
在sil
代码中可以看到:
- 调用了
_allocateUninitializedArray
函数初始了长度为5,类型为Int的数组 - 然后为初始的数据,也就是个元组,取出里面的两个指针赋值到%7和%8
- 根据%8的地址依次偏移,存储数组中的元素
- 最后将%7的指针存储到%3,也就是
numbers
其实这段代码很好理解,但是有一个疑问,大家都说Array
是一个值类型,那么我们刚才其实看到了alloc
的调用,那么这是为什么呢?我们一步Swift源码Swift 5.3.1
中一探究竟。
1.3 源码探索
1.3.1 _allocateUninitializedArray
首先我们搜索一下_allocateUninitializedArray
方法,可以在ArrayShared.swift
文件中找到。
![](https://img.haomeiwen.com/i3275978/80a6962578b915d9.jpg)
在源码中我们可以看到:
- 根据
count
的不同做了不同的初始化 - 如果大于0会调用
allocWithTailElems_1
初始化一个bufferObject
- 然后调用
_adoptStorage
初始一个元组 - 如果不大于0则调用
_allocateUninitialized
初始化一个元组 - 最后返回这个元组
为了更好的跟代码,我们在Debug
调试一下:
![](https://img.haomeiwen.com/i3275978/98405ddbd404970a.jpg)
![](https://img.haomeiwen.com/i3275978/b39f524141d48792.jpg)
继续跟下去就到了这里:
![](https://img.haomeiwen.com/i3275978/f0ebbed1d97dce49.jpg)
很熟悉的创建HeapObject
的代码,接下来调用的是一个Array
的函数,我我们跟一下:
1.3.2 _adoptStorage
![](https://img.haomeiwen.com/i3275978/87b6b7a2c932bdfb.jpg)
在这个方法中我们可以看到:
- 首先创建了一个
_ContiguousArrayBuffer
类型的变量 - 然后返回了一个元组
- 元组的第一个元素是一个
Array
的实例对象 - 第二个元素是第一个元素的地址
- 元组的第一个元素是一个
所以在sil
代码中,%7和%8对应的就是元组中的两个值。%7是Array
的地址,%8是原生首地址的地址。
![](https://img.haomeiwen.com/i3275978/55c628d2a5705786.jpg)
1.3.3 _ContiguousArrayBuffer
![](https://img.haomeiwen.com/i3275978/038e146265e92e9d.jpg)
_ContiguousArrayBuffer
也是个结构体。
1.4 Array 的内存布局
下面我们来看看Array
的内存布局,从_adoptStorage
方法继续向上翻就可以看到Array
这个结构体中只有一个_buffer
的成员变量。
![](https://img.haomeiwen.com/i3275978/b58a0217239169e1.jpg)
下面我回到_ContiguousArrayBuffer
中继续探索,在该结构体的最后,我们可以看到_storage
属性的定义。
![](https://img.haomeiwen.com/i3275978/658c308726481243.jpg)
在其初始化的时候也都能看到对_storage
的初始化:
![](https://img.haomeiwen.com/i3275978/1a03bf8a63f0ff16.jpg)
_storage
的类型是__ContiguousArrayStorageBase
,下面我们就研究一下__ContiguousArrayStorageBase
1.4.1 __ContiguousArrayStorageBase
__ContiguousArrayStorageBase
是一个类,定义在SwiftNativeNSArray.swift
文件中:
![](https://img.haomeiwen.com/i3275978/4fc791ea6f3f0377.jpg)
这里面有一个成员属性,回到_ContiguousArrayBuffer
中可以看到如下代码:
![](https://img.haomeiwen.com/i3275978/ce14ece03401f35f.jpg)
这里面初始化了countAndCapacity
。
1.4.2 _ArrayBody
下面我们来看看_ArrayBody
是什么,在ArrayBody.swift
文件中我们可以看到如下代码:
![](https://img.haomeiwen.com/i3275978/fa6227edb90b3530.jpg)
我们可以看到_ArrayBody
是一个结构体,里面有一个_SwiftArrayBodyStorage
类型的属性。
搜索一下_SwiftArrayBodyStorage
可以在GlobalObjects.h
文件中找到其定义:
struct _SwiftArrayBodyStorage {
__swift_intptr_t count;
__swift_uintptr_t _capacityAndFlags;
};
可以看到_SwiftArrayBodyStorage
是一个结构体,里面定义了两个成员变量。
1.4.3 内存布局总结
经过上面的一番探索我们总结一下Array
的内存布局:
struct Array
----->struct _ContiguousArrayBuffer
----->class __ContiguousArrayStorageBase
----->包含一个属性类型是struct ArrayBody
----->包含一个属性struct _SwiftArrayBodyStorage
----->包含两个属性count和_capacityAndFlags
这里最主要的就是__ContigousArrayStorageBase
,因为前面都是值类型。
![](https://img.haomeiwen.com/i3275978/14ea4771828ffd51.jpg)
1.4.4 通过lldb验证以上结论
下面我们通过lldb
打印一下:
![](https://img.haomeiwen.com/i3275978/f210bae2c8b20821.jpg)
1.4.5 _capacityAndFlags
乍一看_capacityAndFlags
的值好像是count
的两倍,下面我们通过源码来看一看:
@inlinable
internal init(
count: Int, capacity: Int, elementTypeIsBridgedVerbatim: Bool = false
) {
_internalInvariant(count >= 0)
_internalInvariant(capacity >= 0)
_storage = _SwiftArrayBodyStorage(
count: count,
_capacityAndFlags:
(UInt(truncatingIfNeeded: capacity) &<< 1) |
(elementTypeIsBridgedVerbatim ? 1 : 0))
}
以上是_ArrayBody
的初始化代码,我们可以看到对_capacityAndFlags
赋值的时候是将capacity
的值左移1位 在或(|)上(elementTypeIsBridgedVerbatim ? 1 : 0)
。
/// Is the Element type bitwise-compatible with some Objective-C
/// class? The answer is---in principle---statically-knowable, but
/// I don't expect to be able to get this information to the
/// optimizer before 1.0 ships, so we store it in a bit here to
/// avoid the cost of calls into the runtime that compute the
/// answer.
@inlinable
internal var elementTypeIsBridgedVerbatim: Bool {
get {
return (_capacityAndFlags & 0x1) != 0
}
set {
_capacityAndFlags
= newValue ? _capacityAndFlags | 1 : _capacityAndFlags & ~1
}
}
elementTypeIsBridgedVerbatim
是一个计算属性,看看是否当兼容Objective-C
的时候是1,否则是0,这里我们并没有兼容OC
所以使用0。
那么调用的时候传值是怎么传的呢?这个属性是__ContiguousArrayStorageBase
中的countAndCapacity
,初始化是在_ContiguousArrayBuffer
中初始化的:
// We can initialize by assignment because _ArrayBody is a trivial type,
// i.e. contains no references.
_storage.countAndCapacity = _ArrayBody(
count: count,
capacity: capacity,
elementTypeIsBridgedVerbatim: verbatim)
}
这里的capacity
也是调用处传过来的,调用点也在_ContiguousArrayBuffer
中:
@inlinable
internal init(count: Int, storage: _ContiguousArrayStorage<Element>) {
_storage = storage
_initStorageHeader(count: count, capacity: count)
}
可以看到这个capacity
的值就是count
,所以说_capacityAndFlags
也是个按位存储的变量,通过计算得出capacity
,并不是这存储capacity
,根据变量的名称我们也可以知道该变量并不是只存储一个值的。
1.4.6 metadata
在lldb
调试的时候,我们发现存储metadata
的地址很大,那么怎么怎么回事呢?我们通过cat address
命令查看一下(需要安装插件),Xcode默认不带这个命令:
![](https://img.haomeiwen.com/i3275978/869ba7d9f67b17c9.jpg)
那么这个InitialAllocationPool
是什么呢?我们去源码中看看,可以在Metadata.cpp
中找到:
![](https://img.haomeiwen.com/i3275978/13d2700a0ccb1e26.jpg)
下面我们测试一下,初始化一个数组:
![](https://img.haomeiwen.com/i3275978/f0904fe7d822bfbb.jpg)
可以看到调用堆栈确实调用了。但其实不同的创建方式,这个位置的内容是不一样的:
![](https://img.haomeiwen.com/i3275978/786bbf657541a8e4.jpg)
![](https://img.haomeiwen.com/i3275978/162dbec221828e29.jpg)
其实这么大的地址就是静态变量(这里也应该是Metadata),尝试了几次,如果初始化一个空数组就是_swiftEmptyArrayStorage
,有值的时候就是InitialAllocationPool
,并没有过多测试,不知道具体准不准确。
2. 数组的拼接
2.1 数组的扩容
一个数组初始完成后,我们会需要往里面添加数据,也就是append
操作,涉及到append
就存在扩容的问题,那么Swift中的Array
是如何扩容的呢?
我们直接找到append
方法:
![](https://img.haomeiwen.com/i3275978/2530c81ed6e362ef.jpg)
我们可以看到,扩容时调用的_reserveCapacityAssumingUniqueBuffer
方法,如果全局搜索会有好几个_reserveCapacityAssumingUniqueBuffer
方法,经过测试实际是调用的Array.Swift
文件中的。
![](https://img.haomeiwen.com/i3275978/4565bd77322260bc.jpg)
这里也很简单,判断oldCount + 1 > _buffer.capacity
就扩容。
接下来我们找到_createNewBuffer
方法,也在Array.Swift
文件中。
![](https://img.haomeiwen.com/i3275978/e1eb0eaca1f66258.jpg)
扩容的时候调用的是_growArrayCapacity(oldCapacity:minimumCapacity: growForAppend:)
方法,在ArrayShared.swift
文件中:
@inlinable
internal func _growArrayCapacity(_ capacity: Int) -> Int {
return capacity * 2
}
@_alwaysEmitIntoClient
internal func _growArrayCapacity(
oldCapacity: Int, minimumCapacity: Int, growForAppend: Bool
) -> Int {
if growForAppend {
if oldCapacity < minimumCapacity {
// When appending to an array, grow exponentially.
return Swift.max(minimumCapacity, _growArrayCapacity(oldCapacity))
}
return oldCapacity
}
// If not for append, just use the specified capacity, ignoring oldCapacity.
// This means that we "shrink" the buffer in case minimumCapacity is less
// than oldCapacity.
return minimumCapacity
}
_growArrayCapacity(oldCapacity:minimumCapacity: growForAppend:)
方法中:
- 首先会判断使其扩容的是不是
append
方法 - 如果是判断
oldCapacity
是否小于minimumCapacity
- 如果不是返回
oldCapacity
- 如果是取出
oldCapacity
的两倍和minimumCapacity
中的大值进行返回
这个minimumCapacity
的值在这条调用堆栈上是oldCount + 1
,growForAppend
的值为true
可以在_reserveCapacityAssumingUniqueBuffer
方法中看到。
所以基本可以认为数组的扩容时之前的两倍。
扩容时对于就元素的的处理,如果原始缓冲区是唯一的,我们可以移动元素,而不是复制。如果不是就需要复制元素。
回到我们的代码中,通过lldb
调试一下:
![](https://img.haomeiwen.com/i3275978/46a9486702800eba.jpg)
append
后数组的地址就已经改变了。
读取两段地址:
![](https://img.haomeiwen.com/i3275978/4a249af4c2457d05.jpg)
_capacityAndFlags
的值分别是0xa
和0x14
。
![](https://img.haomeiwen.com/i3275978/87e86744bb28855d.jpg)
右移1位分别是5和10。
2.1 Array的拷贝
首先我们看看下面这段代码:
var numbers = [1, 2, 3, 4, 5]
var tmpArray = numbers
![](https://img.haomeiwen.com/i3275978/3dac364faad2d764.jpg)
我们看到这里只调用了copy_addr
,下面我们看看copy_addr
的作用是什么:
![](https://img.haomeiwen.com/i3275978/8e478bf3822dba9f.jpg)
可以看到这里面就是一个值的拷贝,在这里就是把numbers
里面存储的内容(这里是一个指针)拷贝一份到tmpArray
中。
那么这里就存在一个问题了,Array
在底层是一个结构体,在Swift中结构体是一个值类型,那么也就意味着当前我们改变numbers
或者tmpArray
不会影响另外一个里面的值。
var numbers = [1, 2, 3, 4, 5]
var tmpArray = numbers
tmpArray[0] = 2
print(numbers)
print(tmpArray)
![](https://img.haomeiwen.com/i3275978/5b83e14e87d159b1.jpg)
我们可以看到确实没有影响到另一个。但是numbers
里面存储的是地址,当初我们在探索值类型和引用类型的时候,当值类型内嵌套引用类型的时候,改变值类型中的引用类型的值,会影响到拷贝的另一个值类型,这点就不一样了,还是上面的代码,我们通过lldb
调试看一看:
![](https://img.haomeiwen.com/i3275978/b9c60b20ba34d957.jpg)
我们可以看到给tmpArray
修改值后,其存储的值已经改变了,那么修改的时候发生可什么呢?下面我们在看看Sil
代码:
![](https://img.haomeiwen.com/i3275978/a27e50dfbcd7db78.jpg)
下面我们到源码中看看:
![](https://img.haomeiwen.com/i3275978/b7035acc007e16f5.jpg)
在Array.swift
中我们可以看到subscript
方法有get
和_modify
两个方法。在_modify
里面会调用_makeMutableAndUnique
方法,下面我们就看看这个方法:
![](https://img.haomeiwen.com/i3275978/f21954b0ac808a29.jpg)
这里会调用_createNewBuffer
方法,这就是我们在数组拼接的时候介绍的这个方法:
![](https://img.haomeiwen.com/i3275978/84044725f42ca169.jpg)
这里会创建一个新的buffer
赋值给_buffer
3. 总结
至此我们对 Swift 中数组的介绍就结束,下面总结一下:
- Swift中的数组本质是个结构体,里面的结构如下图:
![](https://img.haomeiwen.com/i3275978/01d07e87065966b3.jpg)
- 因为是结构体,所以我们认为数组是值类型
- 数组空间的扩容时原来容量的两倍,目前没有发现有最大容量限制
- 数组的拷贝是写时复制机制,只有修改的时候才会修改
- 首先拷贝的时候是拷贝数组的地址
- 当需要修改任一指向同一空间的数组的时候创建一个新的
buffer
来存储里面的元素
网友评论