寫更好的 Swift 程式碼:COW(Copy-On-Write)

語言: CN / TW / HK

Swift 有值型別和引用型別,而值型別在被賦值或被傳遞給函式時是會被拷貝的。在寫程式碼時,這些值型別每次賦值傳遞都是會重新在記憶體裡拷貝一份嗎?

答案是否定的。如有個包含上千個元素的陣列,然後你把它 copy 一份給另一個變數,那麼 Swift 就要拷貝所有的元素,即使這兩個變數的陣列內容完全一樣,這對它效能來說是多麼糟糕。

Structures and Enumerations Are Value Types 明確的提到了對其實現做了優化,可避免不必要的複製:

Collections defined by the standard library like arrays, dictionaries, and strings use an optimization to reduce the performance cost of copying. Instead of making a copy immediately, these collections share the memory where the elements are stored between the original instance and any copies. If one of the copies of the collection is modified, the elements are copied just before the modification. The behavior you see in your code is always as if a copy took place immediately.

使用了 COW, 當將兩個變數指向同一陣列時,他們指向相同的底層資料。當修改第二個變數的時候,Swift 才會去複製一個副本,第一個不會改變。

  • 通過延遲複製操作,直到實際使用到的時候才去複製,以此確保沒有浪費的工作。
  • 使得值型別可以被多次複製而無需耗費多餘的記憶體,只有在變化的時候才會增加開銷。因此記憶體的使用更加高效。

下面我們一起來驗證下上面所說:

基本型別(Int、String等):

```swift import Foundation

var num1 = 101 var num2 = num1 print(address(of: &num1)) //0x108074090 print(address(of: &num2)) //0x108074098

var str1 = "oldbirds" var str2 = str1 print(address(of: &str1)) //0x1080740a0 print(address(of: &str2)) //0x1080740b0

//列印記憶體地址 func address(of object: UnsafeRawPointer) -> String { let addr = Int(bitPattern: object) return NSString(format: "%p", addr) as String } ```

集合型別

```swift var arr1 = [1,2,3,4,5] var arr2 = arr1 print(address(of: &arr1)) //0x600000e55510 print(address(of: &arr2)) //0x600000e55510

arr2[2] = 4 print(address(of: &arr1)) //0x600000e55510 print(address(of: &arr2)) //0x600000e55dd0 ```

自定義型別

COW 是特別新增到 Swift 陣列和字典的功能,自定義的資料型別不會自動實現。

swift struct Person { var name = "" } var p1 = Person(name: "oldbirds") print(address(of: &p1)) // 0x101ab32d0 var p2 = p1 print(address(of: &p2)) // 0x101ab32e0 p2.name = "like" print(address(of: &p2)) // 0x101ab32e0

上述程式碼可以看出,雖然將 p1 賦值給了 p2,但它倆的記憶體地址依然是不同的。由此可見自定義的結構體並不能支援 Copy-on-Write。

Copy-on-Write 如何實現的

你可以在 OptimizationTips.rst 裡發現如下程式碼:

```swift final class Ref { var val : T init(_ v : T) {val = v} }

struct Box { var ref : Ref init(_ x : T) { ref = Ref(x) }

var value: T {
    get { return ref.val }
    set {
      if (!isKnownUniquelyReferenced(&ref)) {
        ref = Ref(newValue)
        return
      }
      ref.val = newValue
    }
}

} ```

isKnownUniquelyReferenced用來檢查某個例項是不是唯一的引用。

該例子顯示瞭如何用一個引用型別去實現一個擁有 Copy-on-Write 特性的泛型值型別T。當你呼叫 set 的時候判斷是否有多個 reference,如果是多個 reference 則進行拷貝,反之則不會。

```swift struct Persion { var name = "oldbirds" } let oldbirds = Persion() var box = Box(oldbirds) var box2 = box // box2 與 box 共享 box.ref print(box.value.name) // oldbirds print(box2.value.name) // oldbirds

box2.value.name = "like" // box2 會建立新的 ref print(box.value.name) // oldbirds print(box2.value.name) // like ```

Swift 標準庫中大量使用了這種技術。

有了上面的技術理論,我們一起來運用 COW 技術:

```swift import UIKit import PlaygroundSupport

final class Box { var value: A init(_ value: A) { self.value = value } }

/// 高斯模糊 struct GaussianBlur { private var boxedFilter: Box = { var filter = CIFilter(name: "CIGaussianBlur", parameters: [:])! filter.setDefaults() return Box(filter) }()

private var filter: CIFilter {
    get { boxedFilter.value }
    set { boxedFilter = Box(newValue) }
}

private var filterForWriting: CIFilter {
    mutating get {
      if !isKnownUniquelyReferenced(&boxedFilter) {
        filter = filter.copy() as! CIFilter
        print("😄拷貝filter,\(address(of: &self))")
      } else {
        print("共享filter, \(address(of: &self))")
      }
      return filter
    }
}

var inputImage: CIImage {
    get { return filter.value(forKey: kCIInputImageKey) as! CIImage }
    set { filterForWriting.setValue(newValue, forKey: kCIInputImageKey) }
}

var radius: Double {
    get { return filter.value(forKey: kCIInputRadiusKey) as! Double }
    set { filterForWriting.setValue(newValue, forKey: kCIInputRadiusKey) }
}

var outputImage: CIImage? {
  return filter.outputImage
}

}

let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 660))

let imgUrl = Bundle.main.url(forResource: "6924717", withExtension: "jpeg")! let beginImage = CIImage(contentsOf: imgUrl)! var gaussianBlur = GaussianBlur() gaussianBlur.radius = 5 // 共享 gaussianBlur.inputImage = beginImage // 共享 let filterImg = UIImageView(frame: CGRect(x: 10, y: 10, width: 300, height: 200)) filterImg.image = UIImage(ciImage: gaussianBlur.outputImage!) view.addSubview(filterImg)

print("\n") var gaussianBlur2 = gaussianBlur gaussianBlur2.radius = 10 gaussianBlur2.inputImage = beginImage let filterImg2 = UIImageView(frame: CGRect(x: 10, y: 220, width: 300, height: 200)) filterImg2.image = UIImage(ciImage: gaussianBlur2.outputImage!) view.addSubview(filterImg2) PlaygroundPage.current.liveView = view

print("\n") var gaussianBlur3 = gaussianBlur gaussianBlur3.radius = 2 let filterImg3 = UIImageView(frame: CGRect(x: 10, y: 440, width: 300, height: 200)) filterImg3.image = UIImage(ciImage: gaussianBlur3.outputImage!) view.addSubview(filterImg3) PlaygroundPage.current.liveView = view

print("OK") ```

輸出結果:

cart

```swift 共享filter, 0x107547678 共享filter, 0x107547678 共享filter, 0x107547678 共享filter, 0x107547678

😄拷貝filter,0x107547688 共享filter, 0x107547688 共享filter, 0x107547688 共享filter, 0x107547688

😄拷貝filter,0x107547698 共享filter, 0x107547698 OK ```

總結:

  • Copy-on-Write 是一種用來優化佔用記憶體大的值型別的拷貝操作的機制。
  • 對於Int,Double,String 等基本型別的值型別,它們在賦值的時候就會發生拷貝。
  • 對於 Array、Dictionary、Set 型別,當它們賦值的時候不會發生拷貝,只有在修改的之後才會發生拷貝。
  • 對於自定義的資料型別不會自動實現COW,可按需實現。