起因
对于Swift中的值类型,为了避免不必要的拷贝行为,使用了copy on write 的优化手段,那么它是怎么实现的呢?
认知Copy-On-Write
定义一个Swift中的Array
|
|
虽然Array是一个struct
,但numbers1中的数字并不保存在numbers1对象里,它们会另外存储在系统堆内存
中。numbers1对象里,只会保存指向堆内存的一个引用。我们可以用下面的代码确认这个事情:
|
|
可以看到,这段代码的返回的结果是8,也就是一个64位内存地址占用的空间,事实上,无论数组里存放多少元素,一个Array对象的大小都是一个内存地址的大小。
此时,如果我们复制一个numbers1对象:
由于Array实现了Copy-On-Write机制,numbers1和numbers2会指向系统堆中的同一个位置。直到我们修改了其中的一个对象:
这时,numbers2就会拷贝numbers1的内容,并把数组第一个位置的值设置成11。但是,假设没有numbers2,分配出来的系统堆就只会有numbers1对象的一个引用。这时,先拷贝原数组的值,再进行修改就显得多余了,作为一项可以优化的手段,我们可以选择直接在numbers1的原始内存中修改。
以上谈到的这个机制,就是Swift中的Copy-On-Write。通过之前的描述你应该能感受到,不同的自定义类型很难存在通用的COW实现机制。因此,这不是一个Swift语言的福利,我们需要自己编写额外的代码。
并且,通过观察Array的工作机制不难发现,当一个值类型中包含引用类型的对象时,为了在拷贝对象时的值语义正确,我们必须明确处理被包含的引用对象的拷贝规则。因此,也可以说,COW并不是一个可有可无的优化手段,在某些情况下,还是我们必须思考和处理的问题。
接下来,我们就自己实现一个支持COW的MyArray,以此,加深对COW运行机制的了解。
一个简单粗暴的Copy-On-Write实现
首先,为了让所有的MyArray对象共享存储元素的空间,我们让它包含一个NSMutableArray对象:
然后,为了在操作MyArray对象时隐藏data,我们再给它添加一个插入元素的方法:
在创建新的MyArray对象时,为了实现值语义,我们让self.data等于了init参数的一个拷贝。但这样做并没有Copy-On-Write的效果,我们看下面的例子:
在上面的代码里,尽管m和n都是常量,但data是一个引用类型,我们仍旧可以修改它引用的内容,这种修改并不会被认为是修改MyArray对象本身。不信,你回头去看,我们甚至都不需要使用mutating来修饰append(:)方法。
但是,把m拷贝到n之后,尽管我们修改了m,但m和n引用相等的比较结果,仍旧是true。也就是说,m和n中的data仍旧是同一个对象。当然,这也不意外,毕竟我们没有特别处理拷贝MyArray对象时,data引用内容的处理方式。
那么,究竟该如何实现COW的效果呢?
由于Swift并不像C++一样允许我们通过拷贝构造函数来明确定义对象的拷贝行为。我们只能在需要COW的属性上下功夫,把它用一个computed property封装起来。然后,把所有修改属性的操作,都交由这个computed property完成。
例如,我们给MyArray.data添加一个新的属性:
很简单,每当读取dataCOW的时候,我们就创建一个data的拷贝。但是,由于我们在get里修改了data,就像我们之前提到的,这也是一个修改self的行为,因此,我们也要使用mutating来修饰。
接下来,所有要对data的修改操作,我们可以使用dataCOW来完成。例如:append(:)方法可以修改成这样:
|
|
这样,append(:)就会在一个data的拷贝上添加元素了。并且,由于append(:)使用了mutating修饰,我们也无法再修改常量MyArray对象了。此时,编译器会对m.append(11)这行代码报错。这样就在实现了COW效果的同时,完美隐藏了MyArray内部使用了引用类型作为数据存储的事实。我们把m改成变量,这时之前的引用相等比较就会返回false了。
怎么样,是不是这个实现方式比你想象的要简单的多?的确,这样可以工作,但是却很暴力,它存在一个明显的硬伤,当我们需要多次修改MyArray对象,而只需要最后的结果时,所有中间的拷贝操作就成了浪费。例如,我们通过for循环给MyArray添加内容:
|
|
如果你理解了MyArray的COW机制,就会立刻发现for循环每执行一次,MyArray.data就会被拷贝一次。但中间过程的拷贝明显是没意义的。因此,这个方案有点儿过于简单粗暴了。