標籤:編譯器 elf index 長度 語言 操作 code swift 帶來
1、記憶體配置1.1 實值型別的記憶體配置
在 Swift 中定長的實值型別都是儲存在棧上的,操作時不會涉及堆上的記憶體。變長的實值型別(字串、集合類型是可變長度的實值型別)會分配堆記憶體。
- 這相當於一個 “福利”,意味著你可以使用實值型別更快速的完成一個方法的執行。
- 實值型別的執行個體只會儲存其內部的儲存屬性,並且通過 “=” 賦值的執行個體彼此的儲存是獨立的。
- 實值型別的賦值是拷貝的,對於定長的實值型別來說,由於所需的記憶體空間是固定的,所以這種拷貝的開銷是在常數時間內完成的。
struct Point { var x: Double var y: Double}
let point1 = Point(x: 3, y: 5)var point2 = point1print(point1) // Point(x: 3.0, y: 5.0)print(point2) // Point(x: 3.0, y: 5.0)
上面的樣本在棧上的實際分配如。
棧point1 x: 3.0 y: 5.0point2 x: 3.0 y: 5.0
如果嘗試修改 point2 的屬性,只會修改 point2 在棧上的地址中儲存的 x 值,不會影響 point1 的值。
point2.x = 5print(point1) // Point(x: 3.0, y: 5.0)print(point2) // Point(x: 5.0, y: 5.0)
棧point1 x: 3.0 y: 5.0point2 x: 5.0 y: 5.0
1.2 參考型別的記憶體配置
參考型別的儲存屬性不會直接儲存在棧上,系統會在棧上開闢空間用來儲存執行個體的指標,棧上的指標負責去堆上找到相應的對象。
- 參考型別的賦值不會發生 “拷貝”,當你嘗試修改樣本的值的時候,執行個體的指標會 “指引” 你來到堆上,然後修改堆上的內容。
下面把 Point 的定義修改成類。
class Point { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y }}
let point1 = Point(x: 3, y: 5)let point2 = point1print(point1.x, point1.y) // 3.0 5.0print(point2.x, point2.y) // 3.0 5.0
因為 Point 是類,所以 Point 的儲存屬性不能直接儲存在棧上,系統會在棧上開闢兩個指標的長度用來儲存 point1 和 point2 的指標,棧上的指標負責去堆上找到對應的對象,point1 和 point2 兩個執行個體的儲存屬性會儲存在堆上。
當使用 “=” 進行賦值時,棧上會產生一個 point2 的指標,point2 指標與 point1 指標指向堆的同一地址。
棧 堆point1 [ ] --| |--> 類型資訊point2 [ ] --| 引用計數 x: 3 y: 5
在棧上產生 point1 和 point2 的指標後,指標的內容是空的,接下來會去堆上分配記憶體,首先會對堆加鎖,找到尺寸合適的記憶體空間,然後分配目標記憶體並解除堆的鎖定,將堆中記憶體片段的首地址儲存在棧上的指標中。
相比在棧上儲存 point1 和 point2,堆上需要的記憶體空間要更大,除了儲存 x 和 y 的空間,在頭部還需要兩個 8 位元組的空間,一個用來索引類的類型資訊的指標地址,一個用來儲存對象的 “引用計數”。
當嘗試修改 point2 的值的時候,point2 的指標會 “指引” 你來到堆上,然後修改堆上的內容,這個時候 point1 也被修改了。
point2.x = 5print(point1.x, point1.y) // 5.0 5.0print(point2.x, point2.y) // 5.0 5.0
我們稱 point1 和 point2 之間的這種關係為 “共用”。“共用” 是參考型別的特性,在很多時候會給人帶來困擾,“共用” 形態出現的根本原因是我們無法保證一個參考型別的對象的不可變性。
2、可變性和不可變性
2.1 參考型別的可變性和不可變性
對於參考型別的對象,當你需要一個不可變的對象的時候,你無法通過關鍵字來控制其屬性的不可變性。
當你建立一個 Point 類的執行個體,你希望它是不可變的,所以使用 let 關鍵字聲明,但是 let 只能約束棧上的內容,也就是說,即便你對一個類型執行個體使用了 let 關鍵字,也只能保證它的指標地址不發生變化,但是不能約束它的屬性不發生變化。。
class Point { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y }}
let point1 = Point(x: 3, y: 5)let point2 = Point(x: 0, y: 0)print(point1.x, point1.y) // 3.0 5.0print(point2.x, point2.y) // 0.0 0.0point1 = point2 // 發生編譯錯誤,不能修改 point1 的指標point1.x = 0 // 因為 x 屬性是使用 var 定義的,所以可以被修改print(point1.x, point1.y) // 0.0 5.0print(point2.x, point2.y) // 0.0 0.0
如果把所有的屬性都設定成不可變的,這的確可以保證參考型別的不可變性,而且有不少語言就是這麼設計的。
class Point { let x: Double let y: Double init(x: Double, y: Double) { self.x = x self.y = y }}
let point1 = Point(x: 3, y: 5)print(point1.x, point1.y) // 3.0 5.0point1.x = 0 // 發生編譯錯誤,x 屬性是不可變的
新的問題是如果你要修改 Point 的屬性,你只能重建立一個對象並賦值,這意味著一次沒有必要的加鎖、定址與記憶體回收的過程,大大損耗了系統的效能。
let point1 = Point(x: 3, y: 5)point1 = Point(x: 0, y: 5)
2.2 實值型別的可變性和不可變性
因為實值型別的屬性儲存在棧上,所以可以被 let 關鍵字所約束。
你可以把一個實值型別的屬性都聲明稱 var,保證其靈活性,在需要該類型的執行個體是一個不可變對象時,使用 let 聲明對象,即便對象的屬性是可變的,但是對象整體是不可變的,所以不能修改執行個體的屬性。
struct Point { var x: Double var y: Double}
let point1 = Point(x: 3, y: 5)print(point1.x, point1.y) // 3.0 5.0point1.x = 0 // 編輯報錯,因為 point1 是不可變的
因為賦值時是 “拷貝” 的,所以舊對象的可變性限制不會影響新對象。
let point1 = Point(x: 3, y: 5)var point2 = point1 // 賦值時發生拷貝print(point1.x, point1.y) // 3.0 5.0print(point2.x, point2.y) // 3.0 5.0point2.x = 0 // 編譯通過,因為 point2 是可變的print(point1.x, point1.y) // 0.0 5.0print(point2.x, point2.y) // 0.0 5.0
3、參考型別的共用
“共用” 是參考型別的特性,在很多時候會給人帶來困擾,“共用” 形態出現的根本原因是我們無法保證一個參考型別的對象的不可變性。
下面展示應用類型中的共用。
// 標籤class Tag { var price: Double init(price: Double) { self.price = price }}// 商品class Merchandise { var tag: Tag var description: String init(tag: Tag, description: String) { self.tag = tag self.description = description }}
let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag, description: "tomato")print("tomato: \(tomato.tag.price)") // tomato: 8.0// 修改標籤tag.price = 3.0// 新商品let potato = Merchandise(tag: tag, description: "potato")print("tomato: \(tomato.tag.price)") // tomato: 3.0print("potato: \(potato.tag.price)") // potato: 3.0
這個例子中所描述的情景就是 “共用”, 你修改了你需要的部分(馬鈴薯的價格),但是引起了意料之外的其它改變(番茄的價格),這是由於番茄和馬鈴薯共用了一個標籤執行個體。
語意上的共用在真實的記憶體環境中是由記憶體位址引起的。上例中的對象都是參考型別,由於我們只建立了三個對象,所以系統會在堆上分配三塊記憶體位址,分別儲存 tomato、potato 和 tag。
棧 堆tamoto Tag --| description | tag |--> price: 3.0 |patoto Tag --| description
在 OC 時代,並沒有如此豐富的實值型別可供使用,有很多類型都是參考型別的,因此使用參考型別時需要一個不會產生 “共用” 的安全性原則,拷貝就是其中一種。
首先建立一個標籤對象,在標籤上打上你需要的價格,然後在標籤上調用 copy() 方法,將返回的拷貝對象傳給商品。
let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag.copy(), description: "tomato")print("tomato: \(tomato.tag.price)") // tomato: 8.0
當你對 tag 執行 copy 後再傳給 Merchandise 構造器,記憶體配置情況如。
棧 堆tamoto Tag -----> Copied tag description price: 8.0 tag price: 8.0
如果有新的商品上架,可以繼續使用 “拷貝” 來打標籤。
let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag.copy(), description: "tomato")print("tomato: \(tomato.tag.price)") // tomato: 8.0// 修改標籤tag.price = 3.0// 新商品let potato = Merchandise(tag: tag.copy(), description: "potato")print("tomato: \(tomato.tag.price)") // tomato: 8.0print("potato: \(potato.tag.price)") // potato: 3.0
現在記憶體中的分配。
棧 堆tamoto Tag -----> Copied tag description price: 8.0 tag price: 3.0patoto Tag -----> Copied tag description price: 3.0
這種拷貝叫做 “保護性拷貝”,在保護性拷貝的模式下,不會產生 “共用”。
4、變長實值型別的拷貝
變長實值型別不能像定長實值型別那樣把全部的內容都儲存在棧上,這是因為棧上的記憶體空間是連續的,你總是通過移動尾指標去開闢和釋放棧的記憶體。在 Swift 中集合類型和字串類型是實值型別的,在棧上保留了變長實值型別的身份資訊,而變長實值型別的內部元素全部保留在堆上。
定長實值型別不會發生 “共用” 這很好理解,因為每次賦值都會開闢新的棧記憶體,但是對於變長的實值型別來說是如何處理哪些尾儲存內部元素而佔用的堆記憶體呢?蘋果在 WWWDC2015 的 414 號視頻中揭示了定長實值型別的拷貝奧秘:相比定長實值型別的 “拷貝” 和參考型別的 “保護性拷貝”,變長實值型別的拷貝規則要複雜一些,使用了名為 Copy-on-Write 的技術,從字面上理解就是只有在寫入的時候才拷貝。
在 Swift 3.0 中出現了很多 Swift 原生的變長實值型別,這些變長實值型別在拷貝時使用了 Copy-on-Write 技術以提升效能,比如 Date、Data、Measurement、URL、URLSession、URLComponents、IndexPath。
5、利用參考型別的共用
“共用” 並不總是有害的,“共用” 的好處之一是堆上的記憶體空間得到了複用,尤其是對於記憶體佔用空間較大的對象(比片),效果明顯。所以如果堆上的對象在 “共用” 狀態下不會被修改,那麼我們應該對該對象進行複用從而避免在堆上建立重複的對象,此時你需要做的是建立一個對象,然後向對象的引用者傳遞對象的指標,簡單來說,就是利用 “共用” 來實現一個 “緩衝” 的策略。
假如你的應用中會用到許多重複的內容,比如用到很多相似的圖片,如果你在每個需要的地方都調用 UIImage(named:) 方法,那麼會建立很多重複的內容,所以我們需要把所有用到的圖片集中建立,然後從中挑選需要的圖片。很顯然,在這個情境中字典最適合作為緩衝圖片的容器,把字典的索引值作為圖片索引資訊。這是參考型別的經典用例之一,字典的索引值就是每個圖片的 “身份資訊”,可以看到在這個樣本中 “身份資訊” 是多麼的重要。
enum Color: String { case red case blue case green}enum Shape: String { case circle case square case triangle}
let imageArray = ["redsquare": UIImage(named: "redsquare"), ...]func searchImage(color: Color, shape: Shape) -> UIImage { let key = color.rawValue + shape.rawValue return imageArray[key]!!}
一個變長的實值型別實際會把記憶體儲存在堆上,因此建立一個變長實值型別時不可避免的會對堆加鎖並分配記憶體,我們使用緩衝的目的之一就是避免過多的堆記憶體操作,在上例中我們習慣性的把 String 作為字典的索引值,但是 String 是變長的實值型別,在 searchImage 中產生 key 的時候會觸發堆上的記憶體配置。
如果想繼續提升 searchImage 的效能,可以使用定長實值型別作為索引值,這樣在合成索引值時將不會訪問堆上的記憶體。要注意的一點是你所使用的定長實值型別必須滿足 Hashable 協議才能作為字典的索引值。
enum Color: Equatable { case red case blue case green}enum Shape: Equatable { case circle case square case triangle}struct PrivateKey: Hashable { var color: Color = .red var shape: Shape = .circle internal var hsahValue: Int { return color.hashValue + shape.hashValue }}
let imageArray = [PrivateKey(color: .red, shape: .square): UIImage(named: "redsquare"), PrivateKey(color: .blue, shape: .circle): UIImage(named: "bluecircle")]func searchImage(privateKey: PrivateKey) -> UIImage { return imageArray[privateKey]!!}
Swift 實值型別和參考型別的記憶體管理