我們使用寫時復(fù)制 copy on write 的思想,對 NSMutableData 進行封裝,以此來理解我們的標準庫的實現(xiàn)方式。
標準庫中提供的所有的基本集合類型都是值類型,通過寫時復(fù)制的思想保證了他的高效性。集合類型是我們比較常用到的數(shù)據(jù)類型,所以了解他的性能特性很重要,我們來一起看一下寫時復(fù)制是如何工作的,并且嘗試自己手動實現(xiàn)一個。
引用類型
舉個例子,我們比較一下Swift的Data(結(jié)構(gòu)體)和Foundation庫中的NSMutableData(類)。首先我們使用一些字節(jié)數(shù)據(jù)來初始化 NSMutableData 實例。
var sampleBytes: [UInt8] = [0x0b,0xad,0xf0,0x0d]
let nsData = NSMutableData(bytes: sampleBytes, length: sampleBytes.count)
我們使用了 let 來聲明 nsData,但是像 NSMutableData 這樣的引用類型不收let/var 的控制。對于引用類型來說,用 let 聲明代表 nsData 這個指針不能在指向別的內(nèi)存,但是他指向的這個內(nèi)存中的數(shù)據(jù)是可以變化的。也就是說我們依然可以往 nsData 中 append 數(shù)據(jù)。
nsData.append(sampleBytes, length: sampleBytes.count)
當我們再聲明一個對象,改變其中一個對象,另一個對象也會發(fā)生變化。
let nsOtherData = nsData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 也會變
如果我們想產(chǎn)生一個獨立的副本,我們需要使用 mutableCopy(返回一個 Any 類型),我們需要把返回值強轉(zhuǎn)成我們需要的 NSMutableData 類型。
let nsOtherData = nsData.mutableCopy() as! NSMutableData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 不變
值類型
首先我們也是通過 sampleBytes 來初始化一個 Data。
let data = Data(bytes: sampleBytes, count: sampleBytes.count)
如果我們使用 let 關(guān)鍵字,那編譯器就不會允許我們調(diào)用類型 append 這樣的方法。所以如果要改變 data 的值,要使用 var 。
var data = Data(bytes: sampleBytes, count: sampleBytes.count)
data.append(contentsOf: sampleBytes)
Data 和 NSData 最主要的不同之處是:把值賦給另一個變量時或者作為參數(shù)傳到方法中,Data 總是會生成一個新的副本,但是 NSData 只會生成一個新的引用,但是兩個引用指向同一個內(nèi)存區(qū)域。
當我們創(chuàng)建 Data 的一個副本的時候,他的所有的字段都會被復(fù)制,但是又不是立刻復(fù)制,因為 Data 內(nèi)存有對實際內(nèi)存空間的引用,所以當結(jié)構(gòu)體被復(fù)制時,也只是會生成一個新的引用,只有我們對這個新的引用修改數(shù)據(jù)是,實際的數(shù)據(jù)才會被復(fù)制。
實現(xiàn)寫時復(fù)制
我們自己實現(xiàn)一個 Data 類型來幫我們理解寫時復(fù)制是如何工作的,我們內(nèi)部使用 NSMutableData 來實際的存儲數(shù)據(jù)(只是為了更快的完成,實際的Data 內(nèi)部肯定是用到更底層的數(shù)據(jù)結(jié)構(gòu)來存儲數(shù)據(jù))。改變數(shù)據(jù)的方法我們只實現(xiàn)一個 append 方法。
struct MyData {
var data = NSMutableData()
func append(_ bytes: [UInt8]) {
data.append(bytes, length: bytes.count)
}
}
我們可以創(chuàng)建一個 MyData
let data = MyData()
為了能更好的打印出 data 中存儲的數(shù)據(jù),我們可以讓 MyData 實現(xiàn) CustomDebugStringConvertible 協(xié)議。
extension MyData: CustomDebugStringConvertible {
var debugDescription: String {
return String(describing: data)
}
}
現(xiàn)在我們可以調(diào)用 append 方法了。
data.append(sampleBytes)
但這是有問題的,首先我們的MyData是結(jié)構(gòu)體,而且創(chuàng)建 data 使用的是let,我們不應(yīng)該可以修改他的值。
而且看下面的代碼,他的復(fù)制行為也是有問題的,在我們聲明了一個新的引用是,并沒有獲得一個完全獨立的副本。
var copy = data
copy.append(sampleBytes)
print(data)
print(copy)
// copy 調(diào)用 append, data 也會改變
所以說我們雖然創(chuàng)建了一個結(jié)構(gòu)體,但是他并沒有表現(xiàn)出值語義來。
目前,我們在把 data 賦給一個新的變量時,雖然他是所有字段都復(fù)制,但是我們MyData內(nèi)部的 data 是一個 NSMutableData 引用類型,所以說 data 和 copy 這兩個變量的值現(xiàn)在都包含對同一個 NSMutableData 實例的引用。
為了解決這個問題,我們要先處理寫時復(fù)制的’寫時‘問題。當我們在調(diào)用 append 方法添加數(shù)據(jù)時,我們要把內(nèi)部進行實際存儲功能的data進行深拷貝,此時 我們的 append 方法就必須加上 mutating 關(guān)鍵字,要不然編譯器不允許修改結(jié)構(gòu)體的變量。
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)
}
}
現(xiàn)在我們要重新生成一個 var 類型的 data 來調(diào)用 append 方法,因為編譯器不允許let 類型的調(diào)用帶 mutating 關(guān)鍵字的方法。
var data = MyData()
var copy = data
copy.append(sampleBytes)
在我們繼續(xù)之前,進行一個小的重構(gòu),并將生成 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)
}
}
讓寫時復(fù)制更高效
目前我們的寫時復(fù)制是非常簡單的,就是每次當我們調(diào)用 append 的時候,都會拷貝,不管我們是不是這個實例的唯一持有者。
for _ in 0..<10 {
data.append(sampleBytes)
}
// making a copy 會打印10次
其實真正需要執(zhí)行復(fù)制操作的是當我們把data賦值給另一個變量后,這時調(diào)用append 方法,因為此時有兩個引用,所以需要進行深拷貝。當拷貝結(jié)束后,這兩個都是引用指向的都是完全獨立的備份了,所以再一次調(diào)用時就不需要拷貝了。
所以說我們的MyData結(jié)構(gòu)沒有問題,但是多次拷貝會降低性能。我們可以使用 isKnownUniquelyReferenced 這個方法來幫助我們實現(xiàn)想要的效果。
var dataForWriting: NSMutableData {
mutating get {
if isKnownUniquelyReferenced(&data) {
return data
}
print("make a copy")
data = data.mutableCopy() as! NSMutableData
return data
}
}
雖然我們現(xiàn)在加上了 isKnownUniquelyReferenced 檢查,但是運行一下測試代碼還是會copy多次,那是因為 isKnownUniquelyReferenced 方法只是對Swift類型有效果,如果是傳入的OC類型的對象,總是會返回false,所以我們應(yīng)該使用一個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)
}
}
現(xiàn)在我們的代碼只對 NSMutableData 實例copy一次。
var data = MyData()
var copy = data
for _ in 0..<10 {
data.append(sampleBytes)
}
// Prints:
// making a copy 一次
標準庫中數(shù)組和字典的實現(xiàn)方式其實也是類似的,只是他們用了更低級的數(shù)據(jù)結(jié)構(gòu)來存儲,我們這樣手動實現(xiàn)一次寫時復(fù)制,有助于我們更好理解他們內(nèi)部的性能。
寫時復(fù)制注意點
寫時復(fù)制很高效,但是他不是適應(yīng)于所有的場景,比如說我們上面的for循環(huán)是可以的,但是如果我們使用reduce來實現(xiàn)上面的循環(huán),他就不起作用了。
(0..<10).reduce(data) { result, _ in
var copy = result
copy.append(sampleBytes)
return copy
}
這個實現(xiàn)方式會生成 10 個副本,因為當我們調(diào)用 append 時,總是有兩個變量——copy 和 result——引用指向同一個實例。
所以我們應(yīng)該注意我們代碼中那些產(chǎn)品大量不必要副本的地方,不過我們一般都不會這么寫,所以說問題不大。