ios 底层

探究写时复制

2020-01-08  本文已影响0人  leejnull

写时复制

和Objective-C不同,在Swift中,Array、Dictionary、Set这样的集合不再是引用类型而是值类型了,这意味着,每次传递不再是传递指针而是一个Copy后的值,但是如果每次都要Copy一次的话就会太浪费性能,所以这时候就要用到一个写时复制(copy-or-write)的技术。

var x = [1, 2, 3]
var y = x
x.append(5)
y.removeLast()
x // [1, 2, 3, 5]
y // [1, 2]

在内部,这些Array的结构体含有指向某个内存的引用。这个内存就是数组中元素所存储的位置。两个数组的引用指向的是内存中同一个位置,这两个数据共享了它们的存储部分。当我们改变 x 的时候,这个共享会被检测到,内存将会被复制。所以说,复制操作只会则必要的时候发生。

这种行为被称为写时复制。它的工作方式是,每当数组被改变,它首先检查它对存储缓冲区的引用是否是唯一,或者说,检查数组本身是不是这块缓冲区的唯一拥有者。如果是,那么缓冲区可以进行原地变更;也不会有复制被进行。如果缓冲区有一个以上的持有者,那么数组就需要先进行复制,然后对复制的值进行变化,而保持其他的持有者不受影响。

实现写时复制

使用 NSMutableData 作为内部引用类型来实现 Data 结构体。

struct MyData {
    var _data: NSMutableData
    var flag: String?
    init(_ data: NSData) {
        _data = data.mutableCopy() as! NSMutableData
    }
}

extension MyData {
    func append(_ byte: UInt8) {
        var mutableByte = byte
        _data.append(&mutableByte, length: 1)
    }
}

let theData = NSData(base64Encoded: "wAEP/w==")!
var x = MyData(theData)
x.flag = "flag"
var y = x
x._data == y._data
y.flag = "new flag"

x.append(0x55)
print(x)    // MyData(_data: <c0010fff 55>, flag: Optional("flag"))
print(y)    // MyData(_data: <c0010fff 55>, flag: Optional("new flag"))

MyData虽然是一个结构体,是一个值类型,对于值类型数据遵循写时复制的特性,但是对于内部 NSMutableData 这样的引用类型,多个 MyData 的变量指向的还是同一个 NSMutableData 地址。所以我们要手动实现 NSMutableData 的写时复制

简单的实现
struct MyData {
    fileprivate var _data: NSMutableData
    fileprivate var _dataForWriting: NSMutableData {
        mutating get {
            _data = _data.mutableCopy() as! NSMutableData
            return _data
        }
    }
    var flag: String?
    
    init() {
        _data = NSMutableData()
    }
    
    init(_ data: NSData) {
        _data = data.mutableCopy() as! NSMutableData
    }
}

extension MyData {
    mutating func append(_ byte: UInt8) {
        var mutableByte = byte
        _dataForWriting.append(&mutableByte, length: 1)
    }
}

不直接变更 _data,通过一个 _dataForWriting 来访问。每次都会复制 _data 并将该复制返回。当我们调用 append 时,将会进行复制

let theData = NSData(base64Encoded: "wAEP/w==")!
var x = MyData(theData)
x.flag = "flag"
var y = x
x._data == y._data
y.flag = "new flag"

x.append(0x55)

print(x)    // MyData(_data: <c0010fff 55>, flag: Optional("flag"))
print(y)    // MyData(_data: <c0010fff>, flag: Optional("new flag"))

但是这样有一个问题,多次 append 时,就会非常浪费,因为每次都要 copy

高效的方式

我们可以通过判断一个对象是否是唯一的引用,来决定是否需要对这个对象进行复制。如果它是唯一引用,那就直接修改对象,否则,需要在修改前创建该对象的复制。
在 Swift 中,通过 isKnownUniquelyReferenced 函数来检查某个引用只有一个持有者。只有一个返回 true,否则返回 false。对于 OC 类,它会直接返回 false,我们需要创建一个 Swift 的类来包装 OC 类

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

var x = Box(NSMutableData())
isKnownUniquelyReferenced(&x)   // true

var y = x
isKnownUniquelyReferenced(&y)   // false

让我们再写一个循环

struct MyData {
    fileprivate var _data: Box<NSMutableData>
    fileprivate var _dataForWriting: NSMutableData {
        mutating get {
            if !isKnownUniquelyReferenced(&_data) {
                _data = Box(_data.unbox.mutableCopy() as! NSMutableData)
                print("Making a copy")
            }
            return _data.unbox
        }
    }
    
    init() {
        _data = Box(NSMutableData())
    }
    
    init(_ data: NSData) {
        _data = Box(data.mutableCopy() as! NSMutableData)
    }
}

extension MyData {
    mutating func append(_ byte: UInt8) {
        var mutableByte = byte
        _dataForWriting.append(&mutableByte, length: 1)
    }
}

var bytes = MyData()
var copy = bytes
for byte in 0..<5 as CountableRange<UInt8> {
    print("Appending 0x\(String(byte, radix: 16))")
    bytes.append(byte)
}
print(bytes)
print(copy)

/*
Appending 0x0
Making a copy
Appending 0x1
Appending 0x2
Appending 0x3
Appending 0x4
MyData(_data: __lldb_expr_26.Box<__C.NSMutableData>)
MyData(_data: __lldb_expr_26.Box<__C.NSMutableData>)
*/

可以看到当第一次 append 的时候,拷贝了一份引用,之后因为新拷贝的引用是惟一的,就没有进行复制操作

来自 https://leejnull.github.io/2020/01/03/2020-01-03-02/

上一篇下一篇

猜你喜欢

热点阅读