swift中copy on write的研究

本文部分相关知识参考了《Advanced Swift》以及泊学网

什么是写时复制(copy-on-write)

swift标准库中,像Array,Dictionary,Set这些结构体都实现了写时复制技术,那到底什么是写时复制呢?我们看一个例子就明白了,如下:

1
2
var a = [1,2,3]
var b = a

如上面的代码所示,我们创建了一个数组a,同时我们将a赋给b,这是很常见的操作,其实这个时候,a和b是两个独立的值,但是在内部,a和b都是指向内存中同一个位置的引用,其实这两个数组共享了他们的存储部分,也就是说在堆内存中只有一份这样的数据,但是a和b都引用了这份数据。如果我们再增加一个操作,如下:

1
2
a.append(4)//[1,2,3,4]
b//[1,2,3]

在这种时候,swift就会对内存进行复制,然后在复制的值上进行处理,从而不影响原来的那份值,也就是说只在必要的时候去复制。

总结一下就是:每当值类型内容发生变化时,它会首先检查对存储缓冲区的引用是否唯一,如果只有自己一个引用,那就在这份存储数据上原地修改即可,不会有复制发生。如果发现对存储缓冲区有不止一个引用,例如上面的例子,那么就会先进行复制,然后对复制的值进行操作,以免影响其他引用。总之只在必要时复制,否则不会复制。

在标准库中的集合类型,譬如Array,Dictionary,Set这些结构体都实现了写时复制技术,我们只管使用即可,它保持了值语义,同时也优化了性能,避免了昂贵的不必要的复制操作,但是这个福利并不是所有值类型都有,当我们自己创建一些值类型的时候,特别是我们自定义的值类型里包含引用类型对象时,为了保持值语义,我们就得自己来实现copy-on-write了。

我们自己动手来实现COW

一个比较粗糙的copy-on-write实现

譬如我们来实现一个我们自定义的Array,里面用来存储数据的是一个oc对象,如下:

1
2
3
4
5
6
struct CustomArray {
var elements: NSMutableArray
init(_ elements: NSMutableArray) {
self.elements = elements.mutableCopy() as! NSMutableArray
}
}

为了实现值语义,我们将传入的elements复制一份,赋给内部的elements变量。同时,为了在操作CustomArray对象时隐藏elements属性,我们为它添加一个方法,如下:

1
2
3
4
5
extension CustomArray {
func append(_ element: Any) {
elements.insert(element, at: elements.count)
}
}

这是我们平常会写的代码,但是这样会有问题,看如下操作:

1
2
3
4
let aArr = CustomArray([1,2,3])
let bArr = aArr
aArr.append(4)
aArr.elements === bArr.elements//true

aArrbArr是两个独立的结构体,按照我们之前对copy-on-write的理解,当aArr内容发生变更时,会被复制一份,那么aArrbArr将不再享有同一份内存数据,而是两份独立的数据,内存不再共享,但是上面的aArrbArrelements却仍旧是同一个引用对象,这个可以理解,因为我们没有对它做任何特殊处理,但是这样子却是不符合值语义的,所以为了保持值语义,我们需要来实现copy-on-write,我们来修改一下代码,我们创建一个计算属性elementsCOW,每当自定义数组需要改变内容时,我们都使用这个计算属性,来对内部的elements进行复制,然后使用这份拷贝来进行操作。相应的append方法都会使用这个计算属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
var elementsCOW: NSMutableArray {
mutating get {
elements = elements.mutableCopy() as! NSMutableArray
return elements
}
}

extension CustomArray {
mutating func append(_ element: Any) {
elementsCOW.insert(element, at: elementsCOW.count)
}
}

这样子的话,每次append新的元素,都会对内部elements进行拷贝后,使用拷贝的值进行添加操作,完全符合了值语义,如下:

1
2
3
4
var aArr = CustomArray([1,2,3])
let bArr = aArr
aArr.append(4)
aArr.elements === bArr.elements//false

如果这样就完成了,那就太简单了,5b39fae014738

如果这样,我们确实为自定义的数组实现了值语义,每次使用修改时,都会复制一份,但是你会发现,如果这样,像上述的例子那样,如果没有bArr,只有一个aArr,我们每次都对aArr进行修改都会被复制一份,譬如这样:

1
2
3
for i in 1...10 {
aArr.append(i)//aArr会被复制10次
}

上述的aArr会被复制10次,这样太不合理,内存明明只有一个引用,不会影响到其他对象使用,太浪费了。所以我们需要对上述代码在做一次更改,只在必要的时候复制。我们的思路是这样的,当值被更改时,判断对象是否只有一个引用,如果是,那就不复制,如果不是,那就需要复制一份。

一个更高效的copy-on-write实现

为此我们需要用到一个方法swift标准库中的isKnownUniquelyReferenced,对于Swift原生类对象,只有单一引用时返回true,否则返回false;对于Objective-C中的类对象,总是返回false,所以我们还不能直接用,我们需要把oc对象封装在swift对象之内,所以我们这样来做,如下:

1
2
3
4
5
6
7
final class Pack<U> {
var unpack: U

init(_ unpack: U) {
self.unpack = unpack
}
}

我们自定义一个类型Pack用来打包oc对象,使用final,不想让它被继承。然后我们可以修改一下上面的代码,内部元素elements可以打包,并且判断打包好后的swift类型是否只有唯一引用,不是的话就复制,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct CustomArray {
var elementsCOW: NSMutableArray {
mutating get {
if !isKnownUniquelyReferenced(&elements) {
elements = Pack(elements.unpack.mutableCopy() as! NSMutableArray)
print("occur copy")
}
return elements.unpack
}
}

var elements: Pack<NSMutableArray>

init(_ elements: NSMutableArray) {
self.elements = Pack(elements.mutableCopy() as! NSMutableArray)
}
}

ok,再次执行原先的代码:

1
2
3
for i in 1...10 {
aArr.append(i)//aArr将不再被复制
}

至此,一个更加高效的写时复制完成了,它保证了我们自定义数组的值语义,并且只在必要的时候才去复制。

5b3a1499291b6

总结

在swift中,值类型是非常重要的类型,你会发现原来OC中是引用类型的数组,字典等在swift中都是值类型,因为更安全,高效。我们在自定义一些值类型时,为了维护值语义,通常都需要在每次变更时,都进行昂贵的复制操作,但是写时复制技术避免了在非必要的情况下的复制操作。希望以上这些可以帮到你。

Author: Uncle Peter
Link: http://yoursite.com/2018/03/05/swift中copy on write的研究/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.