什么是COW

我们都知道Swift有值类型和引用类型,而值类型在被赋值或被传递给函数时是会被拷贝的。在Swift中,所有的基本类型,包括整数、浮点数、字符串、数组和字典等都是值类型,并且都以结构体的形式实现。那么,我们在写代码时,这些值类型每次赋值传递都是会重新在内存里拷贝一份吗?

答案是否定的,想象一下,假如有个包含上千个元素的数组,然后你把它copy一份给另一个变量,那么Swift就要拷贝所有的元素,即使这两个变量的数组内容完全一样,这对它性能来说是多么糟糕。

在The Swift Programming Language (Swift 4.1) Classes and Structures 章节中有如下一段话,也明确地提到了Swift对其实现做了优化,可避免不必要的复制:

The description above refers to the “copying” of strings, arrays, and dictionaries. The behavior you see in your code will always be as if a copy took place. However, Swift only performs an actual copy behind the scenes when it is absolutely necessary to do so. Swift manages all value copying to ensure optimal performance, and you should not avoid assignment to try to preempt this optimization.

而这个优化方式就是 Copy-On-Write(写时复制),即只有当这个值需要改变时才进行复制行为。

例子

首先,让我们看下面的例子我们更容易理解,我们创建了数组arr1,然后将arr1赋值给arr2,再给arr2数组添加多一个元素,我们通过查看其地址变化来确定是否进行了拷贝行为。

let arr1 = [1, 2, 3, 4]
var arr2 = arr1
//断点1
arr2.append(2) 
//断点2

由于网上很多有关获取内存地址的方法打印出来有差异,在此,使用lldb命令fr v -R [object] 来查看对象内存结构。

断点1位置,打印arr1, arr2 内存结构如下,我们可以看到arr1arr2内存地址都是0x000060400047e480,说明arr1arr2此时是共享同一个实例

(lldb) fr v -R arr1
(Swift.Array<Swift.Int>) arr1 = {
  _buffer = {
    _storage = {
      rawValue = 0x000060400047e480 {
        Swift._ContiguousArrayStorageBase = {
          Swift._SwiftNativeNSArrayWithContiguousStorage = {
            Swift._SwiftNativeNSArray = {}
          }
          countAndCapacity = {
            _storage = {
              count = {
                _value = 4
              }
              _capacityAndFlags = {
                _value = 8
              }
            }
          }
        }
      }
    }
  }
}
(lldb) fr v -R arr2 
(Swift.Array<Swift.Int>) arr2 = {
  _buffer = {
    _storage = {
      rawValue = 0x000060400047e480 {
        Swift._ContiguousArrayStorageBase = {
          Swift._SwiftNativeNSArrayWithContiguousStorage = {
            Swift._SwiftNativeNSArray = {}
          }
          countAndCapacity = {
            _storage = {
              count = {
                _value = 4
              }
              _capacityAndFlags = {
                _value = 8
              }
            }
          }
        }
      }
    }
  }
}

断点2位置,此时arr2添加了新元素,打印arr2,内存结构如下,我们可以看到arr2内存地址已经变成了0x00006000000b32c0,说明此时它们不再共享同一个实例,arr2对应的值进行了拷贝行为

(lldb) fr v -R arr2 
(Swift.Array<Swift.Int>) arr2 = {
  _buffer = {
    _storage = {
      rawValue = 0x00006000000b32c0 {
        Swift._ContiguousArrayStorageBase = {
          Swift._SwiftNativeNSArrayWithContiguousStorage = {
            Swift._SwiftNativeNSArray = {}
          }
          countAndCapacity = {
            _storage = {
              count = {
                _value = 5
              }
              _capacityAndFlags = {
                _value = 16
              }
            }
          }
        }
      }
    }
  }
}

由此可见,arr2未做修改时,arr1arr2是共享同一个实例

具体实现

在结构体内部存储了一个指向实际数据的引用reference,在不进行修改操作的普通传递过程中,都是将内部的reference的应用计数+1,在进行修改时,对内部的reference做一次copy操作,再在这个复制出来的数据进行真正的修改,防止和之前的reference产生意外的数据共享

值类型内嵌引用类型

我们已经知道值类型在不进行修改操作的普通数据传递时不进行拷贝行为,但是修改时就会进行拷贝行为,但是所有的值类型都是这样的吗,如果,这个值类型内嵌了引用类型呢?

class TestClass {
    var value: String
    init(value: String) {
        self.value = value
    }
}

struct TestStruct {
    var testClass = TestClass(value: "hello")
}

var test1 = TestStruct()
var test2 = test1

print(test1.testClass.value)
print(test2.testClass.value)
// 断点1
test1.testClass.value = "hello world"
// 断点2
print(test1.testClass.value)
print(test2.testClass.value)

其打印结果如下:

hello
hello
hello world
hello world

再用lldb查看下其内存结构:

// 断点1 位置 test1 和 test2 的内存结构 
(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "hello"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}
(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "hello"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

test1 赋值给test2 后,它们的内存地址都是0x0000000101839aa0,其引用类型实例变量 testClass 的地址也都是 0x00000001005162cc ,它们共享同一个实例,其引用类型的实例变量也共享

// 断点2 位置 test1 和 test2 的内存结构 
(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}
(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

而执行test1.testClass.value = "hello world" 后,test1test2 的内存地址不变,其实例变量 testClass 地址都改变且相同,还是共享同一个实例变量,也就是说,虽然对值类型有所修改,但没有进行拷贝行为

那么如果直接修改整个testClass 呢?

test1.testClass = TestClass(value: "12345")

print(test1.testClass.value)
print(test2.testClass.value)

打印结果为:

12345
hello world

此时,再用lldb查看下其内存结构

(lldb) fr v -R test1
(TestTool.TestStruct) test1 = {
  testClass = 0x0000000101a14de0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162cc "12345"
          }
        }
        _countAndFlags = {
          _value = 5
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

(lldb) fr v -R test2
(TestTool.TestStruct) test2 = {
  testClass = 0x0000000101839aa0 {
    value = {
      _core = {
        _baseAddress = some {
          some = {
            _rawValue = 0x00000001005162c0 "hello world"
          }
        }
        _countAndFlags = {
          _value = 11
        }
        _owner = none {
          some = {
            instance_type = 0x0000000000000000
          }
        }
      }
    }
  }
}

由此可见,直接修改testClass变量,test1test1.testClass 的内存地址都变化,而test2test2.testClass 内存地址不变,说明,此时对结构体进行了拷贝行为,而testClass 这个引用类型是直接指向另一个实例,而不是对原实例进行修改

手动 COW

那么,如何上面的值类型做到写时复制呢?

我们可以让testClass 私有化,让外部无法对这个引用类型进行修改,再提供一个接口控制这个引用类型的写入操作,如下所示:

struct TestStruct {
    private var testClass = TestClass(value: "hello")

    var testValue: String {
        get {
            return testClass.value
        }
        set {
            testClass = TestClass(value: newValue)
        }
    }
}

那么对TestStruct这个结构体,可以通过计算型属性testValue来控制引用类型的修改,进行修改testClass的值时,直接指向一个新的实例,而非修改,保证了其实现写时复制

进一步优化

在Swift提供一个函数isKnownUniquelyReferenced,能检查一个类的实例是不是唯一的引用,如果是,说明实例没有被共享,我们就不需要对结构体实例进行复制,如果不是,说明实例被共享,这时对它进行更改就需要先复制。

TestStruct 优化如下:

struct TestStruct {
    private var testClass = TestClass(value: "hello")

    var testValue: String {
        get {
            return testClass.value
        }
        set {
            if isKnownUniquelyReferenced(&testClass) {
                testClass.value = newValue
            }
            else {
                testClass = TestClass(value: newValue)
            }
        }
    }
}