Swift - Array

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]
-w999

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文件中找到。

-w623

在源码中我们可以看到:

  • 根据count的不同做了不同的初始化
  • 如果大于0会调用allocWithTailElems_1初始化一个bufferObject
  • 然后调用_adoptStorage初始一个元组
  • 如果不大于0则调用_allocateUninitialized初始化一个元组
  • 最后返回这个元组

为了更好的跟代码,我们在Debug调试一下:

-w768
-w901

继续跟下去就到了这里:

-w1229

很熟悉的创建HeapObject的代码,接下来调用的是一个Array的函数,我我们跟一下:

1.3.2 _adoptStorage

-w604

在这个方法中我们可以看到:

  • 首先创建了一个_ContiguousArrayBuffer类型的变量
  • 然后返回了一个元组
    • 元组的第一个元素是一个Array的实例对象
    • 第二个元素是第一个元素的地址

所以在sil代码中,%7和%8对应的就是元组中的两个值。%7是Array的地址,%8是原生首地址的地址。

-w679

1.3.3 _ContiguousArrayBuffer

-w589

_ContiguousArrayBuffer也是个结构体。

1.4 Array 的内存布局

下面我们来看看Array的内存布局,从_adoptStorage方法继续向上翻就可以看到Array这个结构体中只有一个_buffer的成员变量。

-w641

下面我回到_ContiguousArrayBuffer中继续探索,在该结构体的最后,我们可以看到_storage属性的定义。

image

在其初始化的时候也都能看到对_storage的初始化:

image

_storage的类型是__ContiguousArrayStorageBase,下面我们就研究一下__ContiguousArrayStorageBase

1.4.1 __ContiguousArrayStorageBase

__ContiguousArrayStorageBase是一个类,定义在SwiftNativeNSArray.swift文件中:

-w876

这里面有一个成员属性,回到_ContiguousArrayBuffer中可以看到如下代码:

-w678

这里面初始化了countAndCapacity

1.4.2 _ArrayBody

下面我们来看看_ArrayBody是什么,在ArrayBody.swift文件中我们可以看到如下代码:

-w753

我们可以看到_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,因为前面都是值类型。

image

1.4.4 通过lldb验证以上结论

下面我们通过lldb打印一下:

-w619

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默认不带这个命令:

image

那么这个InitialAllocationPool是什么呢?我们去源码中看看,可以在Metadata.cpp中找到:

image

下面我们测试一下,初始化一个数组:


image

可以看到调用堆栈确实调用了。但其实不同的创建方式,这个位置的内容是不一样的:


image
image

其实这么大的地址就是静态变量(这里也应该是Metadata),尝试了几次,如果初始化一个空数组就是_swiftEmptyArrayStorage,有值的时候就是InitialAllocationPool,并没有过多测试,不知道具体准不准确。

2. 数组的拼接

2.1 数组的扩容

一个数组初始完成后,我们会需要往里面添加数据,也就是append操作,涉及到append就存在扩容的问题,那么Swift中的Array是如何扩容的呢?

我们直接找到append方法:

-w684

我们可以看到,扩容时调用的_reserveCapacityAssumingUniqueBuffer方法,如果全局搜索会有好几个_reserveCapacityAssumingUniqueBuffer方法,经过测试实际是调用的Array.Swift文件中的。

-w699

这里也很简单,判断oldCount + 1 > _buffer.capacity就扩容。

接下来我们找到_createNewBuffer方法,也在Array.Swift文件中。

-w901

扩容的时候调用的是_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 + 1growForAppend的值为true可以在_reserveCapacityAssumingUniqueBuffer方法中看到。

所以基本可以认为数组的扩容时之前的两倍。

扩容时对于就元素的的处理,如果原始缓冲区是唯一的,我们可以移动元素,而不是复制。如果不是就需要复制元素。

回到我们的代码中,通过lldb调试一下:

-w660

append后数组的地址就已经改变了。

读取两段地址:


image

_capacityAndFlags的值分别是0xa0x14

-w356

右移1位分别是5和10。

2.1 Array的拷贝

首先我们看看下面这段代码:

var numbers = [1, 2, 3, 4, 5]
var tmpArray = numbers
-w638

我们看到这里只调用了copy_addr,下面我们看看copy_addr的作用是什么:

-w567

可以看到这里面就是一个值的拷贝,在这里就是把numbers里面存储的内容(这里是一个指针)拷贝一份到tmpArray中。

那么这里就存在一个问题了,Array在底层是一个结构体,在Swift中结构体是一个值类型,那么也就意味着当前我们改变numbers或者tmpArray不会影响另外一个里面的值。

var numbers = [1, 2, 3, 4, 5]

var tmpArray = numbers
tmpArray[0] = 2

print(numbers)
print(tmpArray)
-w490

我们可以看到确实没有影响到另一个。但是numbers里面存储的是地址,当初我们在探索值类型和引用类型的时候,当值类型内嵌套引用类型的时候,改变值类型中的引用类型的值,会影响到拷贝的另一个值类型,这点就不一样了,还是上面的代码,我们通过lldb调试看一看:

-w678

我们可以看到给tmpArray修改值后,其存储的值已经改变了,那么修改的时候发生可什么呢?下面我们在看看Sil代码:

-w717

下面我们到源码中看看:

-w766

Array.swift中我们可以看到subscript方法有get_modify两个方法。在_modify里面会调用_makeMutableAndUnique方法,下面我们就看看这个方法:

-w720

这里会调用_createNewBuffer方法,这就是我们在数组拼接的时候介绍的这个方法:

-w903

这里会创建一个新的buffer赋值给_buffer

3. 总结

至此我们对 Swift 中数组的介绍就结束,下面总结一下:

  • Swift中的数组本质是个结构体,里面的结构如下图:
image
  • 因为是结构体,所以我们认为数组是值类型
  • 数组空间的扩容时原来容量的两倍,目前没有发现有最大容量限制
  • 数组的拷贝是写时复制机制,只有修改的时候才会修改
    • 首先拷贝的时候是拷贝数组的地址
    • 当需要修改任一指向同一空间的数组的时候创建一个新的buffer来存储里面的元素
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容

  • Array lazy 当我们调用的时候才会触发lazy Array的创建 array存放的是数组的首地址,通过首地...
    Mjs阅读 521评论 5 0
  • apple文档中指出,每个数组都是保留一定量的内存来保存其内容,也就是数组长度固定,当有更多的数据插入到数组中时,...
    我是繁星阅读 261评论 0 1
  • Array使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Array会强制检测元素的...
    帅驼驼阅读 1,215评论 1 3
  • 这几天用swift发现应该仔细研究一下Array---于是找出了手册看了下,发现了一些东西 不知道从什么版本开始 ...
    氮化镓加砷阅读 1,642评论 0 1
  • 概述: Array是一种有序的,可以随机访问的数据结构,可以存储任意的数据类型。 创建Array: 访问Array...
    MikeDull阅读 3,906评论 0 1