探究写时复制

写时复制

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • 感觉被家人抛弃了。
    小花儿飞阅读 185评论 0 0
  • 1 HTTP协议1.1请求报文1.2 响应报文1.3 http的请求方式有哪些1.4 HTTP扩展方法1.5 GE...
    二斤寂寞阅读 326评论 0 0