一.堆棧
棧是一塊空間較小但是運(yùn)行速度很快的內(nèi)存區(qū)域张惹,棧上的內(nèi)存分配遵循后進(jìn)先出的原則疯兼,通過移動棧的尾指針實(shí)現(xiàn)push和pop操作教馆。
堆是內(nèi)存中的另外一塊馋缅,空間比棧大很多扒腕,但是運(yùn)行速度比棧要慢。但是堆可以動態(tài)分配內(nèi)存萤悴。堆的內(nèi)存分配比較復(fù)雜瘾腰,系統(tǒng)需要在堆上不斷尋找不再需要的內(nèi)存然后進(jìn)行回收。在ARC中上述過程是自動的覆履。另外在多線程環(huán)境中蹋盆,多個(gè)線程會共享堆內(nèi)存。為了確保線程安全硝全,堆會對資源進(jìn)行加鎖操作栖雾。但是加鎖是很耗費(fèi)性能的,你在堆上所獲得的數(shù)據(jù)安全性實(shí)際上是在犧牲性能的代價(jià)下得來的伟众。
二.swift中的值類型和引用類型
1.值類型
在Swift中析藕,值類型有兩種。一種是定長值類型凳厢,比如數(shù)值類型Int,Double,Float账胧,還有一些只包含定長值類型的結(jié)構(gòu)體(CGPoint)等等;另外一種叫做變長值類型先紫,比如String找爱,數(shù)組,字典等等泡孩。定長值類型都會保存在棧上车摄,而變長值類型則會分配堆內(nèi)存。
值類型的實(shí)例(結(jié)構(gòu)體)只會在棧上保存它內(nèi)部的存儲屬性,并且通過=賦值的實(shí)例彼此的存儲是獨(dú)立的吮播。也就是我們所說的拷貝变屁。如下:
struct Point{
var x,y:Double
}
let point1 = Point(x:3,y:5)
var point2 = point1
point1和point2會被分配到棧上,并且會分別為point1和point2分配內(nèi)存空間意狠。在語句point2 = point1的時(shí)候粟关,point1進(jìn)行了拷貝。因?yàn)槎ㄩL值類型的空間是固定的环戈,所以這種拷貝的開銷很小闷板。
2.引用類型
引用類型并不會直接保存在棧上,還是以上述Point為例院塞,如果把Point修改為類遮晚,并生成兩個(gè)引用point1和point2,這時(shí)系統(tǒng)會在棧上開辟兩個(gè)指針長度來保存point1和ponit2指針拦止,棧上的指針負(fù)責(zé)去堆上找對應(yīng)的對象县遣。point1和point2所指向的實(shí)例的存儲屬性會保存在堆上。
在棧上生成point1指針后汹族,指針內(nèi)容是空的萧求,接下來會去堆上分配內(nèi)存,首先會對堆加鎖顶瞒,找到尺寸合適的空間夸政,然后分配目標(biāo)內(nèi)存并解除堆的鎖定,將堆內(nèi)存片段的首地址保存在棧的指針中榴徐。相比在棧上保存point1和point2的指針守问,堆上需要的空間更大。除了x和y的空間箕速,在頭部還有8個(gè)字節(jié)的空間酪碘,一個(gè)用來索引類的類型信息的指針地址,一個(gè)用來保存對象的引用計(jì)數(shù)盐茎。當(dāng)使用=賦值時(shí)兴垦,棧上會生成point2指針,point1和point2指針指向同一個(gè)堆地址
引用類型的賦值不會發(fā)生拷貝字柠。所以無論改變point1或者是point2的屬性探越,改變的都是同一塊堆地址上的屬性。這里要特別說明let和var窑业。swift提供了let和var來限制對象的可變性和不可變性钦幔,但是對于某個(gè)實(shí)例,有意義的是其內(nèi)部屬性常柄。如果你用let聲明一個(gè)引用類型對象鲤氢,你只能保證它的指針地址不能被改變搀擂,但是不能約束它的內(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)存稱為資源共享哨颂。不過在實(shí)際開發(fā)過程中,很多時(shí)候我們并不想讓point1和point2資源共享相种,這樣會造成很多難以判斷的錯(cuò)誤威恼。在Swift中,一種新的類型來專門解決共享的問題寝并,就是我們所說的變長值類型箫措。剛才講值類型的時(shí)候有提過定長值類型的內(nèi)存分配,那么變長值類型是什么樣的衬潦?這就是我們今天的主題Copy-On-Write斤蔓。
三.Copy-On-Write
因?yàn)闂I系目臻g是連續(xù)的,你總是通過移動棧尾指針去開辟和釋放棧內(nèi)存别渔,而變長值類型中有一些成員在初始化的時(shí)候并不能確定它所占用的內(nèi)存附迷。比如集合類型惧互,你可以隨時(shí)往里面添加和刪除元素哎媚,這會導(dǎo)致內(nèi)存的增加和減少。類似的還有字符串喊儡,在內(nèi)存中儲存字符串實(shí)際上是存儲的每一個(gè)字符拨与,所以對于變長值類型并不能把全部內(nèi)容都保存在棧上。在Swift中用了一種很巧妙的技術(shù)來實(shí)現(xiàn)變長值類型艾猜,那就是Copy-On-Write买喧。
Copy-On-Write故名思議就是寫時(shí)復(fù)制,當(dāng)我們對變量進(jìn)行寫操作的時(shí)候會觸發(fā)拷貝操作匆赃。但是我們也不能在每一次寫入的時(shí)候都拷貝淤毛,思考一下,如果該變量的引用計(jì)數(shù)只有1算柳,那就沒有任何拷貝的必要低淡。所以在拷貝前我們需要檢測變量的引用計(jì)數(shù)是否唯一。在swift中提供了isKnownUniquelyReferenced瞬项,它能檢查一個(gè)類的實(shí)例是不是唯一的引用蔗蹋。然而這個(gè)方法只能對Swift的類使用,所以對于不是Swift的類我們需要在外面包裝一下囱淋。下面我們看代碼:
import UIKit
//聲明swift包裝類猪杭,用于包裝OC對象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 檢測引用類型的引用是否唯一 但是只對swift類有用 這里我們針對的對象是UIBezierPath 所以我們需要用Swift的類包裝一下 在這里我們聲明了Box類
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對象引用計(jì)數(shù)為1,不拷貝
bizer.pathForWriting.rawValue.lineWidth = 10
//Box對象引用計(jì)數(shù)+1
//bizer和bizer1會共享內(nèi)部Box對象
//要注意這里的賦值把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)的地方在代碼中都有給出注釋戒傻,所以在這里就不在贅述。