一.堆棧
棧是一塊空間較小但是運行速度很快的內存區域,棧上的內存分配遵循后進先出的原則,通過移動棧的尾指針實現push和pop操作。
堆是內存中的另外一塊,空間比棧大很多,但是運行速度比棧要慢。但是堆可以動態分配內存。堆的內存分配比較復雜,系統需要在堆上不斷尋找不再需要的內存然后進行回收。在ARC中上述過程是自動的。另外在多線程環境中,多個線程會共享堆內存。為了確保線程安全,堆會對資源進行加鎖操作。但是加鎖是很耗費性能的,你在堆上所獲得的數據安全性實際上是在犧牲性能的代價下得來的。
二.swift中的值類型和引用類型
1.值類型
在Swift中,值類型有兩種。一種是定長值類型,比如數值類型Int,Double,Float,還有一些只包含定長值類型的結構體(CGPoint)等等;另外一種叫做變長值類型,比如String,數組,字典等等。定長值類型都會保存在棧上,而變長值類型則會分配堆內存。
值類型的實例(結構體)只會在棧上保存它內部的存儲屬性,并且通過=賦值的實例彼此的存儲是獨立的。也就是我們所說的拷貝。如下:
struct Point{
var x,y:Double
}
let point1 = Point(x:3,y:5)
var point2 = point1
point1和point2會被分配到棧上,并且會分別為point1和point2分配內存空間。在語句point2 = point1的時候,point1進行了拷貝。因為定長值類型的空間是固定的,所以這種拷貝的開銷很小。
2.引用類型
引用類型并不會直接保存在棧上,還是以上述Point為例,如果把Point修改為類,并生成兩個引用point1和point2,這時系統會在棧上開辟兩個指針長度來保存point1和ponit2指針,棧上的指針負責去堆上找對應的對象。point1和point2所指向的實例的存儲屬性會保存在堆上。
在棧上生成point1指針后,指針內容是空的,接下來會去堆上分配內存,首先會對堆加鎖,找到尺寸合適的空間,然后分配目標內存并解除堆的鎖定,將堆內存片段的首地址保存在棧的指針中。相比在棧上保存point1和point2的指針,堆上需要的空間更大。除了x和y的空間,在頭部還有8個字節的空間,一個用來索引類的類型信息的指針地址,一個用來保存對象的引用計數。當使用=賦值時,棧上會生成point2指針,point1和point2指針指向同一個堆地址
引用類型的賦值不會發生拷貝。所以無論改變point1或者是point2的屬性,改變的都是同一塊堆地址上的屬性。這里要特別說明let和var。swift提供了let和var來限制對象的可變性和不可變性,但是對于某個實例,有意義的是其內部屬性。如果你用let聲明一個引用類型對象,你只能保證它的指針地址不能被改變,但是不能約束它的內部屬性。舉個例子:
//這里的Point是Class
//聲明point1和point2指向不同的內存地址
let point1 = Point(x:3,y:5)
let point2 = Point(x:5,y:1)
point1 = point2 //發生編譯錯誤,不能修改point1的指針
point1.x = 0 //因為x是用var定義的所以可以修改
//這里point1.x == 0
我們把多個引用指向同一塊內存稱為資源共享。不過在實際開發過程中,很多時候我們并不想讓point1和point2資源共享,這樣會造成很多難以判斷的錯誤。在Swift中,一種新的類型來專門解決共享的問題,就是我們所說的變長值類型。剛才講值類型的時候有提過定長值類型的內存分配,那么變長值類型是什么樣的?這就是我們今天的主題Copy-On-Write。
三.Copy-On-Write
因為棧上的空間是連續的,你總是通過移動棧尾指針去開辟和釋放棧內存,而變長值類型中有一些成員在初始化的時候并不能確定它所占用的內存。比如集合類型,你可以隨時往里面添加和刪除元素,這會導致內存的增加和減少。類似的還有字符串,在內存中儲存字符串實際上是存儲的每一個字符,所以對于變長值類型并不能把全部內容都保存在棧上。在Swift中用了一種很巧妙的技術來實現變長值類型,那就是Copy-On-Write。
Copy-On-Write故名思議就是寫時復制,當我們對變量進行寫操作的時候會觸發拷貝操作。但是我們也不能在每一次寫入的時候都拷貝,思考一下,如果該變量的引用計數只有1,那就沒有任何拷貝的必要。所以在拷貝前我們需要檢測變量的引用計數是否唯一。在swift中提供了isKnownUniquelyReferenced,它能檢查一個類的實例是不是唯一的引用。然而這個方法只能對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 聲明的方法可以修改結構體中變量
//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
//內部Box對象引用計數為1,不拷貝
bizer.pathForWriting.rawValue.lineWidth = 10
//Box對象引用計數+1
//bizer和bizer1會共享內部Box對象
//要注意這里的賦值把bizer進行了拷貝,但是其內部的引用屬性還是指向相同地址。
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.
}
}
相關的地方在代碼中都有給出注釋,所以在這里就不在贅述。