咱们应用写时复制 copy on write 的思维,对 NSMutableData 进行封装,以此来了解咱们的规范库的实现形式。

规范库中提供的所有的根本汇合类型都是值类型,通过写时复制的思维保障了他的高效性。汇合类型是咱们比拟罕用到的数据类型,所以理解他的性能个性很重要,咱们来一起看一下写时复制是如何工作的,并且尝试本人手动实现一个。

援用类型

举个例子,咱们比拟一下Swift的Data(构造体)和Foundation库中的NSMutableData(类)。首先咱们应用一些字节数据来初始化 NSMutableData 实例。

var sampleBytes: [UInt8] = [0x0b,0xad,0xf0,0x0d]let nsData = NSMutableData(bytes: sampleBytes, length: sampleBytes.count)

咱们应用了 let 来申明 nsData,然而像 NSMutableData 这样的援用类型不受let/var 的管制。对于援用类型来说,用 let 申明代表 nsData 这个指针不能在指向别的内存,然而他指向的这个内存中的数据是能够变动的。也就是说咱们仍然能够往 nsData 中 append 数据。

nsData.append(sampleBytes, length: sampleBytes.count)

当咱们再申明一个对象,扭转其中一个对象,另一个对象也会发生变化。

let nsOtherData = nsDatansData.append(sampleBytes, length: sampleBytes.count)// nsOtherData 也会变

如果咱们想产生一个独立的正本,咱们须要应用 mutableCopy(返回一个 Any 类型),咱们须要把返回值强转成咱们须要的 NSMutableData 类型。

let nsOtherData = nsData.mutableCopy() as! NSMutableDatansData.append(sampleBytes, length: sampleBytes.count)// nsOtherData 不变

值类型

首先咱们也是通过 sampleBytes 来初始化一个 Data。

let data = Data(bytes: sampleBytes, count: sampleBytes.count)

如果咱们应用 let 关键字,那编译器就不会容许咱们调用类型 append 这样的办法。所以如果要扭转 data 的值,要应用 var 。

var data = Data(bytes: sampleBytes, count: sampleBytes.count)data.append(contentsOf: sampleBytes)

Data 和 NSData 最次要的不同之处是:把值赋给另一个变量时或者作为参数传到办法中,Data 总是会生成一个新的正本,然而 NSData 只会生成一个新的援用,然而两个援用指向同一个内存区域。

当咱们创立 Data 的一个正本的时候,他的所有的字段都会被复制,然而又不是立即复制,因为 Data 内存有对理论内存空间的援用,所以当构造体被复制时,也只是会生成一个新的援用,只有咱们对这个新的援用批改数据是,理论的数据才会被复制。

实现写时复制

咱们本人实现一个 Data 类型来帮咱们了解写时复制是如何工作的,咱们外部应用 NSMutableData 来理论的存储数据(只是为了更快的实现,理论的Data 外部必定是用到更底层的数据结构来存储数据)。扭转数据的办法咱们只实现一个 append 办法。

struct MyData {    var data = NSMutableData()        func append(_ bytes: [UInt8]) {        data.append(bytes, length: bytes.count)    }}

咱们能够创立一个 MyData

let data = MyData()

为了能更好的打印出 data 中存储的数据,咱们能够让 MyData 实现 CustomDebugStringConvertible 协定。

extension MyData: CustomDebugStringConvertible {    var debugDescription: String {        return String(describing: data)    }}

当初咱们能够调用 append 办法了。

data.append(sampleBytes)

但这是有问题的,首先咱们的MyData是构造体,而且创立 data 应用的是let,咱们不应该能够批改他的值。

而且看上面的代码,他的复制行为也是有问题的,在咱们申明了一个新的援用时,并没有取得一个齐全独立的正本。

var copy = datacopy.append(sampleBytes)print(data)print(copy)// copy 调用 append, data 也会扭转

所以说咱们尽管创立了一个构造体,然而他并没有体现出值语义来。

目前,咱们在把 data 赋给一个新的变量时,尽管他是所有字段都复制,然而咱们MyData外部的 data 是一个 NSMutableData 援用类型,所以说 data 和 copy 这两个变量的值当初都蕴含对同一个 NSMutableData 实例的援用。

为了解决这个问题,咱们要先解决写时复制的’写时‘问题。当咱们在调用 append 办法增加数据时,咱们要把外部进行理论存储性能的data进行深拷贝,此时 咱们的 append 办法就必须加上 mutating 关键字,要不然编译器不容许批改构造体的变量。

struct MyData {    var data = NSMutableData()        mutating func append(_ bytes: [UInt8]) {        print("make a copy")        data = data.mutableCopy() as! NSMutableData        data.append(bytes, length: bytes.count)    }}

当初咱们要从新生成一个 var 类型的 data 来调用 append 办法,因为编译器不容许let 类型的调用带 mutating 关键字的办法。

var data = MyData()var copy = datacopy.append(sampleBytes)

在咱们持续之前,进行一个小的重构,并将生成 NSMutableData 实例正本的代码提取到一个独自的属性中。

struct MyData {    var data = NSMutableData()    var dataForWriting: NSMutableData {        mutating get {            print("make a copy")            data = data.mutableCopy() as! NSMutableData            return data        }    }        mutating func append(_ bytes: [UInt8]) {        dataForWriting.append(bytes, length: bytes.count)    }}

让写时复制更高效

目前咱们的写时复制是非常简单的,就是每次当咱们调用 append 的时候,都会拷贝,不论咱们是不是这个实例的惟一持有者。

for _ in 0..<10 {    data.append(sampleBytes)}// making a copy 会打印10次

其实真正须要执行复制操作的是当咱们把data赋值给另一个变量后,这时调用append 办法,因为此时有两个援用,所以须要进行深拷贝。当拷贝完结后,这两个都是援用指向的都是齐全独立的备份了,所以再一次调用时就不须要拷贝了。

所以说咱们的MyData构造没有问题,然而屡次拷贝会升高性能。咱们能够应用 isKnownUniquelyReferenced 这个办法来帮忙咱们实现想要的成果。

var dataForWriting: NSMutableData {    mutating get {        if isKnownUniquelyReferenced(&data) {            return data        }        print("make a copy")        data = data.mutableCopy() as! NSMutableData        return data    }}

尽管咱们当初加上了 isKnownUniquelyReferenced 查看,然而运行一下测试代码还是会copy屡次,那是因为 isKnownUniquelyReferenced 办法只是对Swift类型有成果,如果是传入的OC类型的对象,总是会返回false,所以咱们应该应用一个Swift类型来包装一下这个data类型。

final class Box<A> {    let unbox: A    init(_ value: A) {        self.unbox = value    }}

咱们应用这个Box类来包装 NSMutableData , 最终咱们的MyData 变成上面这样子

struct MyData {    var data = Box(NSMutableData())    var dataForWriting: NSMutableData {        mutating get {            if isKnownUniquelyReferenced(&data) {                return data.unbox            }            print("make a copy")            data = Box(data.unbox.mutableCopy() as! NSMutableData)            return data.unbox        }    }        mutating func append(_ bytes: [UInt8]) {        dataForWriting.append(bytes, length: bytes.count)    }}

当初咱们的代码只对 NSMutableData 实例copy一次。

var data = MyData()var copy = datafor _ in 0..<10 {    data.append(sampleBytes)}// Prints:// making a copy 一次

规范库中数组和字典的实现形式其实也是相似的,只是他们用了更低级的数据结构来存储,咱们这样手动实现一次写时复制,有助于咱们更好了解他们外部的性能。

写时复制留神点

写时复制很高效,然而他不是适应于所有的场景,比如说咱们下面的for循环是能够的,然而如果咱们应用reduce来实现下面的循环,他就不起作用了。

(0..<10).reduce(data) { result, _ in    var copy = result    copy.append(sampleBytes)    return copy}

这个实现形式会生成 10 个正本,因为当咱们调用 append 时,总是有两个变量——copy 和 result——援用指向同一个实例。

所以咱们应该留神咱们代码中那些产品大量不必要正本的中央,不过咱们个别都不会这么写,所以说问题不大。