美文网首页ios 底层
探究写时复制

探究写时复制

作者: leejnull | 来源:发表于2020-01-08 22:34 被阅读0次

    写时复制

    和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/

    相关文章

      网友评论

        本文标题:探究写时复制

        本文链接:https://www.haomeiwen.com/subject/zkheactx.html