在Swift中,如果你具有较大的值类型对象或数据并且必须将其作为参数分配或传递给一个函数,则在性能方面复制它代价可能是非常昂贵的,因为必须将所有基础数据复制到内存中的其他位置。Advice: Use copy-on-write semantics for large values,苹果建议当复制大的值类型数据的时候,使用写时复制技术,那什么是写时复制呢?我们现在看一段代码:
import Foundation
func print(address o: UnsafeRawPointer ) {
print(String(format: "%p", Int(bitPattern: o)))
}
var array1: [Int] = [0, 1, 2, 3]
var array2 = array1
print(address: array1) //0x600000078de0
print(address: array2) //0x600000078de0
array2.append(4)
print(address: array2) //0x6000000aa100
我们看到当array2
的值没有发生变化的时候,array1
和array2
指向同一个地址,但是当array2
的发生变化时,array2
指向地址也变了,很奇怪是吧。
《Advanced Swift》关于写时复制解释的非常好:
在 Swift 标准库中,像是 Array,Dictionary 和 Set 这样的集合类型是通过一种叫做写时复制 (copy-on-write) 的技术实现的。我们这里有一个整数数组:
var x = [1,2,3]
vary=x
如果我们创建了一个新的变量 y,并且把 x 赋值给它时,会发生复制,现在 x 和 y 含有的是独立
的结构体:
vary=x
在内部,这些 Array 结构体含有指向某个内存的引用。这个内存就是数组中元素所存储的位置。 两个数组的引用指向的是内存中同一个位置,这两个数组共享了它们的存储部分。不过,当我 们改变 x 的时候,这个共享会被检测到,内存将会被复制。这样一来,我们得以独立地改变两个 变量。昂贵的元素复制操作只在必要的时候发生,也就是我们改变这两个变量的时候发生复制:
x.append(5)
y.removeLast()
x // [1, 2, 3, 5]
y // [1, 2]
这种行为就被称为写时复制。它的工作方式是,每当数组被改变,它首先检查它对存储缓冲区 的引用是否是唯一的,或者说,检查数组本身是不是这块缓冲区的唯一拥有者。如果是,那么 缓冲区可以进行原地变更;也不会有复制被进行。不过,如果缓冲区有一个以上的持有者 (如本 例中),那么数组就需要先进行复制,然后对复制的值进行变化,而保持其他的持有者不受影响。
作为一个结构体的作者,你并不能免费获得写时复制的行为,你需要自己进行实现。当你自己的类型内部含有一个或多个可变引用,同时你想要保持值语义时,你应该为其实现写时复制。
为了维护值语义,通常都需要进行在每次变更时,都进行昂贵的复制操作,但是写时复制技术
避免了在非必要的情况下的复制操作。
苹果在Advice: Use copy-on-write semantics for large values中教我们怎么去使用 copy-on-write 技术。
我们使用class
,这是一个引用类型,因为当我们将引用类型分配给另一个时,两个变量将共享同一个实例,而不是像值类型一样复制它。
final class Ref<T> {
var value: T
init(value: T) {
self.value = value
}
}
然后,我们可以创建一个struct
包装Ref
:
struct Box<T> {
private var ref: Ref<T>
init(value: T) {
ref = Ref(value: value)
}
var value: T {
get { return ref.value }
set {
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
ref.value = newValue
}
}
}
由于struct是一个值类型,当我们将它分配给另一个变量时,它的值被复制,而属性ref的实例仍由两个副本共享,因为它是一个引用类型。
然后,我们第一次更改两个Box变量的值时,我们创建了一个新的ref实例,这要归功于:
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
这样,两个Box变量不再共享相同的ref实例。
为了提供高效的写时复制特性,我们需要知道一个对象是否是唯一的。如果它是唯一引用,那么我们就可以直接原地修改对象。否则,我们需要在修改前创 建对象的复制。在 Swift 中,我们可以使用
isKnownUniquelyReferenced
函数来检查某个引 用只有一个持有者。如果你将一个 Swift 类的实例传递给这个函数,并且没有其他变量强引用 这个对象的话,函数将返回true
。如果还有其他的强引用,则返回false
。不过,对于 Objective-C 的类,它会直接返回 false。
比如我们想在一个使用struct
类型的User中使用copy-on-write的:
struct User {
var identifier = 1
}
let user = User()
let box = Box(value: user)
var box2 = box // box2 shares instance of box.ref
box2.value.identifier = 2
Advice: Use copy-on-write semantics for large values
When to Use Value Types and Reference Types in Swift
Use Copy-On-Write With Swift Value Types