Why Swift? Generics(泛型), Collection(集合型別), POP(協議式程式設計), Memory Management(記憶體管理)

戴銘發表於2018-01-30

前言

寫這篇文章主要是為了給組內要做的分享準備內容。這段時間幾個專案都用到 Swift,在上次 GIAC 大會上就被問到為什麼要用 Swift,正好這個主題可以聊聊 Swift 的哪些特性吸引了我。

泛型

先來個例子看下泛型是解決什麼問題的。

let nations = ["中國", "美國", "日本"]
func showNations(arr : [String]) {
    arr.map { str in
        print("\(str)")
    }
}
複製程式碼

我們先定一個字串陣列,然後把裡面的字串列印出來。這裡的 map 寫法還可以優化下:

arr.map { print("\($0)") }
複製程式碼

那麼還能做什麼優化呢。將 showNations 的入引數組泛型以支援多型別,比如 [int],[double] 等。

func showArray<T>(arr: [T]) {
    arr.map { print("\($0)") }
}
複製程式碼

可以看出泛型能夠很方便的將不同型別資料進行相同操作的邏輯歸併在一起。

型別約束

先看下我的 HTN 專案裡狀態機的 Transition 結構體的定義

struct HTNTransition<S: Hashable, E: Hashable> {
    let event: E
    let fromState: S
    let toState: S
    
    init(event: E, fromState: S, toState: S) {
        self.event = event
        self.fromState = fromState
        self.toState = toState
    }
}
複製程式碼

這裡的 fromState,toState 和 event 可以是不同型別的資料,可以是列舉,字串或者整數等,定義 S 和 E 兩個不同的泛型可以讓狀態和事件型別不相同,這樣介面會更加的靈活,更容易適配更多的專案。

大家會注意到 S 和 E 的冒號後面還有個 Hashable 協議,這就是要求它們符合這個協議的型別約束。使用協議的話可以使得這兩個型別更加的規範和易於擴充套件。

Swift 的基本型別 String,Int,Double 和 Bool 等都是遵循 Hashable 的,還有無關聯值的列舉也是的。Hashable 提供了一個 hashValue 方法用在判斷遵循協議物件是否相等時用。

Hashable 協議同時也是遵守 Equatable 協議,通過實現 == 運算子來確定自定義的類或結構是否相同。

關聯型別

在協議裡定義的關聯型別也可以用泛型來處理。比如我們先定義一個協議

protocol HTNState {
    associatedtype StateType
    func add(_ item: StateType)
}
複製程式碼

採用非泛型的實現如下:

struct states: HTNState {
    typealias StateType = Int
    func add(_ item: Int) {
        //...
    }
}
複製程式碼

採用泛型遵循協議可以按照下面方式來寫:

struct states<T>: HTNState {
    func add(_ item: T) {
        //...
    }
}
複製程式碼

這樣關聯型別也能夠享受泛型的好處了。

型別擦除

但是在使用關聯型別的時候需要注意當宣告一個使用了關聯屬性的協議作為屬性時,比如下面的程式碼:

class stateDelegate<T> {
    var state: T
    var delegate: HTNState
}
複製程式碼

先會提示 no initializers 的錯誤,接著會提示 error: protocol 'HTNState' can only be used as a generic constraint because it has Self or associated type requirements 。意思是 HTNState 協議只能作為泛型約束來用,因為它裡面包含必需的 self 或者關聯型別。

那麼該如何處理呢?這裡需要通過型別擦除來解決,主要思路就是加個中間層在程式碼中讓這個抽象的型別具體化。實際上在 Swift 的標準庫裡就有型別擦除很好的運用,比如 AnySequence 的協議。

Where 語句

函式,擴充套件和關聯型別都可以使用 where 語句。where 語句是對泛型在應用時的一種約束。比如:

func stateFilter<FromState:HTNState, ToState:HTNState>(_ from:FromState, _ to:ToState) where FromState.StateType == ToState.StateType {
    //...
}
複製程式碼

這個函式就要求他們的 StateType 具有相同型別。

泛型和 Any 型別

這兩個型別看起來很相似,但是一定要小心兩者的區別。他們區別在於 Any 型別會避開型別的檢查,所以儘量少用最好不用。泛型一方面很靈活一方面也很安全,下面舉個例子感受下兩者的區別:

func add<T>(_ input: T) -> T {
    //...
    return input;
}

func anyAdd(_ input: Any) -> Any {
    //...
    return input;
}
複製程式碼

這兩個函式都是可以允許任意型別的 input 引數,不同在於返回的型別在 anyAdd 函式裡是可以和入參不一樣的,這樣就會失控,在後續的操作中容易出錯。

集合

基本概念

先來了解下集合的基本概念,首先集合是泛型的比如:

let stateArray: Array<String> = ["工作","吃飯","玩遊戲","睡覺"]
複製程式碼

集合它需要先有個遍歷的功能,通過 GeneratorType 協議,可以不關注具體元素型別只要不斷的用迭代器調 next 就可以得到全部元素。但是使用迭代器沒法進行多次的遍歷,這時就需要使用 Sequence 來解決這個問題。像集合的 forEach,elementsEqual,contains,minElement,maxElement,map,flatMap,filter,reduce 等功能都是因為有了 Sequence 的多次遍歷。

最後 Collection 概念是因為 Sequence 無法確定集合裡的位置而在 Sequence 的基礎上實現了 Indexable 協議。有了 Collection 就可以確定元素的位置,包括開始位置和結束位置,這樣就能夠確定哪些元素是已經訪問過的,從而避免多次訪問同一個元素。還能夠通過一個給定的位置直接找到那個位置的元素。

以上描述如下圖:

Why Swift? Generics(泛型), Collection(集合型別), POP(協議式程式設計), Memory Management(記憶體管理)

迭代器

Swift 裡有個簡單的 AnyIterator 結構體

struct AnyIterator<Element>: IteratorProtocol { 
    init(_ body: @escaping () -> Element?)
    //...
}
複製程式碼

AnyIterator 實現了 IteratorProtocol 和 Sequence 協議。通過下面的例子我們來看看如何使用 AnyIterator :

class stateItr : IteratorProtocol {
    var num:Int = 1
    func next() -> Int?{
        num += 2
        return num
    }
}

func findNext<I: IteratorProtocol>( elm: I) -> AnyIterator<I.Element> where I.Element == Int
{
    var l = elm
    print("\(l.next() ?? 0)")
    return AnyIterator { l.next() }
}

findNext(elm: findNext(elm: findNext(elm: stateItr())))
複製程式碼

首先是定義個遵循了 IteratorProtocol 並實現了 next 函式的類。再實現一個 AnyIterator 的迭代器方法,這樣通過這個方法的呼叫就可以不斷的去找符合的元素了。

這裡有個對 where 語句的運用,where I.Element == Int。如果把這句改成 where I.Element == String 會出現下面的錯誤提示

Playground execution failed:

error: MyPlayground.playground:18:37: error: cannot invoke 'findNext(elm:)' with an argument list of type '(elm: stateItr)'
findNext(elm: findNext(elm: findNext(elm: stateItr())))
                                    ^

MyPlayground.playground:11:6: note: candidate requires that the types 'Int' and 'String' be equivalent (requirement specified as 'I.Element' == 'String' [with I = stateItr])
func findNext<I: IteratorProtocol>( elm: I) -> AnyIterator<I.Element> where I.Element == String
     ^
複製程式碼

編譯器會在程式碼檢查階段通過程式碼跟蹤就發現型別不匹配的安全隱患,這裡不得不對 Swift 的設計點個贊先

Sequence

上面的迭代器只會以單次觸發的方式反覆計算下個元素,但是對於希望能夠重新查詢或重新生成已生成的元素,這樣還需要有個新的迭代器和一個子 Sequence。在 Sequence 協議裡可以看到這樣的定義:

public protocol Sequence {
    //Element 表示序列元素的型別
    associatedtype Element where Self.Element == Self.Iterator.Element
    //迭代介面型別
    associatedtype Iterator : IteratorProtocol
    //子 Sequence 型別
    associatedtype SubSequence
    //返回 Sequence 元素的迭代器
    public func makeIterator() -> Self.Iterator
    //...
}
複製程式碼

重新查詢靠的是這個新的迭代器,而對於切片這樣的會重新生成新 Sequence 的操作就需要 SubSequence 進行儲存和返回。

Collection

對 Sequence 進行進一步的完善,最重要的就是使其具有下標索引,使得元素能夠通過下標索引方式取到。Collection 是個有限的範圍,有開始索引和結束索引,所以 Collection 和 Sequence 的無限範圍是不一樣的。有了有限的範圍 Collection 就可以有 count 屬性進行計數了。

除了標準庫裡的 String,Array,Dictionary 和 Set 外比如 Data 和 IndexSet 也由於遵循了 Collection 協議而獲得了標準庫了那些集合型別的能力。

map

在泛型的第一個例子裡我們就看到了 map 的使用,我們看看 map 的定義:

func map<T>(transform: (Self.Generator.Element) -> T) rethrows -> [T]
複製程式碼

這裡 (Self.Generator.Element) -> T 就是 map 閉包的定義,Self.Generator.Element 就是當前元素的型別。

flatmap

二維陣列經過 flatmap 會降維到一維,還能過濾掉 nil 值。下面看看 Swift 原始碼(swift/stdlib/public/core/SequenceAlgorithms.swift.gyb)中 flatmap 的實現:

//===----------------------------------------------------------------------===//
// flatMap()
//===----------------------------------------------------------------------===//

extension Sequence {
  /// Returns an array containing the concatenated results of calling the
  /// given transformation with each element of this sequence.
  ///
  /// Use this method to receive a single-level collection when your
  /// transformation produces a sequence or collection for each element.
  ///
  /// In this example, note the difference in the result of using `map` and
  /// `flatMap` with a transformation that returns an array.
  ///
  ///     let numbers = [1, 2, 3, 4]
  ///
  ///     let mapped = numbers.map { Array(count: $0, repeatedValue: $0) }
  ///     // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
  ///
  ///     let flatMapped = numbers.flatMap { Array(count: $0, repeatedValue: $0) }
  ///     // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
  ///
  /// In fact, `s.flatMap(transform)`  is equivalent to
  /// `Array(s.map(transform).joined())`.
  ///
  /// - Parameter transform: A closure that accepts an element of this
  ///   sequence as its argument and returns a sequence or collection.
  /// - Returns: The resulting flattened array.
  ///
  /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
  ///   and *n* is the length of the result.
  /// - SeeAlso: `joined()`, `map(_:)`
  public func flatMap<SegmentOfResult : Sequence>(
    _ transform: (${GElement}) throws -> SegmentOfResult
  ) rethrows -> [SegmentOfResult.${GElement}] {
    var result: [SegmentOfResult.${GElement}] = []
    for element in self {
      result.append(contentsOf: try transform(element))
    }
    return result
  }
}

extension Sequence {
  /// Returns an array containing the non-`nil` results of calling the given
  /// transformation with each element of this sequence.
  ///
  /// Use this method to receive an array of nonoptional values when your
  /// transformation produces an optional value.
  ///
  /// In this example, note the difference in the result of using `map` and
  /// `flatMap` with a transformation that returns an optional `Int` value.
  ///
  ///     let possibleNumbers = ["1", "2", "three", "///4///", "5"]
  ///
  ///     let mapped: [Int?] = possibleNumbers.map { str in Int(str) }
  ///     // [1, 2, nil, nil, 5]
  ///
  ///     let flatMapped: [Int] = possibleNumbers.flatMap { str in Int(str) }
  ///     // [1, 2, 5]
  ///
  /// - Parameter transform: A closure that accepts an element of this
  ///   sequence as its argument and returns an optional value.
  /// - Returns: An array of the non-`nil` results of calling `transform`
  ///   with each element of the sequence.
  ///
  /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
  ///   and *n* is the length of the result.
  public func flatMap<ElementOfResult>(
    _ transform: (${GElement}) throws -> ElementOfResult?
  ) rethrows -> [ElementOfResult] {
    var result: [ElementOfResult] = []
    for element in self {
      if let newElement = try transform(element) {
        result.append(newElement)
      }
    }
    return result
  }
}
複製程式碼

從程式碼中可以看出打平的原理是將集合中所有元素都新增到另外一個集合裡。在第二個 extension 裡通過 if let 語句會擋住那些解包不成功的元素。

Reduce

Reduce 是程式語言語義學裡的歸約語義學,也叫累加器。下面同樣可以看看 Swift 原始碼裡對其的實現:

//===----------------------------------------------------------------------===//
// reduce()
//===----------------------------------------------------------------------===//

extension Sequence {
  /// Returns the result of combining the elements of the sequence using the
  /// given closure.
  ///
  /// Use the `reduce(_:_:)` method to produce a single value from the elements
  /// of an entire sequence. For example, you can use this method on an array
  /// of numbers to find their sum or product.
  ///
  /// The `nextPartialResult` closure is called sequentially with an
  /// accumulating value initialized to `initialResult` and each element of
  /// the sequence. This example shows how to find the sum of an array of
  /// numbers.
  ///
  ///     let numbers = [1, 2, 3, 4]
  ///     let numberSum = numbers.reduce(0, { x, y in
  ///         x + y
  ///     })
  ///     // numberSum == 10
  ///
  /// When `numbers.reduce(_:_:)` is called, the following steps occur:
  ///
  /// 1. The `nextPartialResult` closure is called with `initialResult`---`0`
  ///    in this case---and the first element of `numbers`, returning the sum:
  ///    `1`.
  /// 2. The closure is called again repeatedly with the previous call's return
  ///    value and each element of the sequence.
  /// 3. When the sequence is exhausted, the last value returned from the
  ///    closure is returned to the caller.
  ///
  /// If the sequence has no elements, `nextPartialResult` is never executed
  /// and `initialResult` is the result of the call to `reduce(_:_:)`.
  ///
  /// - Parameters:
  ///   - initialResult: The value to use as the initial accumulating value.
  ///     `initialResult` is passed to `nextPartialResult` the first time the
  ///     closure is executed.
  ///   - nextPartialResult: A closure that combines an accumulating value and
  ///     an element of the sequence into a new accumulating value, to be used
  ///     in the next call of the `nextPartialResult` closure or returned to
  ///     the caller.
  /// - Returns: The final accumulated value. If the sequence has no elements,
  ///   the result is `initialResult`.
  public func reduce<Result>(
    _ initialResult: Result,
    _ nextPartialResult:
      (_ partialResult: Result, ${GElement}) throws -> Result
  ) rethrows -> Result {
    var accumulator = initialResult
    for element in self {
      accumulator = try nextPartialResult(accumulator, element)
    }
    return accumulator
  }
}
複製程式碼

可以看到裡面會通過 initialResult 來記錄前面的返回結果和當前元素進行在閉包裡的操作。

Array

看看陣列的基本用法

//建立陣列
var nums = [Int]() //建立空陣列
var mArray = nums + [2,3,5] + [5,9]//合併多個有相同型別元素陣列的值
var animals: [String] = ["dragon", "cat", "mice", "dog"]

//新增陣列
animals.append("bird")
animals += ["ant"]

//獲取和改變陣列
var firstItem = mArray[0]
animals[0] = "red dragon"
animals[2...4] = ["black dragon", "white dragon"] //使用下標改變多個元素
animals.insert("chinese dragon", at: 0) //在索引值之前新增元素
let mapleSyrup = animals.remove(at: 0) //移除陣列中的一個元素
let apples = animals.removeLast() //移除最後一個元素

////陣列遍歷
for animal in animals {
    print(animal)
}
for (index, animal) in animals.enumerated() {
    print("animal \(String(index + 1)): \(animal)")
}
/*
 animal 1: red dragon
 animal 2: cat
 animal 3: black dragon
 animal 4: white dragon
 */
複製程式碼

弱引用的 Swift 陣列

Swift 裡的陣列預設會強引用裡面的元素,但是有時候可能希望能夠弱引用,那麼就可以使用 NSPointerArray。它在初始化的時候可以決定是用弱引用方式還是強引用方式。

let strongArr = NSPointerArray.strongObjects() // 強引用
let weakArr = NSPointerArray.weakObjects() // Maintains weak references
複製程式碼

Dictionary 的要想用弱引用可以使用 NSMapTable,Set 對應的是 NSHashTable。

Dictionary

看看基本用法:

//建立 Dictionary
var strs = [Int: String]()
var colors: [String: String] = ["red": "#e83f45", "yellow": "#ffe651"]
strs[16] = "sixteen"

//updateValue 這個方法會返回更新前的值
if let oldValue = colors.updateValue("#e83f47", forKey: "red") {
    print("The old value for DUB was \(oldValue).")
}

//遍歷
for (color, value) in colors {
    print("\(color): \(value)")
}

//map
let newColorValues = colors.map { "hex:\($0.value)" }
print("\(newColorValues)")

//mapValues 返回完整的新 Dictionary
let newColors = colors.mapValues { "hex:\($0)" }
print("\(newColors)")
複製程式碼

協議式程式設計

Swift 被設計成單繼承,如果希望是多繼承就需要使用協議。協議還有個比較重要的作用就是通過 associatedtype 要求使用者遵守指定的泛型約束。

下面先看看傳統程式設計的開發模式:

class Dragon {
    
}
class BlackDragon: Dragon{
    func fire() {
        print("fire!!!")
    }
}

class WhiteDragon: Dragon {
    func fire() {
        print("fire!!!")
    }
}

BlackDragon().fire()
WhiteDragon().fire()
複製程式碼

這個例子可以看出 fire() 就是重複程式碼,那麼首先想到的方法就是通過直接在基類裡新增這個方法或者通過 extension 來對他們基類進行擴充套件:

extension Dragon {
    func fire() {
        print("fire!!!")
    }
}
複製程式碼

這時我們希望加個方法讓 Dragon 能夠 fly:

extension Dragon {
    func fire() {
        print("fire!!!")
    }
    func fly() {
        print("fly~~~")
    }
}
複製程式碼

這樣 BlackDragon 和 WhiteDragon 就都有這兩個能力了,如果我們設計出一個新的龍 YellowDragon 或者更多 Dragon 都沒有 fly 的能力,這時該如何。因為沒法多繼承,那麼沒法拆成兩個基類,這樣必然就會出現重複程式碼。但是有了協議這個問題就好解決了。具體實現如下:

protocol DragonFire {}
protocol DragonFly {}

extension DragonFire {
    func fire() {
        print("fire!!!")
    }
}
extension DragonFly {
    func fly() {
        print("fly~~~")
    }
}

class BlackDragon: DragonFire, DragonFly {}
class WhiteDragon: DragonFire, DragonFly {}
class YellowDragon: DragonFire {}
class PurpleDragon: DragonFire {}

BlackDragon().fire()
WhiteDragon().fire()
BlackDragon().fly()
YellowDragon().fire()
複製程式碼

可以看到一來沒有了重複程式碼,二來結構也清晰了很多而且更容易擴充套件,Dragon 的種類和能力的組合也更加方便和清晰。extension 使得協議有了實現預設方法的能力。

關於多繼承 Swift 是採用 Trait 的方式,其它語言 C++ 是直接支援多繼承的,方式是這個類會持有多個父類的例項。Java 的多繼承只繼承能做什麼,怎麼做還是要自己來。和 Trait 類似的解決方案是 Mixin,Ruby 就是用的這種超程式設計思想。

協議還可以繼承,還可以通過 & 來聚合,判斷一個類是否遵循了一個協議可以使用 is 關鍵字。

當然協議還可以作為型別,比如一個陣列泛型元素指定為一個協議,那麼這個陣列裡的元素只要遵循這個協議就可以了。

Swift 記憶體管理

記憶體分配

Heap

在 Heap 上記憶體分配的時候需要鎖定 Heap 上能夠容納存放物件的空閒塊,主要是為了執行緒安全,我們需要對這些進行鎖定和同步。

Heap 是完全二叉樹,即除最底層節點外都是填滿的,最底層節點填充是從左到右。Swift 的 Heap 是通過雙向連結串列實現。由於 Heap 是可以 retain 和 release 所以很容易分配空間就不連續了。採用連結串列的目的是希望能夠將記憶體塊連起來,在 release 時通過調整連結串列指標來整合空間。

在 retain 時不可避免需要遍歷 Heap,找到合適大小的記憶體塊,能優化的也只是記錄以前遍歷的情況減少一些遍歷。但是 Heap 是很大的,這樣每次遍歷還是很耗時,而且 release 為了能夠整合空間還需要判斷當前記憶體塊的前一塊和後面那塊是否為空閒等,如果空閒還需要遍歷連結串列查詢,所以最終的解決方式是雙向連結串列。只把空閒記憶體塊用指標連起來形成連結串列,這樣 retain 時可以減少遍歷,效率理論上可以提高一倍,在 release 時將多餘空間插入到 Heap 開始的位置和先前移到前面的空間進行整合。

即使效率高了但是還是比不過 Stack,所以蘋果也將以前 OC 裡的一些放在 Heap 裡的型別改造成了值型別。

Stack

Stack 的結構很簡單,push 和 pop 就完事了,記憶體上只需要維護 Stack 末端的指標即可。由於它的簡單所以處理一些時效性不高,臨時的事情是非常合適的,所以可以把 Stack 看成是一個交換臨時資料的記憶體區域。在多執行緒上,由於 Stack 是執行緒獨有的,所以也不需要考慮執行緒安全相關問題。

記憶體對齊

Swift 也有記憶體對齊的概念

struct DragonFirePosition {
    var x:Int64 //8 Bytes
    var y:Int32 //4 Bytes
    //8 + 4
}
struct DragonHomePosition {
    var y:Int32 //4 Bytes + 對齊記憶體(4 Bytes)
    var x:Int64 //8 Bytes
    //4 + 4 + 8
}
let firePositionSize = MemoryLayout<DragonFirePosition>.size //12
let homePositionSize = MemoryLayout<DragonHomePosition>.size //16
複製程式碼

Swift 派發機制

派發目的是讓 CPU 知道被呼叫的函式在哪裡。Swift 語言是支援編譯型語言的直接派發,函式表派發和訊息機制派發三種派發方式的,下面分別對這三種派發方式說明下。

直接派發

C++ 預設使用的是直接派發,加上 virtual 修飾符可以改成函式表派發。直接派發是最快的,原因是呼叫指令會少,還可以通過編譯器進行比如內聯等方式的優化。缺點是由於缺少動態性而不支援繼承。

struct DragonFirePosition {
    var x:Int64
    var y:Int32
    func land() {}
}

func DragonWillFire(_ position:DragonFirePosition) {
    position.land()
}
let position = DragonFirePosition(x: 342, y: 213)
DragonWillFire(position)
複製程式碼

編譯 inline 後 DragonWillFire(DragonFirePosition(x: 342, y: 213)) 會直接跳到方法實現的地方,結果就變成 position.land()。

函式表派發

Java 預設就是使用的函式表派發,通過 final 修飾符改成直接派發。函式表派發是有動態性的,在 Swift 裡函式表叫 witness table,大部分語言叫 virtual table。一個類裡會用陣列來儲存裡面的函式指標,override 父類的函式會替代以前的函式,子類新增的函式會被加到這個陣列裡。舉個例子:

class Fish {
    func swim() {}
    func eat() {
        //normal eat
    }
}

class FlyingFish: Fish {
    override func eat() {
        //flying fish eat
    }
    func fly() {}
}
複製程式碼

編譯器會給 Fish 類和 FlyingFish 類分別建立 witness table。在 Fish 的函式表裡有 swim 和 eat 函式,在 FlyingFish 函式表裡有父類 Fish 的 swim,覆蓋了父類的 eat 和新增加的函式 fly。

一個函式被呼叫時會先去讀取物件的函式表,再根據類的地址加上該的函式的偏移量得到函式地址,然後跳到那個地址上去。從編譯後的位元組碼這方面來看就是兩次讀取一次跳轉,比直接派發還是慢了些。

訊息機制派發

這種機制是在執行時可以改變函式的行為,KVO 和 CoreData 都是這種機制的運用。OC 預設就是使用的訊息機制派發,使用 C 來直接派發獲取高效能。Swift 可以通過 dynamic 修飾來支援訊息機制派發。

當一個訊息被派發,執行時就會按照繼承關係向上查詢被呼叫的函式。但是這樣效率不高,所以需要通過快取來提高效率,這樣查詢效能就能和函式派發差不多了。

具體派發

宣告

值型別都會採用直接派發。無論是 class 還是協議 的 extension 也都是直接派發。class 和協議是函式表派發。

指定派發方式

  • final:讓類裡的函式使用直接派發,這樣該函式將會沒有動態性,執行時也沒法取到這個函式。
  • dynamic:可以讓類裡的函式使用訊息機制派發,可以讓 extension 裡的函式被 override。

派發優化

Swift 會在這上面做優化,比如一個函式沒有 override,Swift 就可能會使用直接派發的方式,所以如果屬性繫結了 KVO 它的 getter 和 setter 方法可能會被優化成直接派發而導致 KVO 的失效,所以記得加上 dynamic 的修飾來保證有效。後面 Swift 應該會在這個優化上去做更多的處理。

基本資料型別記憶體管理

通過 MemoryLayout 來看看基本資料型別的記憶體是佔用多大

MemoryLayout<Int>.size      //8
MemoryLayout<Int16>.size    //2
MemoryLayout<Bool>.size     //1
MemoryLayout<Float>.size    //4
MemoryLayout<Double>.size   //8
複製程式碼

Struct 記憶體管理

對於 Struct 在編譯中就能夠確定空間,也就不需要額外的空間給執行時用,執行過程呼叫時就是直接傳地址。

下面我們再看看 Struct 的 MemoryLayout

struct DragonFirePosition {
    var x:Int64 //8 Bytes
    var y:Int32 //4 Bytes
    //8 + 4
    func land() {}
}

MemoryLayout<DragonFirePosition>.size       //12
MemoryLayout<DragonFirePosition>.alignment  //8
MemoryLayout<DragonFirePosition>.stride     //16
複製程式碼

alignment 可以看出是按照 8 Bytes 來對齊的,所以這裡 struct 是用到了位元組對齊,實際佔用大小通過 stride 可以看出就是 8 * 2 為16。

如果把 var x:Int64 改成可選型別會增加 4個 Bytes,不過就這個 case 來說實際大小還是16,這個也是因為記憶體對齊的原因。

Class 記憶體管理

Class 本身是在 Stack 上分配的,在 Heap 上還需要儲存 Class 的 Type 資訊,這個 Type 資訊裡有函式表,在函式派發時就可以按照這個函式表進行派發了。繼承的話子類只要在自己的 Type 資訊裡記錄自己的資訊即可。

協議型別記憶體管理

協議型別的記憶體模型是 Existential Container。先看下面例子

protocol DragonFire {}
extension DragonFire {
    func fire() {
        print("fire!!!")
    }
}

struct YellowDragon: DragonFire {
    let eyes = "blue"
    let teeth = 48
}

let classSize = MemoryLayout<YellowDragon>.size  //32
let protocolSize = MemoryLayout<DragonFire>.size //40
複製程式碼

可以看出來協議要比類大。這個是由於 Existential Container 的前三個 word 是叫 Value buffer 用來儲存 inline 的值。第四個 word 是 Value Witness Table,儲存值的各種操作比如 allocate,copy,destruct 和 deallocate 等。第五個 word 是 Protocol Witness Table 是儲存協議的函式。

泛型的記憶體管理

泛型採用的和 Existential Container 原理類似。Value Witness Table 和 Protocol Witness Table 會作為隱形的引數傳遞到泛型方法裡。不過經過編譯器的層層 inline 優化,最終型別都會被推匯出來也就不再需要 Existential Container 這一套了。

相關文章