Swift - Array

Swift - Array

[TOC]

Array是我們?nèi)粘4蚪坏婪浅6嗟囊粋€(gè)集合,下面我們就來(lái)研究一下它。

1. Array 的創(chuàng)建

1.1 初始化一個(gè)數(shù)組

Swift 中有很多創(chuàng)建Array的方式:

// 通過(guò)字面量初始化一個(gè)Int類型的Array
var numbers = [1, 2, 3, 4, 5]
// 通過(guò)字面量初始化一個(gè)String類型的Array
var strArray = ["element1", "element2", "element3", "element4",]

// 初始化一個(gè)Any類型的空Array,此處必須指定類型,否則會(huì)報(bào)編譯錯(cuò)誤
var emptyArray = Array<Any>()
var emptyArray1: [Any] = Array()
var emptyArray2: Array<Any> = Array()
var emptyArray3 = [Any]()

// 當(dāng)然為了防止訪問(wèn)越界我們也可以這樣初始化指定長(zhǎng)度和初始值的數(shù)組
var array = Array(repeating: 0, count: 10)
print(array[0])

以上代碼提供了數(shù)組的基本初始化方法。

  • 可以通過(guò)字面量初始化
  • 可以初始化空數(shù)組
  • 可以初始化指定大小和初始值的數(shù)組

1.2 初始化數(shù)組的底層實(shí)現(xiàn)(SIL)

那么數(shù)組在底層是如何創(chuàng)建的呢?下面我們通過(guò)sil代碼進(jìn)行查看:

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

sil代碼中可以看到:

  • 調(diào)用了_allocateUninitializedArray函數(shù)初始了長(zhǎng)度為5,類型為Int的數(shù)組
  • 然后為初始的數(shù)據(jù),也就是個(gè)元組,取出里面的兩個(gè)指針賦值到%7和%8
  • 根據(jù)%8的地址依次偏移,存儲(chǔ)數(shù)組中的元素
  • 最后將%7的指針存儲(chǔ)到%3,也就是numbers

其實(shí)這段代碼很好理解,但是有一個(gè)疑問(wèn),大家都說(shuō)Array是一個(gè)值類型,那么我們剛才其實(shí)看到了alloc的調(diào)用,那么這是為什么呢?我們一步Swift源碼Swift 5.3.1中一探究竟。

1.3 源碼探索

1.3.1 _allocateUninitializedArray

首先我們搜索一下_allocateUninitializedArray方法,可以在ArrayShared.swift文件中找到。

-w623

在源碼中我們可以看到:

  • 根據(jù)count的不同做了不同的初始化
  • 如果大于0會(huì)調(diào)用allocWithTailElems_1初始化一個(gè)bufferObject
  • 然后調(diào)用_adoptStorage初始一個(gè)元組
  • 如果不大于0則調(diào)用_allocateUninitialized初始化一個(gè)元組
  • 最后返回這個(gè)元組

為了更好的跟代碼,我們?cè)?code>Debug調(diào)試一下:

-w768
-w901

繼續(xù)跟下去就到了這里:

-w1229

很熟悉的創(chuàng)建HeapObject的代碼,接下來(lái)調(diào)用的是一個(gè)Array的函數(shù),我我們跟一下:

1.3.2 _adoptStorage

-w604

在這個(gè)方法中我們可以看到:

  • 首先創(chuàng)建了一個(gè)_ContiguousArrayBuffer類型的變量
  • 然后返回了一個(gè)元組
    • 元組的第一個(gè)元素是一個(gè)Array的實(shí)例對(duì)象
    • 第二個(gè)元素是第一個(gè)元素的地址

所以在sil代碼中,%7和%8對(duì)應(yīng)的就是元組中的兩個(gè)值。%7是Array的地址,%8是原生首地址的地址。

-w679

1.3.3 _ContiguousArrayBuffer

-w589

_ContiguousArrayBuffer也是個(gè)結(jié)構(gòu)體。

1.4 Array 的內(nèi)存布局

下面我們來(lái)看看Array的內(nèi)存布局,從_adoptStorage方法繼續(xù)向上翻就可以看到Array這個(gè)結(jié)構(gòu)體中只有一個(gè)_buffer的成員變量。

-w641

下面我回到_ContiguousArrayBuffer中繼續(xù)探索,在該結(jié)構(gòu)體的最后,我們可以看到_storage屬性的定義。

image

在其初始化的時(shí)候也都能看到對(duì)_storage的初始化:

image

_storage的類型是__ContiguousArrayStorageBase,下面我們就研究一下__ContiguousArrayStorageBase

1.4.1 __ContiguousArrayStorageBase

__ContiguousArrayStorageBase是一個(gè)類,定義在SwiftNativeNSArray.swift文件中:

-w876

這里面有一個(gè)成員屬性,回到_ContiguousArrayBuffer中可以看到如下代碼:

-w678

這里面初始化了countAndCapacity。

1.4.2 _ArrayBody

下面我們來(lái)看看_ArrayBody是什么,在ArrayBody.swift文件中我們可以看到如下代碼:

-w753

我們可以看到_ArrayBody是一個(gè)結(jié)構(gòu)體,里面有一個(gè)_SwiftArrayBodyStorage類型的屬性。

搜索一下_SwiftArrayBodyStorage可以在GlobalObjects.h文件中找到其定義:

struct _SwiftArrayBodyStorage {
  __swift_intptr_t count;
  __swift_uintptr_t _capacityAndFlags;
};

可以看到_SwiftArrayBodyStorage是一個(gè)結(jié)構(gòu)體,里面定義了兩個(gè)成員變量。

1.4.3 內(nèi)存布局總結(jié)

經(jīng)過(guò)上面的一番探索我們總結(jié)一下Array的內(nèi)存布局:

struct Array----->struct _ContiguousArrayBuffer----->class __ContiguousArrayStorageBase----->包含一個(gè)屬性類型是struct ArrayBody----->包含一個(gè)屬性struct _SwiftArrayBodyStorage----->包含兩個(gè)屬性count和_capacityAndFlags

這里最主要的就是__ContigousArrayStorageBase,因?yàn)榍懊娑际侵殿愋汀?/p>

image

1.4.4 通過(guò)lldb驗(yàn)證以上結(jié)論

下面我們通過(guò)lldb打印一下:

-w619

1.4.5 _capacityAndFlags

乍一看_capacityAndFlags的值好像是count的兩倍,下面我們通過(guò)源碼來(lái)看一看:

@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的初始化代碼,我們可以看到對(duì)_capacityAndFlags賦值的時(shí)候是將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是一個(gè)計(jì)算屬性,看看是否當(dāng)兼容Objective-C的時(shí)候是1,否則是0,這里我們并沒(méi)有兼容OC所以使用0。

那么調(diào)用的時(shí)候傳值是怎么傳的呢?這個(gè)屬性是__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也是調(diào)用處傳過(guò)來(lái)的,調(diào)用點(diǎn)也在_ContiguousArrayBuffer中:

  @inlinable
  internal init(count: Int, storage: _ContiguousArrayStorage<Element>) {
    _storage = storage

    _initStorageHeader(count: count, capacity: count)
  }

可以看到這個(gè)capacity的值就是count,所以說(shuō)_capacityAndFlags也是個(gè)按位存儲(chǔ)的變量,通過(guò)計(jì)算得出capacity,并不是這存儲(chǔ)capacity,根據(jù)變量的名稱我們也可以知道該變量并不是只存儲(chǔ)一個(gè)值的。

1.4.6 metadata

lldb調(diào)試的時(shí)候,我們發(fā)現(xiàn)存儲(chǔ)metadata的地址很大,那么怎么怎么回事呢?我們通過(guò)cat address命令查看一下(需要安裝插件),Xcode默認(rèn)不帶這個(gè)命令:

image

那么這個(gè)InitialAllocationPool是什么呢?我們?nèi)ピ创a中看看,可以在Metadata.cpp中找到:

image

下面我們測(cè)試一下,初始化一個(gè)數(shù)組:


image

可以看到調(diào)用堆棧確實(shí)調(diào)用了。但其實(shí)不同的創(chuàng)建方式,這個(gè)位置的內(nèi)容是不一樣的:


image
image

其實(shí)這么大的地址就是靜態(tài)變量(這里也應(yīng)該是Metadata),嘗試了幾次,如果初始化一個(gè)空數(shù)組就是_swiftEmptyArrayStorage,有值的時(shí)候就是InitialAllocationPool,并沒(méi)有過(guò)多測(cè)試,不知道具體準(zhǔn)不準(zhǔn)確。

2. 數(shù)組的拼接

2.1 數(shù)組的擴(kuò)容

一個(gè)數(shù)組初始完成后,我們會(huì)需要往里面添加數(shù)據(jù),也就是append操作,涉及到append就存在擴(kuò)容的問(wèn)題,那么Swift中的Array是如何擴(kuò)容的呢?

我們直接找到append方法:

-w684

我們可以看到,擴(kuò)容時(shí)調(diào)用的_reserveCapacityAssumingUniqueBuffer方法,如果全局搜索會(huì)有好幾個(gè)_reserveCapacityAssumingUniqueBuffer方法,經(jīng)過(guò)測(cè)試實(shí)際是調(diào)用的Array.Swift文件中的。

-w699

這里也很簡(jiǎn)單,判斷oldCount + 1 > _buffer.capacity就擴(kuò)容。

接下來(lái)我們找到_createNewBuffer方法,也在Array.Swift文件中。

-w901

擴(kuò)容的時(shí)候調(diào)用的是_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:)方法中:

  • 首先會(huì)判斷使其擴(kuò)容的是不是append方法
  • 如果是判斷oldCapacity是否小于minimumCapacity
  • 如果不是返回oldCapacity
  • 如果是取出oldCapacity的兩倍和minimumCapacity中的大值進(jìn)行返回

這個(gè)minimumCapacity的值在這條調(diào)用堆棧上是oldCount + 1,growForAppend的值為true可以在_reserveCapacityAssumingUniqueBuffer方法中看到。

所以基本可以認(rèn)為數(shù)組的擴(kuò)容時(shí)之前的兩倍。

擴(kuò)容時(shí)對(duì)于就元素的的處理,如果原始緩沖區(qū)是唯一的,我們可以移動(dòng)元素,而不是復(fù)制。如果不是就需要復(fù)制元素。

回到我們的代碼中,通過(guò)lldb調(diào)試一下:

-w660

append后數(shù)組的地址就已經(jīng)改變了。

讀取兩段地址:


image

_capacityAndFlags的值分別是0xa0x14。

-w356

右移1位分別是5和10。

2.1 Array的拷貝

首先我們看看下面這段代碼:

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

我們看到這里只調(diào)用了copy_addr,下面我們看看copy_addr的作用是什么:

-w567

可以看到這里面就是一個(gè)值的拷貝,在這里就是把numbers里面存儲(chǔ)的內(nèi)容(這里是一個(gè)指針)拷貝一份到tmpArray中。

那么這里就存在一個(gè)問(wèn)題了,Array在底層是一個(gè)結(jié)構(gòu)體,在Swift中結(jié)構(gòu)體是一個(gè)值類型,那么也就意味著當(dāng)前我們改變numbers或者tmpArray不會(huì)影響另外一個(gè)里面的值。

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

var tmpArray = numbers
tmpArray[0] = 2

print(numbers)
print(tmpArray)
-w490

我們可以看到確實(shí)沒(méi)有影響到另一個(gè)。但是numbers里面存儲(chǔ)的是地址,當(dāng)初我們?cè)谔剿?a href="http://www.itdecent.cn/p/d02ef86bbf79" target="_blank">值類型和引用類型的時(shí)候,當(dāng)值類型內(nèi)嵌套引用類型的時(shí)候,改變值類型中的引用類型的值,會(huì)影響到拷貝的另一個(gè)值類型,這點(diǎn)就不一樣了,還是上面的代碼,我們通過(guò)lldb調(diào)試看一看:

-w678

我們可以看到給tmpArray修改值后,其存儲(chǔ)的值已經(jīng)改變了,那么修改的時(shí)候發(fā)生可什么呢?下面我們?cè)诳纯?code>Sil代碼:

-w717

下面我們到源碼中看看:

-w766

Array.swift中我們可以看到subscript方法有get_modify兩個(gè)方法。在_modify里面會(huì)調(diào)用_makeMutableAndUnique方法,下面我們就看看這個(gè)方法:

-w720

這里會(huì)調(diào)用_createNewBuffer方法,這就是我們?cè)跀?shù)組拼接的時(shí)候介紹的這個(gè)方法:

-w903

這里會(huì)創(chuàng)建一個(gè)新的buffer賦值給_buffer

3. 總結(jié)

至此我們對(duì) Swift 中數(shù)組的介紹就結(jié)束,下面總結(jié)一下:

  • Swift中的數(shù)組本質(zhì)是個(gè)結(jié)構(gòu)體,里面的結(jié)構(gòu)如下圖:
image
  • 因?yàn)槭墙Y(jié)構(gòu)體,所以我們認(rèn)為數(shù)組是值類型
  • 數(shù)組空間的擴(kuò)容時(shí)原來(lái)容量的兩倍,目前沒(méi)有發(fā)現(xiàn)有最大容量限制
  • 數(shù)組的拷貝是寫(xiě)時(shí)復(fù)制機(jī)制,只有修改的時(shí)候才會(huì)修改
    • 首先拷貝的時(shí)候是拷貝數(shù)組的地址
    • 當(dāng)需要修改任一指向同一空間的數(shù)組的時(shí)候創(chuàng)建一個(gè)新的buffer來(lái)存儲(chǔ)里面的元素
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Array lazy 當(dāng)我們調(diào)用的時(shí)候才會(huì)觸發(fā)lazy Array的創(chuàng)建 array存放的是數(shù)組的首地址,通過(guò)首地...
    Mjs閱讀 691評(píng)論 5 0
  • apple文檔中指出,每個(gè)數(shù)組都是保留一定量的內(nèi)存來(lái)保存其內(nèi)容,也就是數(shù)組長(zhǎng)度固定,當(dāng)有更多的數(shù)據(jù)插入到數(shù)組中時(shí),...
    我是繁星閱讀 349評(píng)論 0 1
  • Array使用有序列表存儲(chǔ)同一類型的多個(gè)值。相同的值可以多次出現(xiàn)在一個(gè)數(shù)組的不同位置中。Array會(huì)強(qiáng)制檢測(cè)元素的...
    帥駝駝閱讀 1,390評(píng)論 1 3
  • 這幾天用swift發(fā)現(xiàn)應(yīng)該仔細(xì)研究一下Array---于是找出了手冊(cè)看了下,發(fā)現(xiàn)了一些東西 不知道從什么版本開(kāi)始 ...
    氮化鎵加砷閱讀 1,762評(píng)論 0 1
  • 概述: Array是一種有序的,可以隨機(jī)訪問(wèn)的數(shù)據(jù)結(jié)構(gòu),可以存儲(chǔ)任意的數(shù)據(jù)類型。 創(chuàng)建Array: 訪問(wèn)Array...
    MikeDull閱讀 4,374評(píng)論 0 1

友情鏈接更多精彩內(nèi)容