Swift標準庫原始碼閱讀筆記 - Array和ContiguousArray

FFIB發表於2018-06-27

關於 ContiguousArray ,這邊有喵神的文章介紹的很詳細了,可以先看看這個文章。

Array

接著喵神的思路,看一下 Array 以下是從原始碼中擷取的程式碼片段。

public struct Array<Element>: _DestructorSafeContainer {
  #if _runtime(_ObjC)
    internal typealias _Buffer = _ArrayBuffer<Element>
  #else
    internal typealias _Buffer = _ContiguousArrayBuffer<Element>
  #endif

  internal var _buffer: _Buffer
  
  internal init(_buffer: _Buffer) {
    self._buffer = _buffer
  }
}
複製程式碼

if _runtime(_ObjC) 等價於 #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS),從這個操作也可以看出 Swift 的野心不僅僅只是替換 Objective-C那麼簡單,而是往更加寬泛的方向發展。由於本次主要是研究在 iOS下的開發,所以主要看一下 _ArrayBuffer

_ArrayBuffer

去掉了註釋和與型別檢查相關的屬性和方法。

internal typealias _ArrayBridgeStorage
  = _BridgeStorage<_ContiguousArrayStorageBase, _NSArrayCore>

internal struct _ArrayBuffer<Element> : _ArrayBufferProtocol {

  internal init() {
    _storage = _ArrayBridgeStorage(native: _emptyArrayStorage)
  }
  
  internal var _storage: _ArrayBridgeStorage
}
複製程式碼

可見 _ArrayBuffer 僅有一個儲存屬性 _storage ,它的型別 _ArrayBridgeStorage,本質上是 _BridgeStorage
_NSArrayCore 其實是一個協議,定義了一些 NSArray 的方法,主要是為了橋接 Objective-CNSArray
最主要的初始化函式,是通過 _emptyArrayStorage 來初始化 _storage

實際上 _emptyArrayStorage_EmptyArrayStorage 的例項,主要作用是初始化一個空的陣列,並且將記憶體指定在堆上。

internal var _emptyArrayStorage : _EmptyArrayStorage {
  return Builtin.bridgeFromRawPointer(
    Builtin.addressof(&_swiftEmptyArrayStorage))
}
複製程式碼

_BridgeStorage

struct _BridgeStorage<NativeClass: AnyObject, ObjCClass: AnyObject> {
  
  typealias Native = NativeClass
  typealias ObjC = ObjCClass
  
  init(native: Native, bits: Int) {
    rawValue = _makeNativeBridgeObject(
      native, UInt(bits) << _objectPointerLowSpareBitShift)
  }
  
  init(objC: ObjC) {
    rawValue = _makeObjCBridgeObject(objC)
  }
  
  init(native: Native) {
    rawValue = Builtin.reinterpretCast(native)
  }
  
  internal var rawValue: Builtin.BridgeObject
複製程式碼

_BridgeStorage 實際上區分 是否是 class、@objc ,進而提供不同的儲存策略,為上層呼叫提供了不同的介面,以及型別判斷,通過 Builtin.BridgeObject 這個中間引數,實現不同的儲存策略。

The ContiguousArray type is a specialized array that always stores its elements in a contiguous region of memory. This contrasts with Array, which can store its elements in either a contiguous region of memory or an NSArray instance if its Element type is a class or @objc protocol.

If your array’s Element type is a class or @objc protocol and you do not need to bridge the array to NSArray or pass the array to Objective-C APIs, using ContiguousArray may be more efficient and have more predictable performance than Array. If the array’s Element type is a struct or enumeration, Array and ContiguousArray should have similar efficiency.

正因為儲存策略的不同,特別是在class 或者 @objc,如果不考慮橋接到 NSArray 或者呼叫 Objective-C,蘋果建議我們使用 ContiguousArray,會更有效率。

Array 和 ContiguousArray 區別

通過一些常用的陣列操作,來看看兩者之間的區別。

append

ContiguousArray

public mutating func append(_ newElement: Element) {
    _makeUniqueAndReserveCapacityIfNotUnique()
    let oldCount = _getCount()
    _reserveCapacityAssumingUniqueBuffer(oldCount: oldCount)
    _appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement)
  }
 
 internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() {
    if _slowPath(!_buffer.isMutableAndUniquelyReferenced()) {
      _copyToNewBuffer(oldCount: _buffer.count)
    }
  }
  
  internal mutating func _reserveCapacityAssumingUniqueBuffer(oldCount: Int) {
    let capacity = _buffer.capacity == 0

    if _slowPath(oldCount + 1 > _buffer.capacity) {
      _copyToNewBuffer(oldCount: oldCount)
    }
  }
  
  internal mutating func _copyToNewBuffer(oldCount: Int) {
    let newCount = oldCount + 1
    var newBuffer = _buffer._forceCreateUniqueMutableBuffer(
      countForNewBuffer: oldCount, minNewCapacity: newCount)
    _buffer._arrayOutOfPlaceUpdate(
      &newBuffer, oldCount, 0, _IgnorePointer())
  }
  
  internal mutating func _appendElementAssumeUniqueAndCapacity(
    _ oldCount: Int,
    newElement: Element
  ) {
    _buffer.count = oldCount + 1
    (_buffer.firstElementAddress + oldCount).initialize(to: newElement)
  }
複製程式碼

_makeUniqueAndReserveCapacityIfNotUnique() 檢查陣列是否是唯一持有者,以及是否是可變陣列。
_reserveCapacityAssumingUniqueBuffer(oldCount: oldCount)檢查陣列內的元素個數加一後,是否超出超過所分配的空間。
前兩個方法在檢查之後都呼叫了 _copyToNewBuffer ,主要操作是如果當前陣列需要申請空間,則申請空間,然後再複製 buffer
_appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement) 從首地址後的第 oldCount 個儲存空間內,初始化 newElement

Array

Array 實現的過程與 ContiguousArray 差不多,但是還是有一些區別,具體看看,主要的區別存在於_ContiguousArrayBuffer_ArrayBuffer

_ContiguousArrayBuffer

internal var firstElementAddress: UnsafeMutablePointer<Element> {
  return UnsafeMutablePointer(Builtin.projectTailElems(_storage,
                                                       Element.self))
}
複製程式碼

直接返回了記憶體地址。

_ArrayBuffer

internal var firstElementAddress: UnsafeMutablePointer<Element> {
   _sanityCheck(_isNative, "must be a native buffer")
   return _native.firstElementAddress
 }

 internal var _native: NativeBuffer {
   return NativeBuffer(
     _isClassOrObjCExistential(Element.self)
     ? _storage.nativeInstance : _storage.nativeInstance_noSpareBits)
 }
 
internal typealias NativeBuffer = _ContiguousArrayBuffer<Element>
複製程式碼

從呼叫的情況來看,本質上還是呼叫了 _ContiguousArrayBufferfirstElementAddress

但是在建立時,會有型別檢查。

_isClassOrObjCExistential(Element.self)檢查是否是類或者@objc修飾的。

在上述中檢查持有者是否唯一和陣列是否可變的函式中, 其實是呼叫了 _buffer內部的 isMutableAndUniquelyReferenced()

_ContiguousArrayBuffer

 @inlinable
  internal mutating func isUniquelyReferenced() -> Bool {
    return _isUnique(&_storage)
  }
複製程式碼
internal func _isUnique<T>(_ object: inout T) -> Bool {
  return Bool(Builtin.isUnique(&object))
}
複製程式碼

最後呼叫的 Builtin 中的 isUnique

_ArrayBuffer

internal mutating func isUniquelyReferenced() -> Bool {
   if !_isClassOrObjCExistential(Element.self) {
     return _storage.isUniquelyReferenced_native_noSpareBits()
   }
   
   if !_storage.isUniquelyReferencedNative() {
     return false
   }
   return _isNative
 }
 
 mutating func isUniquelyReferencedNative() -> Bool {
   return _isUnique(&rawValue)
 }

 mutating func isUniquelyReferenced_native_noSpareBits() -> Bool {
   _sanityCheck(isNative)
   return _isUnique_native(&rawValue)
 }

func _isUnique_native<T>(_ object: inout T) -> Bool {
  _sanityCheck(
    (_bitPattern(Builtin.reinterpretCast(object)) &  _objectPointerSpareBits)
    == 0)
  _sanityCheck(_usesNativeSwiftReferenceCounting(
      type(of: Builtin.reinterpretCast(object) as AnyObject)))
 return Bool(Builtin.isUnique_native(&object))
}
複製程式碼

如果是 class 或者 @objc_ContiguousBuffer 一樣。如果不是則需要呼叫 Builtin 中的 _isUnique_native,即要檢查是否唯一,還要檢查是否是 Swift 原生 而不是 NSArray。 相對於 _ContiguousArrayBuffer 由於 _ArrayBuffer 承載了需要橋接到 NSArray 的功能,所以多了一些型別檢查的操作。

insert

ContiguousArray

//ContiguousArray
 public mutating func insert(_ newElement: Element, at i: Int) {
   _checkIndex(i)
   self.replaceSubrange(i..<i, with: CollectionOfOne(newElement))
 }
  
 public mutating func replaceSubrange<C>(
    _ subrange: Range<Int>,
    with newElements: C
  ) where C : Collection, C.Element == Element {
    let oldCount = _buffer.count
    let eraseCount = subrange.count
    let insertCount = newElements.count
    let growth = insertCount - eraseCount

    if _buffer.requestUniqueMutableBackingBuffer(
      minimumCapacity: oldCount + growth) != nil {

      _buffer.replaceSubrange(
        subrange, with: insertCount, elementsOf: newElements)
    } else {
      _buffer._arrayOutOfPlaceReplace(subrange, with: newElements, count: insertCount)
    }
  }
  
  internal mutating func requestUniqueMutableBackingBuffer(
    minimumCapacity: Int
  ) -> _ContiguousArrayBuffer<Element>? {
    if _fastPath(isUniquelyReferenced() && capacity >= minimumCapacity) {
      return self
    }
    return nil
  }
  
  //extension ArrayProtocol
  internal mutating func replaceSubrange<C>(
    _ subrange: Range<Int>,
    with newCount: Int,
    elementsOf newValues: C
  ) where C : Collection, C.Element == Element {
    _sanityCheck(startIndex == 0, "_SliceBuffer should override this function.")
    let oldCount = self.count //現有陣列大小
    let eraseCount = subrange.count //需要替換大小

    let growth = newCount - eraseCount //目標大小 和 需要替換大小 的差值
    self.count = oldCount + growth  //替換後的陣列大小

    let elements = self.subscriptBaseAddress //陣列首地址。
    let oldTailIndex = subrange.upperBound 
    let oldTailStart = elements + oldTailIndex //需要替換的尾地址。
    let newTailIndex = oldTailIndex + growth //需要增加的空間的尾下標
    let newTailStart = oldTailStart + growth //需要增加的空間的尾地址
    let tailCount = oldCount - subrange.upperBound //需要移動的記憶體空間大小

    if growth > 0 {
      var i = newValues.startIndex
      for j in subrange {
        elements[j] = newValues[i]
        newValues.formIndex(after: &i)
      }
      for j in oldTailIndex..<newTailIndex {
        (elements + j).initialize(to: newValues[i])
        newValues.formIndex(after: &i)
      }
      _expectEnd(of: newValues, is: i)
    }
    else { 
      var i = subrange.lowerBound
      var j = newValues.startIndex
      for _ in 0..<newCount {
        elements[i] = newValues[j]
        i += 1
        newValues.formIndex(after: &j)
      }
      _expectEnd(of: newValues, is: j)

      if growth == 0 {
        return
      }

      let shrinkage = -growth
      if tailCount > shrinkage {   

        newTailStart.moveAssign(from: oldTailStart, count: shrinkage)

        oldTailStart.moveInitialize(
          from: oldTailStart + shrinkage, count: tailCount - shrinkage)
      }
      else {                      
        newTailStart.moveAssign(from: oldTailStart, count: tailCount)

        (newTailStart + tailCount).deinitialize(
          count: shrinkage - tailCount)
      }
    }
  }

複製程式碼

insert 內部實際是 呼叫了 replaceSubrange。 而在 replaceSubrange 的操作是,判斷記憶體空間是否夠用,和持有者是否唯一,如果有一個不滿足條件則複製 buffer 到新的記憶體空間,並且根據需求分配好記憶體空間大小。

_buffer 內部的 replaceSubrange

  • 計算 growth 值看所替換的大小和目標大小差值是多少。
  • 如果 growth > 0 ,則需要將現有的記憶體空間向後移動 growth 位。
  • 替換所需要替換的值。
  • 超出的部分重新分配記憶體並初始化值。
  • 如果 growth <= 0,則將現有的值替換成新的值即可。
  • 如果 growth < 0,則將不需要的記憶體空間回收即可。(ps:刪除多個元素或者需要替換的大小大於目標大小)。

Array insert 兩者基本一致,唯一的區別和 append 一樣在 在buffer的內部方法,isUniquelyReferenced() 中,多了一些型別檢查。

remove

ContiguousArray

public mutating func remove(at index: Int) -> Element {
    _makeUniqueAndReserveCapacityIfNotUnique()
    let newCount = _getCount() - 1
    let pointer = (_buffer.firstElementAddress + index)
    let result = pointer.move()
    pointer.moveInitialize(from: pointer + 1, count: newCount - index)
    _buffer.count = newCount
    return result
  }
複製程式碼

檢查陣列持有者是否唯一,取出所要刪除的記憶體地址,通過將當前的記憶體區域覆蓋為一個未初始化的記憶體空間,以達到回收記憶體空間的作用,進而達到刪除陣列元素的作用。

Array

ContiguousArray 的區別就在於 _makeUniqueAndReserveCapacityIfNotUnique() 前面已經提到過,仍然是多了一些型別檢查。

subscript

ContiguousArray

//ContiguousArray
public subscript(index: Int) -> Element {
    get {
      let wasNativeTypeChecked = _hoistableIsNativeTypeChecked()

      let token = _checkSubscript(
        index, wasNativeTypeChecked: wasNativeTypeChecked)

      return _getElement(
        index, wasNativeTypeChecked: wasNativeTypeChecked,
        matchingSubscriptCheck: token)
    }
  }
  
  public
  func _getElement(
    _ index: Int,
    wasNativeTypeChecked : Bool,
    matchingSubscriptCheck: _DependenceToken
  ) -> Element {
  #if false
    return _buffer.getElement(index, wasNativeTypeChecked: wasNativeTypeChecked)
  #else
    return _buffer.getElement(index)
  #endif
  }

  //ContiguousArrayBuffer
  internal func getElement(_ i: Int) -> Element {
    return firstElementAddress[i]
  }
複製程式碼

_hoistableIsNativeTypeChecked() 不做任何檢查,直接返回 true_checkSubscript(index, wasNativeTypeChecked: wasNativeTypeChecked) 檢查 index 是否越界。 _getElement 最終還是操作記憶體,通過 firstElementAddress 偏移量取出值。

Array

  //Array
  public
  func _checkSubscript(
    _ index: Int, wasNativeTypeChecked: Bool
  ) -> _DependenceToken {
#if _runtime(_ObjC)
    _buffer._checkInoutAndNativeTypeCheckedBounds(
      index, wasNativeTypeChecked: wasNativeTypeChecked)
#else
    _buffer._checkValidSubscript(index)
#endif
    return _DependenceToken()
  }
  
  func _hoistableIsNativeTypeChecked() -> Bool {
   return _buffer.arrayPropertyIsNativeTypeChecked
  }
  
  //ArrayBuffer
  internal var arrayPropertyIsNativeTypeChecked: Bool {
    return _hasNativeBuffer
  }
  
  internal var _isNativeTypeChecked: Bool {
    if !_isClassOrObjCExistential(Element.self) {
      return true
    } else {
      return _storage.isNativeWithClearedSpareBits(deferredTypeCheckMask)
    }
  }
複製程式碼

ContiguousArray_hoistableIsNativeTypeChecked() 直接返回 true, 而 Array 中如果不是 class 或者 @objc 會返回 ture,否則會檢查是否可以橋接到 Swift

而在 Array_checkSubscript 呼叫的 _buffer 內部函式也不一樣,下面來具體看一看內部實現。

  //ArrayBuffer
  internal func _checkInoutAndNativeTypeCheckedBounds(
    _ index: Int, wasNativeTypeChecked: Bool
  ) {
    _precondition(
      _isNativeTypeChecked == wasNativeTypeChecked,
      "inout rules were violated: the array was overwritten")

    if _fastPath(wasNativeTypeChecked) {
      _native._checkValidSubscript(index)
    }
  }
  
  //ContiguousArrayBuffer
  internal func _checkValidSubscript(_ index : Int) {
    _precondition(
      (index >= 0) && (index < count),
      "Index out of range"
    )
  }
複製程式碼

本質上就是多了一些是否是型別檢查。

//Array
func _getElement(
    _ index: Int,
    wasNativeTypeChecked : Bool,
    matchingSubscriptCheck: _DependenceToken
  ) -> Element {
#if _runtime(_ObjC)
    return _buffer.getElement(index, wasNativeTypeChecked: wasNativeTypeChecked)
#else
    return _buffer.getElement(index)
#endif
  }

//ArrayBuffer
internal func getElement(_ i: Int, wasNativeTypeChecked: Bool) -> Element {
    if _fastPath(wasNativeTypeChecked) {
      return _nativeTypeChecked[i]
    }
    return unsafeBitCast(_getElementSlowPath(i), to: Element.self)
  }
  
internal func _getElementSlowPath(_ i: Int) -> AnyObject {
    let element: AnyObject
    if _isNative {
      _native._checkValidSubscript(i)
      
      element = cast(toBufferOf: AnyObject.self)._native[i]
    } else {
      element = _nonNative.objectAt(i)
    }
    return element
  }
  
  //ContiguousArrayBuffer
  internal subscript(i: Int) -> Element {
    get {
      return getElement(i)
    }
  }
複製程式碼

_buffer 內部的 getElement , 與 ContiguousArray 不同的是需要適配橋接到 NSArray 的情況,如果是 非NSArray 的情況呼叫的是 ContiguousArrayBuffer 內部的 subscript ,和 ContiguousArray 相同。

總結

從增刪改查來看,不管是 ContiguousArray 還是 Array 最終都是操作記憶體,稍顯區別的就是 Array 需要更多的型別檢查。所以當不需要 Objective-C,還是儘量使用 ContiguousArray 。 下面是對陣列中一些批量操作的總結:

  • removeAllinsert<C>(contentsOf: C, at: Int)removeSubrange:最終呼叫的是 replaceSubrange
  • append<S : Sequence>(contentsOf newElements: S)init(repeating repeatedValue: Element, count: Int):最終都是操作記憶體,迴圈初始化新的記憶體空間和值。

有什麼不正確的地方,歡迎指出。

相關文章