一.堆棧
棧是一塊空間較小但是運(yùn)行速度很快的內(nèi)存區(qū)域,棧上的內(nèi)存分配遵循后進(jìn)先出的原則,通過(guò)移動(dòng)棧的尾指針實(shí)現(xiàn)push和pop操作。
堆是內(nèi)存中的另外一塊,空間比棧大很多,但是運(yùn)行速度比棧要慢。但是堆可以動(dòng)態(tài)分配內(nèi)存。堆的內(nèi)存分配比較復(fù)雜,系統(tǒng)需要在堆上不斷尋找不再需要的內(nèi)存然后進(jìn)行回收。在ARC中上述過(guò)程是自動(dòng)的。另外在多線程環(huán)境中,多個(gè)線程會(huì)共享堆內(nèi)存。為了確保線程安全,堆會(huì)對(duì)資源進(jìn)行加鎖操作。但是加鎖是很耗費(fèi)性能的,你在堆上所獲得的數(shù)據(jù)安全性實(shí)際上是在犧牲性能的代價(jià)下得來(lái)的。
二.swift中的值類(lèi)型和引用類(lèi)型
1.值類(lèi)型
在Swift中,值類(lèi)型有兩種。一種是定長(zhǎng)值類(lèi)型,比如數(shù)值類(lèi)型Int,Double,Float,還有一些只包含定長(zhǎng)值類(lèi)型的結(jié)構(gòu)體(CGPoint)等等;另外一種叫做變長(zhǎng)值類(lèi)型,比如String,數(shù)組,字典等等。定長(zhǎng)值類(lèi)型都會(huì)保存在棧上,而變長(zhǎng)值類(lèi)型則會(huì)分配堆內(nèi)存。
值類(lèi)型的實(shí)例(結(jié)構(gòu)體)只會(huì)在棧上保存它內(nèi)部的存儲(chǔ)屬性,并且通過(guò)=賦值的實(shí)例彼此的存儲(chǔ)是獨(dú)立的。也就是我們所說(shuō)的拷貝。如下:
struct Point{
var x,y:Double
}
let point1 = Point(x:3,y:5)
var point2 = point1
point1和point2會(huì)被分配到棧上,并且會(huì)分別為point1和point2分配內(nèi)存空間。在語(yǔ)句point2 = point1的時(shí)候,point1進(jìn)行了拷貝。因?yàn)槎ㄩL(zhǎng)值類(lèi)型的空間是固定的,所以這種拷貝的開(kāi)銷(xiāo)很小。
2.引用類(lèi)型
引用類(lèi)型并不會(huì)直接保存在棧上,還是以上述Point為例,如果把Point修改為類(lèi),并生成兩個(gè)引用point1和point2,這時(shí)系統(tǒng)會(huì)在棧上開(kāi)辟兩個(gè)指針長(zhǎng)度來(lái)保存point1和ponit2指針,棧上的指針負(fù)責(zé)去堆上找對(duì)應(yīng)的對(duì)象。point1和point2所指向的實(shí)例的存儲(chǔ)屬性會(huì)保存在堆上。
在棧上生成point1指針后,指針內(nèi)容是空的,接下來(lái)會(huì)去堆上分配內(nèi)存,首先會(huì)對(duì)堆加鎖,找到尺寸合適的空間,然后分配目標(biāo)內(nèi)存并解除堆的鎖定,將堆內(nèi)存片段的首地址保存在棧的指針中。相比在棧上保存point1和point2的指針,堆上需要的空間更大。除了x和y的空間,在頭部還有8個(gè)字節(jié)的空間,一個(gè)用來(lái)索引類(lèi)的類(lèi)型信息的指針地址,一個(gè)用來(lái)保存對(duì)象的引用計(jì)數(shù)。當(dāng)使用=賦值時(shí),棧上會(huì)生成point2指針,point1和point2指針指向同一個(gè)堆地址
引用類(lèi)型的賦值不會(huì)發(fā)生拷貝。所以無(wú)論改變point1或者是point2的屬性,改變的都是同一塊堆地址上的屬性。這里要特別說(shuō)明let和var。swift提供了let和var來(lái)限制對(duì)象的可變性和不可變性,但是對(duì)于某個(gè)實(shí)例,有意義的是其內(nèi)部屬性。如果你用let聲明一個(gè)引用類(lèi)型對(duì)象,你只能保證它的指針地址不能被改變,但是不能約束它的內(nèi)部屬性。舉個(gè)例子:
//這里的Point是Class
//聲明point1和point2指向不同的內(nèi)存地址
let point1 = Point(x:3,y:5)
let point2 = Point(x:5,y:1)
point1 = point2 //發(fā)生編譯錯(cuò)誤,不能修改point1的指針
point1.x = 0 //因?yàn)閤是用var定義的所以可以修改
//這里point1.x == 0
我們把多個(gè)引用指向同一塊內(nèi)存稱(chēng)為資源共享。不過(guò)在實(shí)際開(kāi)發(fā)過(guò)程中,很多時(shí)候我們并不想讓point1和point2資源共享,這樣會(huì)造成很多難以判斷的錯(cuò)誤。在Swift中,一種新的類(lèi)型來(lái)專(zhuān)門(mén)解決共享的問(wèn)題,就是我們所說(shuō)的變長(zhǎng)值類(lèi)型。剛才講值類(lèi)型的時(shí)候有提過(guò)定長(zhǎng)值類(lèi)型的內(nèi)存分配,那么變長(zhǎng)值類(lèi)型是什么樣的?這就是我們今天的主題Copy-On-Write。
三.Copy-On-Write
因?yàn)闂I系目臻g是連續(xù)的,你總是通過(guò)移動(dòng)棧尾指針去開(kāi)辟和釋放棧內(nèi)存,而變長(zhǎng)值類(lèi)型中有一些成員在初始化的時(shí)候并不能確定它所占用的內(nèi)存。比如集合類(lèi)型,你可以隨時(shí)往里面添加和刪除元素,這會(huì)導(dǎo)致內(nèi)存的增加和減少。類(lèi)似的還有字符串,在內(nèi)存中儲(chǔ)存字符串實(shí)際上是存儲(chǔ)的每一個(gè)字符,所以對(duì)于變長(zhǎng)值類(lèi)型并不能把全部?jī)?nèi)容都保存在棧上。在Swift中用了一種很巧妙的技術(shù)來(lái)實(shí)現(xiàn)變長(zhǎng)值類(lèi)型,那就是Copy-On-Write。
Copy-On-Write故名思議就是寫(xiě)時(shí)復(fù)制,當(dāng)我們對(duì)變量進(jìn)行寫(xiě)操作的時(shí)候會(huì)觸發(fā)拷貝操作。但是我們也不能在每一次寫(xiě)入的時(shí)候都拷貝,思考一下,如果該變量的引用計(jì)數(shù)只有1,那就沒(méi)有任何拷貝的必要。所以在拷貝前我們需要檢測(cè)變量的引用計(jì)數(shù)是否唯一。在swift中提供了isKnownUniquelyReferenced,它能檢查一個(gè)類(lèi)的實(shí)例是不是唯一的引用。然而這個(gè)方法只能對(duì)Swift的類(lèi)使用,所以對(duì)于不是Swift的類(lèi)我們需要在外面包裝一下。下面我們看代碼:
import UIKit
//聲明swift包裝類(lèi),用于包裝OC對(duì)象UIBezierPath
class Box<T>{
var rawValue:T
init(rawValue:T) {
self.rawValue = rawValue
}
}
struct BezierPath{
private var _path = Box.init(rawValue: UIBezierPath())
var pathForReading:Box<UIBezierPath>{
return _path
}
var pathForWriting:Box<UIBezierPath>{
//mutating 聲明的方法可以修改結(jié)構(gòu)體中變量
//isKnownUniquelyReferenced 檢測(cè)引用類(lèi)型的引用是否唯一 但是只對(duì)swift類(lèi)有用 這里我們針對(duì)的對(duì)象是UIBezierPath 所以我們需要用Swift的類(lèi)包裝一下 在這里我們聲明了Box類(lèi)
mutating get{
if !isKnownUniquelyReferenced(&_path){
_path = Box.init(rawValue: _path.rawValue.copy() as! UIBezierPath)
print("拷貝")
return _path
}
print("未拷貝")
return _path
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var bizer = BezierPath()
bizer.pathForWriting.rawValue.lineWidth = 3
//內(nèi)部Box對(duì)象引用計(jì)數(shù)為1,不拷貝
bizer.pathForWriting.rawValue.lineWidth = 10
//Box對(duì)象引用計(jì)數(shù)+1
//bizer和bizer1會(huì)共享內(nèi)部Box對(duì)象
//要注意這里的賦值把bizer進(jìn)行了拷貝,但是其內(nèi)部的引用屬性還是指向相同地址。
var bizer1 = bizer
bizer1.pathForWriting.rawValue.lineWidth = 5
print(bizer.pathForReading.rawValue.lineWidth) //輸出10
// Do any additional setup after loading the view, typically from a nib.
}
}
相關(guān)的地方在代碼中都有給出注釋?zhuān)栽谶@里就不在贅述。