在Swift標准庫中,絕大多數的公開類型都是結構體,而枚舉和類只占很小一部分。
一、結構體
常見的Bool、Int、Double、String、Array、Dictionary等常見類型都是結構體。
自定義結構體:
struct Date {
var year: Int;
var month: Int;
var day: Int;
}
var date = Date(year: 2019, month: 06, day: 02)
所有結構體都有一個編譯器自動生成的初始化器(initializer、初始化方法、構造器、構造方法)。
Date(year: 2019, month: 06, day: 02)
傳入的是所有成員值,用來初始化所有成員(叫做存儲屬性)。
1.1. 結構體的初始化器
編譯器會根據情況,可能會為結構體生成多個初始化器,宗旨是:保證所有成員都有初始值。
從上面案例可以看出,編譯器幫助生成初始化器的條件就是:讓所有存儲屬性都有值。
> 思考:下面的代碼能否編譯通過?
可選項都有個默認值nil
,所以可以編譯通過。
1.2. 自定義初始化器
一旦在定義結構體時自定義了初始化器,編譯器就不會再幫它自動生成其他初始化器。
1.3. 探究結構體初始化器的本質
下面的兩段代碼是等效的:
代碼一:
代碼二:
經過對比發現,代碼一和代碼二的init
方法完全一樣。也就是說,存儲屬性的初始化是在初始化構造方法中完成的。
1.4. 結構體的內存結構
struct Point {
var x = 10
var y = 20
var b = true
}
var p = Point()
print(Mems.memStr(ofVal: &p))
print(MemoryLayout<point>.size)
print(MemoryLayout<point>.stride)
print(MemoryLayout<point>.alignment)
/*
輸出:
0x000000000000000a 0x0000000000000014 0x0000000000000001
17
24
8
*/
因為存儲屬性x
和y
各占8個字節(連續內存地址),Bool
在內存中占用1個字節,所以Point
一共占用17個字節,由於內存對齊是8,所以一共分配了24個字節。
二、類
類的定義和結構體類似,但編譯器並沒有為類自動生成可以傳入成員值的初始化器。
定義類:
如果存儲屬性沒有初始值,無參的初始化器也不會自動生成:
如果把上面的類換成結構體(struct
)類型就不會報錯:
2.1. 類的初始化器
如果類的所有成員都在定義的時候指定了初始值,編譯器會為類生成無參的初始化器。
成員的初始化是在這個初始化器中完成的。
下面的兩段代碼是等效的:
代碼一:
class Point {
var x: Int = 0
var y: Int = 0
}
var p1 = Point()
代碼二:
class Point {
var x: Int
var y: Int
init() {
self.x = 0
self.y = 0
}
}
var p1 = Point()
三、結構體與類的本質區別
結構體時值類型(枚舉也是值類型),類是引用類型(指針類型)。
3.1. 內存分析結構體與類
示例代碼:
class Size {
var width: Int = 1
var height: Int = 2
}
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var size = Size()
print("class-size對象的內存",Mems.memStr(ofRef: size))
print("class-size指針的內存地址",Mems.ptr(ofVal: &size))
print("class-size對象的內存地址",Mems.ptr(ofRef: size))
print("class-size.width的內存地址",Mems.ptr(ofVal: &size.width))
print("class-size.height的內存地址",Mems.ptr(ofVal: &size.height))
var point = Point()
print("struct-point對象的內存",Mems.memStr(ofVal: &point))
print("struct-point的內存地址",Mems.ptr(ofVal: &point))
print("struct-point.x的內存地址",Mems.ptr(ofVal: &point.x))
print("struct-point.y的內存地址",Mems.ptr(ofVal: &point.y))
}
test()
/*
輸出:
class-size對象的內存 0x00000001000092a8 0x0000000200000002 0x0000000000000001 0x0000000000000002
class-size指針的內存地址 0x00007ffeefbff4d0
class-size對象的內存地址 0x000000010061fe80
class-size.width的內存地址 0x000000010061fe90
class-size.height的內存地址 0x000000010061fe98
struct-point對象的內存 0x0000000000000003 0x0000000000000004
struct-point的內存地址 0x00007ffeefbff470
struct-point.x的內存地址 0x00007ffeefbff470
struct-point.y的內存地址 0x00007ffeefbff478
*/
示例代碼的在內存中:
經過分析可以看到,結構體的數據是直接存到棧空間的,類的實例是用指針指向堆空間的內存,指針在棧空間。上面示例代碼中類的實例占用32個字節,其中前面16個字節分別存儲指向類型信息和引用計數,后面16個字節才是真正用來存儲數據的。而結構體占用的內存大小等於存儲屬性所占內存大小之和。
> 注意:在C
語言中,結構體是不能定義方法的,但是在C++
和Swift
中,可以在結構體和類中定義方法。在64bit環境中,指針占用8個字節。
>> 擴展:值類型(結構體、枚舉)的內存根據所處的位置不同,內存的位置也不一樣。例如,定義一個全局的結構體,內存在數據段(全局區)中;如果在函數中定義,內存存放在棧空間;如果在類中定義一個結構體,內存跟隨對象在堆空間。
3.2. 匯編分析結構體與類
在Swift
中,創建類的實例對象,要向堆空間申請內存,大概流程如下:
Class.__allocating_init()
libswiftCore.dylib:_swift_allocObject_
libswiftCore.dylib:swift_slowAlloc
libsystem_malloc.dylib:malloc
在Mac,iOS中的malloc
函數分配的內存大小總是16的倍數(為了做內存優化)。
通過class_getInstanceSize
可以得知類的對象真正使用的內存大小。
import Foundation
class Point {
var x: Int = 3
var y: Int = 4
var b: Bool = true
}
var p = Point()
print(class_getInstanceSize(type(of: p)))
print(class_getInstanceSize(Point.self))
/*
輸出:
40
40
*/
內存占用大小 = 8(指向類型信息) + 8(引用計數) + 8(存儲屬性x) + 8(存儲屬性y) + 1(存儲屬性b) = 33;
內存分配大小 = 8(指向類型信息) + 8(引用計數) + 8(存儲屬性x) + 8(存儲屬性y) + Max(1(存儲屬性b), 8(內存對齊數)) = 40;
> 擴展:如果底層調用了alloc或malloc函數,說明該對象存在堆空間,否則就是在棧空間。
3.2.1. 匯編分析結構體
第一步:創建結構體,打斷點進入匯編:
第二步:在callq...init()
函數處進入函數實現體(lldb進入函數體指令:si
):
結論:rbp
就是局部變量,所以結構體創建的對象是在棧中存儲的。
> 擴展:一般情況下,rbp
就是局部變量,rip
是全局變量,ret
是函數返回。
3.2.2. 匯編分析類
第一步:創建結構體,打斷點進入匯編:
第二步:在callq...__allocating_init()...
函數處打斷點,進入函數體:
第三步:在callq...swift_allocObject
函數處打斷點,進入函數體:
第四步:一直進入到libswiftCore.dylib swift_allocObject:
中,在callq...swift_slowAlloc
處打斷點進入:
第五步:malloc
出現了,這時候繼續進入函數體:
第六步:最終,對象是在libsystem_malloc.dylib
庫中執行的malloc
:
經過上面分析,可以清晰的看到,對象是在堆空間存儲的。
擴展:在Mac、iOS中,創建對象都是調用的
libsystem_malloc.dylib
動態庫。