結構體
基本概念
在Swift標準庫中,絕大多數的公開型別都是結構體,而列舉和類只占很小一部分
比如Bool、Int、String、Double、Array、Dictionary等常見型別都是結構體
struct Date {
var year: Int
var month: Int
var day: Int
}
所有的結構體都有一個編譯器自動生成的范訓器(initializer,初始化方法、構造器、構造方法)
可以傳入所有成員值,用以初始化所有成員(存盤屬性,Stored Property)
var date = Date(year: 2019, month: 6, day: 23)
結構體的初始化器
編譯器會根據情況,可能會為結構體生成多個初始化器,宗旨是:保證所有成員都有初始值
如果結構體的成員定義的時候都有默認值了,那么生成的初始化器不會報錯

如果是下面這幾種情況就會報錯



如果是可選型別的初始化器也不會報錯,因為可選型別默認的值就是nil

自定義初始化器
我們也可以自定義初始化器
struct Point {
var x: Int = 0
var y: Int = 0
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 10, y: 10)
下面這幾個初始化器報錯的原因是因為我們已經自定義初始化器了,編譯器就不會再幫我們生成其他的初始化器了

初始化器的本質
下面這兩種寫法是完全等效的
struct Point {
var x: Int = 0
var y: Int = 0
}
等效于
struct Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
var p4 = Point()
我們通過反匯編分別對比一下兩種寫法的實作,發現也是一樣的


1.然后我們分別列印結構體的占用記憶體大小和記憶體布局
struct Point {
var x: Int = 10
var y: Int = 20
}
var p4 = Point()
debugPrint(MemoryLayout<Point>.stride) // 16
debugPrint(MemoryLayout<Point>.size) // 16
debugPrint(MemoryLayout<Point>.alignment) // 8
debugPrint(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014
可以看到系統一共分配了16個位元組的記憶體空間
前8個位元組存盤的是10,后8個位元組存盤的是20
2.我們再看下面這個結構體
struct Point {
var x: Int = 10
var y: Int = 20
var origin: Bool = false
}
var p4 = Point()
debugPrint(MemoryLayout<Point>.stride) // 24
debugPrint(MemoryLayout<Point>.size) // 17
debugPrint(MemoryLayout<Point>.alignment) // 8
debugPrint(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014 0x0000000000000000
可以看到結構體實際只用了17個位元組,而系統因為記憶體對齊分配了24個位元組
前8個位元組存盤的是10,中間8個位元組存盤的是20,最后1個位元組存盤的是false,也就是0
類
類的定義和結構體類似,但編譯器并沒有為類自動生成可以傳入成員值的初始化器

如果成員沒有初始值,所有的初始化器都會報錯

類的初始化器
如果類的所有成員都在定義的時候指定了初始值,編譯器會為類生成無參的初始化器
成員的初始化是在這個初始化器中完成的
class Point {
var x: Int = 0
var y: Int = 0
}
let p1 = Point()
下面這兩種寫法是完全等效的
class Point {
var x: Int = 0
var y: Int = 0
}
等效于
class Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
let p1 = Point()
結構體與類的本質區別
結構體是值型別(列舉也是值型別),類是參考型別(指標型別)
下面我們分析函式內的區域變數分別都在記憶體的什么位置
class Size {
var width = 1
var height = 2
}
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var size = Size()
var point = Point()
}
變數size和point都是在堆疊空間
不同的是size的值是一個結構體型別,結構體因為也是值型別,所以也會在堆疊空間中,它里面的兩個成員x、y按順序的排布
而point的值是類,類是參考型別,所以point是個指標,指向的類是在堆中的,point里存盤的是類的地址

下面我們分析類的詳細記憶體布局
1.我們先來看一下類的占用記憶體大小是多少
class Size {
var width = 1
var height = 2
}
debugPrint(MemoryLayout<Size>.stride) // 8
通過列印我們可以發現MemoryLayout獲取的8個位元組實際上是指標變數占用多少存盤空間,并不是物件在堆中的占用大小
2.然后我們再看類的記憶體布局是怎樣的
var size = Size()
debugPrint(Mems.ptr(ofVal: &size)) // 0x000000010000c388
debugPrint(Mems.memStr(ofVal: &size)) // 0x000000010072dba0
通過列印我們可以看到變數里面存盤的值也是一個地址
3.我們再列印該變數所指向的物件的記憶體布局是什么
debugPrint(Mems.size(ofRef: size)) // 32
debugPrint(Mems.ptr(ofRef: size)) // 0x000000010072dba0
debugPrint(Mems.memStr(ofRef: size)) // 0x000000010000c278 0x0000000200000003 0x0000000000000001 0x0000000000000002
通過列印可以看到在堆中存盤的物件的地址和上面的指標變數里存盤的值是一樣的
記憶體布局里一共占用32個位元組,前16個位元組分別用來存盤一些類資訊和參考計數,后面16個位元組存盤著類的成員變數的值
下面我們再從反匯編的角度來分析
我們要想確定類是否在堆空間中分配空間,通過反匯編來查看是否有呼叫malloc函式


然后就一直跟進直到這里最好呼叫了swift_slowAlloc

發現函式內部呼叫了系統的malloc在堆空間分配記憶體

注意:結構體和列舉存盤在哪里取決于它們是在哪里分配的,如果是在函式中分配的那就是在堆疊里,如果是在全域中分配的那就是在資料段
而類無論是在哪里分配的,物件都是在堆空間中,指向物件記憶體的指標的存盤位置是不確定的,可能在堆疊中也可能在資料段
我們再看下面的型別占用記憶體大小是多少
class Size {
var width: Int = 0
var height: Int = 0
var test = true
}
let s = Size()
print(Mems.size(ofRef: s)) // 48
在Mac、iOS中的malloc函式分配的記憶體大小總是16的倍數
類最前面會有16個位元組用來存盤類的資訊和參考計數,所以實際占用記憶體是33個位元組,但由于malloc函式分配的記憶體都是116的倍數1,所以分配48個位元組
我們還可以通過class_getInstanceSize函式來獲取類物件的記憶體大小
// 獲取的是經過記憶體對齊后的記憶體大小,不是malloc函式分配的記憶體大小
print(class_getInstanceSize(type(of: s))) // 40
print(class_getInstanceSize(Size.self)) // 40
值型別和參考型別
值型別
值型別賦值給var、let或者給函式傳參,是直接將所有內容拷貝一份
類似于對檔案進行copy、paste操作,產生了全新的檔案副本,屬于深拷貝(deep copy)
值型別進行拷貝的記憶體布局如下所示
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var p1 = Point(x: 10, y: 20)
var p2 = p1
p2.x = 4
p2.y = 5
print(p1.x, p1.y)
}
test()

我們通過反匯編來進行分析




通過上述分析可以發現,值型別的賦值內部會先將p1的成員值保存起來,再給p2進行賦值,所以不會影響到p1
值型別的賦值操作
在Swift標準庫中,為了提升性能,Array、String、Dictionary、Set采用了Copy On Write的技術
如果只是將賦值操作,那么只會進行淺拷貝,兩個變數使用的還是同一塊存盤空間
只有當進行了”寫“的操作時,才會進行深拷貝操作
對于標準庫值型別的賦值操作,Swift能確保最佳性能,所以沒必要為了保證最佳性能來避免賦值
var s1 = "Jack"
var s2 = s1
s2.append("_Rose")
print(s1) // Jack
print(s2) // Jack_Rose
var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]
var d1 = ["max" : 10, "min" : 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1) // ["other" : 7, "max" : 10, "min" : 2]
print(d2) // ["max" : 12, "min" : 2]
注意:不需要修改的盡量改成let
我們再看下面這段代碼
對于p1來說,再次賦值也只是覆寫了成員x、y的值而已,都是同一個結構體變數
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 20)
p1 = Point(x: 11, y: 22)
用let定義的賦值操作
如果用let定義的常量賦值結構體型別會報錯,并且修改結構體里的成員值也會報錯
用let定義就意味著常量里存盤的值不可更改,而結構體是由x和y這16個位元組組成的,所以更改x和y就意味著結構體的值要被覆寫,所以報錯

參考型別
參考賦值給var、let或者給函式傳參,是將記憶體地址拷貝一份
類似于制作一個檔案的替身(快捷方式、鏈接),指向的是同一個檔案,屬于淺拷貝(shallow copy)
class Size {
var width = 0
var height = 0
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
func test() {
var s1 = Size(width: 10, height: 20)
var s2 = s1
s2.width = 11
s2.height = 22
print(s1.height, s1.width)
}
test()
由于s1和s2都指向的同一塊存盤空間,所以s2修改了成員變數,s1再呼叫成員變數也已經是改變后的了

我們通過反匯編來進行分析



堆空間分配完記憶體之后,我們拿到rax的值查看記憶體布局,發現rax里和物件的結構一樣,證明rax里存盤的就是物件的地址


將新的值11和22分別覆寫掉堆空間物件的成員值




通過上面的分析可以發現,修改的成員值都是改的同一個地址的物件,所以修改了p2的成員值,會影響到p1
參考型別的賦值操作
將參考型別物件賦值給同一個變數,變數會指向另一塊存盤空間
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
var s1 = Size(width: 10, height: 20)
s1 = Size(width: 11, height: 22)
用let定義的賦值操作
如果用let定義的常量賦值參考型別會報錯,因為會改變指標常量里存盤的8個位元組的地址值
但修改類里的屬性值不會報錯,因為修改屬性值并不是修改的指標常量的記憶體,只是通過指標常量找到類所存盤的堆空間的記憶體地址去修改類的屬性

嵌套型別
struct Poker {
enum Suit: Character {
case spades = "??",
hearts = "??",
diamonds = "??",
clubs = "??"
}
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
}
}
print(Poker.Suit.hearts.rawValue)
var suit = Poker.Suit.spades
suit = .diamonds
var rank = Poker.Rank.five
rank = .king
列舉、結構體、類都可以定義方法
一般把定義在列舉、結構體、類內部的函式,叫做方法
struct Point {
var x: Int = 10
var y: Int = 10
func show() {
print("show")
}
}
let p = Point()
p.show()
class Size {
var width: Int = 10
var height: Int = 10
func show() {
print("show")
}
}
let s = Size()
s.show()
enum PokerFace: Character {
case spades = "??",
hearts = "??",
diamonds = "??",
clubs = "??"
func show() {
print("show")
}
}
let pf = PokerFace.hearts
pf.show()
方法不管放在哪里,其記憶體都是放在代碼段中
列舉、結構體、類里的方法其實會有隱式引數
class Size {
var width: Int = 10
var height: Int = 10
// 默認會有隱式引數,該引數型別為當前列舉、結構體、類
func show(self: Size) {
print(self.width, self.height)
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/271195.html
標籤:iOS
上一篇:Swift 進階(三)列舉
下一篇:Swift 進階(五)閉包
