Swift|Swift - 结构体优化- 写时复制

我们知道Swift中推荐使用具有值语义的结构体,而不是具有可变性的引用语义.

let arr = [1,2,4,3] var arr2 = arr // 这一步会复制arr中的内容给arr2

Swift标准库中Array结构体是具有写时复制(copy-on-write)的.
如上代码在创建一个新Array变量并且将arr赋值给它的时候,会复制这个arr来确保它们各自独立. (这是我们平常的理解)
但是如上这种情况下,并没有对arr2进行任何的操作,arr2与arr独立开似乎并没有什么作用.这种情况下因为赋值进行复制一份内容是比较耗费性能的事.
所以通过写时复制要实现的就是,起初给arr2赋值后,arr和arr2的内容的内存地址是相同的.这两个数组共享了它们的存储部分.不过当我们改变arr2的时候这个内存会被检测到,这个时候再对这块内存的内容进行复制.. 因为复制操作只在必要的时候发生,节省了性能消耗.
来验证一下
按照"写时复制"的要求,上面代码 arr 和 arr2中的元素的内存地址是同一块的,
// 在两行代码后打断点,分别打印两个数组第一个元素的地址 (lldb) po withUnsafePointer(to: &arr[0], {$0}) ? 0x000060400046a920 - pointerValue : 105827998804256(lldb) po withUnsafePointer(to: &arr2[0], {$0}) ? 0x000060400046a920 - pointerValue : 105827998804256

虽然arr和arr2是两个不同的结构体了,arr2的内容并没有真正复制一份.
当我们增加一段修改arr2的代码之后
let arr = [1,2,4,3] var arr2 = arr arr2[1] = 3

我们在代码执行后打印它们的第0个元素的地址
(lldb) po withUnsafePointer(to: &arr[0], {$0}) ? 0x000060400026db60 - pointerValue : 105827996719968(lldb) po withUnsafePointer(to: &arr2[0], {$0}) ? 0x000060400026daa0 - pointerValue : 105827996719776

虽然我们修改的是数组中第一个元素,但是第0个元素和整个内容的地址都变了,可见它的内容确实在修改之前全部复制了一份..
自定义结构体实现写时复制
Swift标准库中的集合类型是已经实现了写时复制的.但是如果我们自己创建一个结构体,它并不免费具备这种特性.
struct MyData { fileprivate(set) var data : NSMutableDatainit(_ data: Data) { self.data = NSMutableData(data: data).mutableCopy() as! NSMutableData } func append(_ otherData: Data) { data.append(otherData) } }

模仿系统中Data集合的操作,我们自定义了一个MyData结构体..获取其中data数据是通过只读的data属性.
当我们创建下列代码:
let theData = https://www.it610.com/article/Data(base64Encoded:"wAEP/w==")! let aData = https://www.it610.com/article/MyData(theData) var bData = aData bData.append(theData)

因为将aData赋值给bData的时候是进行的浅复制.所以MyData中的data对象本身不会被复制,而只有对象的引用会被复制.在给bData进行append操作后
po aData.data //// 我们发现aData也发生了变化

【Swift|Swift - 结构体优化- 写时复制】这不是我们想要的目的,我们现在只是创建了一个具有值语义的结构体.我们想要结构体在复制之后不会彼此间相互独立.并且在写入操作时候再进行必要的复制.
所以我们修改代码:
struct MyData { fileprivate(set) var data : NSMutableData fileprivate var dataForWriting: NSMutableData { mutating get { data = data.mutableCopy() as! NSMutableData return data } } init(_ data: Data) { self.data = NSMutableData(data: data).mutableCopy() as! NSMutableData } mutating func append(_ otherData: Data) { // 每次进行添加操作前调用dataForWriting的get方法 对data进行复制,来保证与之前的对象独立. dataForWriting.append(otherData) } }

我们引入了一个dataForWriting计算型属性来间接修改data.而不是直接修改data. 每次调用append进行"写入"操作的时候便会调动这个计算属性对data复制一份. 如此这个结构体遍完全的具有了值语义.. 而且是在append的时候才进行的复制.
性能优化
但是每次进行append操作都会复制一次原先的data,这样显然是效率极低的. 如果我们已经发现属性中的data属性只有自己引用,修改它的值不会影响到其它任何结构体..那么我们完全就可以直接修改它不需要频繁的复制了.
在Swift中就有一个函数可以检测某一个引用的唯一性.将一个Swift类传递给这个函数,如果没有其它强引用变量指向它,函数将返回true,如果还有其它引用指向它则返回false..对于OC的类它会直接返回false.所以直接对NSmutableData使用这个函数没有意义..我们可以创建一个Swift类,来将任意的OC对象封装到这个类中..
// Swift的class容器,传入一个OC类型 class Box { var unbox: T init(_ value: T) { self.unbox = value } }

接着我们重构一下MyData类
struct MyData { fileprivate(set) var data : Box fileprivate var dataForWriting: NSMutableData { mutating get { // 检验 data是否是唯一引用 if !isKnownUniquelyReferenced(&data) { data = Box(data.unbox.mutableCopy() as! NSMutableData) } return data.unbox } } init(_ data: Data) { self.data = Box(NSMutableData(data: data).mutableCopy() as! NSMutableData) } mutating func append(_ otherData: Data) { dataForWriting.append(otherData) } }

看起来是麻烦了一点.不过这样才能保证结构体的高效操作..
let aData = https://www.it610.com/article/MyData(theData) var bData = aData for _ in 0 ..< 5 { bData.append(theData) }

接着我们执行5次append操作..发现data的mutablecopy只执行了一次.因为拷贝一次后它的引用者也就只有这个结构体本身了.
虽然我们在创建结构体的时候大部分时候属性中都是一些值语义的..我们要确保结构体的不可变性.

    推荐阅读