一励稳、存儲屬性
存儲屬性是一個作為特定類和結(jié)構(gòu)體實例一部分的常量或變量。存儲屬性要么是變量存儲屬性
(由 var
關(guān)鍵字引入)要么是常量存儲屬性
(由 let
關(guān)鍵字引入)犬金。存儲屬性這里沒有什么特別要強調(diào)的念恍,因為隨處可見。
class ZGTeacher {
let age: Int = 32
var name: String = "Zhang"
}
比如這里的 age
和 name
就是我們所說的存儲屬性晚顷,這里我們需要加以區(qū)分的是 let
和 var
兩者的區(qū)別:從定義上: let
用來聲明常量
峰伙,常量的值一旦設(shè)置好便不能再被更改;var
用來聲明變量
该默,變量的值可以在將來設(shè)置為不同的值瞳氓。
1.1 代碼案例
這里我們來看幾個案例:
class ZGTeacher {
let age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
struct ZGStudent {
let age: Int
var name: String
}
let t = ZGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = ZGTeacher(age: 30, name: "Kody")
var t1 = ZGTeacher(age: 18, name: "Hello")
t1.age = 20
t1.name = "Logic"
t1 = ZGTeacher(age: 30, name: "Kody")
let s = ZGStudent(age: 18, name: "Hello")
s.age = 25
s.name = "Doman"
s = ZGStudent(age: 18, name: "Hello")
var s1 = ZGStudent(age: 18, name: "Hello")
s1.age = 25
s1.name = "Doman"
s1 = ZGStudent(age: 18, name: "Hello")
- 第25行代碼:t.age因為age屬性是被let修飾的,是一個常量存儲屬性栓袖,被賦值為18后匣摘,不可以再被更改
- 第26行代碼: t.name因為name屬性是被var修飾的,是一個變量存儲屬性裹刮,被賦值為"hello"后音榜,可以被"Logic"這個值修改替換
- 第27行代碼: t被let修飾并賦值為ZGTeacher(age: 18, name: "Hello")后,也是一個常量存儲屬性捧弃,不可以再次更改為ZGTeacher(age: 30, name: "Kody")
- 第30行代碼:t1.age因為age屬性是被let修飾的赠叼,是一個常量存儲屬性,被賦值為18后违霞,不可以再被更改為20
- 第31行代碼: t1這個類是被var修飾的,t1中的name屬性也是被var修飾的,t1中的name屬性可以被再次賦值
- 第32行代碼:t1這個類是被var修飾的嘴办,可以被再次賦值
- 第35行代碼:s結(jié)構(gòu)體是被let修飾的,其中的age也是被let修飾的,被賦值為18后买鸽,不可以更改為25
- 第36行代碼:s結(jié)構(gòu)體是被let修飾的,其中的name雖然是被var修飾的涧郊,但只可以被賦值一次,被賦值為"Hello"后不可以被再次賦值
- 第37行代碼:s結(jié)構(gòu)體是被let修飾的,賦值一次后眼五,不可以再次賦值
- 第40行代碼:s1結(jié)構(gòu)體中的age屬性是被let修飾的,賦值18后妆艘,不可以再次賦值
- 第41行代碼:s1結(jié)構(gòu)體是被var修飾的,結(jié)構(gòu)體中的name也是被var修飾的弹砚,被賦值為"Hello"后双仍,也可以被再次賦值為"doman"
- 第42行代碼:s1結(jié)構(gòu)體是被var修飾的枢希,可以被多次賦值
1.2 let和var的區(qū)別
1.2.1 從匯編的角度
通過上面的截圖可以看到桌吃,常量和變量的存儲沒有明顯的區(qū)別。
1.2.2 從SIL的角度
///@_hasStorage代表是存儲屬性
///@_hasInitialValue代表有初始值
///有g(shù)et和set方法
@_hasStorage @_hasInitialValue var age: Int { get set }
///只有g(shù)et方法
@_hasStorage @_hasInitialValue let x: Int { get }
代表var修飾的age是變量存儲屬性
苞轿,可以調(diào)用get方法獲取值茅诱,也可以調(diào)用set方法賦值逗物,而let 修飾的x是常量存儲屬性
,只可以調(diào)用get方法獲取值,一旦被賦值后不可以再更改瑟俭。
二翎卓、計算屬性
存儲的屬性是最常見的,除了存儲屬性摆寄,類失暴、結(jié)構(gòu)體和枚舉也能夠定義計算屬性
,計算屬性并不存儲值
微饥,他們提供 getter
和setter
來修改和獲取值
逗扒。對于存儲屬性來說可以是常量或變量,但計算屬性必須定義為變量
欠橘。于此同時我們書寫計算屬性時候必須包含類型
矩肩,因為編譯器需要知道期望返回值是什么。
struct square {
///實例當(dāng)中占據(jù)內(nèi)存的
var width: Double
///本質(zhì)是一個方法肃续,不占據(jù)內(nèi)存
var area: Double {
get {
return width * width
}
set {
self.width = newValue
}
}
}
通過上圖可以發(fā)現(xiàn)square.area.setter黍檩,就是一個方法的靜態(tài)調(diào)用,而不是對屬性的存儲始锚。
下面我們看一下只讀的計算屬性和let 有什么區(qū)別
struct square {
var width: Double = 30
var area: Double {
get {
return width * width
}
}
let height: Double = 20
}
編譯成SIL文件看一下
struct square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get }
@_hasStorage @_hasInitialValue let height: Double { get }
init()
init(width: Double = 30)
}
var area: Double { get }和@_hasStorage @_hasInitialValue let height: Double { get } 相同點是都有g(shù)et方法刽酱,但它們的本質(zhì)是不一樣的。area是方法瞧捌,height是let修飾的屬性肛跌。
三、屬性觀察者
屬性觀察者會用來觀察屬性值的變化察郁,一個 willSet
當(dāng)屬性將被改變調(diào)用衍慎,即使這個值與原有的值相同,而 didSet
在屬性已經(jīng)改變之后調(diào)用皮钠。它們的語法類似于 getter
和 setter
稳捆。
class Subject {
var subjectName: String = "" {
///系統(tǒng)默認(rèn)生成的newValue
willSet {
print("subject will set value \(newValue)")
}
didSet {
print("subject will set value \(oldValue)")
}
// ///這里newBody=newValue,如果你不想用系統(tǒng)默認(rèn)創(chuàng)建的值麦轰,可以自己定義一個別名
// willSet (newBody) {
// print("subject will set value \(newBody)")
// }
// ///這里oldBody=oldBody
// didSet(oldBody) {
// print("subject will set value \(oldBody)")
// }
}
}
let s = Subject()
s.subjectName = "Swift"
這里我們在使用屬性觀察器的時候乔夯,需要注意的一點是在初始化期間設(shè)置屬性時不會調(diào)用 willSet
和 didSet
觀察者;只有在為完全初始化的實例分配新值時才會調(diào)用它們款侵。運行下面這段代碼末荐,你會發(fā)現(xiàn)當(dāng)前并不會有任何的輸出。
class Subject {
var subjectName: String = "" {
///系統(tǒng)默認(rèn)生成的newValue
willSet {
print("subject will set value \(newValue)")
}
didSet {
print("subject will set value \(oldValue)")
}
}
init(subjectName: String) {
///初始化的操作
self.subjectName = subjectName
}
}
let s = Subject(subjectName: "Swift")
上面的屬性觀察者只是對存儲屬性起作用新锈,如果我們想對計算屬性起作用怎么辦甲脏?很簡單,只需將相關(guān)代碼添加到屬性的 setter。我們先來看這段代碼:
class Square {
var width: Double
var area: Double {
get {
return width * width
}
set {
self.width = sqrt(newValue)
}
willSet {
print("area will set value \(newValue)")
}
didSet {
print("area has been changed \(oldValue)")
}
}
init(width: Double) {
self.width = width
}
}
對于計算屬性的觀察者:分為基本計算屬性
和帶有繼承的計算屬性
因為沒有初始化方法块请,不需要添加willSet娜氏,didSet觀察者,如果想被觀察者訪問到墩新,只需要將觀察者需要實現(xiàn)的代碼添加到自己本身自帶的setter方法里贸弥。
class ZGTeacher {
var age: Int {
willSet {
print("age will set value \(newValue)")
}
didSet {
print("age has been changed \(oldValue)")
}
}
var height: Double
init(_ age: Int, _ height: Double) {
self.age = age
self.height = height
}
}
class ZGPartTimeTeacher: ZGTeacher {
override var age: Int {
willSet {
print("override age will set value \(newValue)")
}
didSet {
print("override age has been changed \(oldValue)")
}
}
var subjectName: String
init(_ subjectName: String) {
self.subjectName = subjectName
super.init(18, 30.0)
self.age = 20
}
}
let t = ZGPartTimeTeacher("Swift")
打印結(jié)果如下:
override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18
可以得出以下結(jié)論:繼承屬性調(diào)用setter --> 調(diào)用自身willset --> 調(diào)用父類setter --> 父類調(diào)用自身willset --> 賦值 --> 父類調(diào)用自身didset --> 繼承屬性調(diào)用自身didset
四、延遲存儲屬性
- 延遲存儲屬性的初始值在其第一次使用時才進行計算海渊。
- 用關(guān)鍵字
lazy
來標(biāo)識一個延遲存儲屬性绵疲。
class Subject {
lazy var age: Int = 18
}
var s = Subject()
print(s.age)
print("end")
我們來看下lldb的對應(yīng)打印
po s
<Subject: 0x101b32fc0>
(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000200000003
0x101b32fd0: 0x0000000000000000 0x0000000101b33101
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001
18
(lldb) po s
<Subject: 0x101b32fc0>
(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000400000003
0x101b32fd0: 0x0000000000000012 0x0000000101b33100
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001
這是一個存儲屬性,前16個字節(jié)存儲我們的Metadata(0x0000000100008160)
和refro(0x0000000400000003)
16個字節(jié)后開始存儲屬性對應(yīng)的值0x0000000000000000
過掉s.age斷點值存儲進去變?yōu)?strong>0x0000000000000012
臣疑,所以age被lazy
修飾后最岗,所以它是在第一次訪問之后才會對它進行初始化操作。
那么添加lazy
和不添加lazy
對我們的內(nèi)存大小有沒有影響哪朝捆?我們把代碼編譯成sil文件看一下般渡。
class Subject {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
可以看到Int?
,意味著添加了lazy
以后芙盘,屬性被修飾成了一個可選類型驯用。
// variable initialization expression of Subject.$__lazy_storage_$_age
sil hidden [transparent] @$s4main7SubjectC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
bb0:
%0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
return %0 : $Optional<Int> // id: %1
}
lazy_storage
初始化表達(dá)式中 給了一個枚舉值Optional.none!enumelt
,可以看作OC中的nil
%18 = class_method %15 : $Subject, #Subject.age!getter : (Subject) -> () -> Int, $@convention(method) (@guaranteed Subject) -> Int // user: %19
Subject.age!getter調(diào)用age的getter方法, class_method
是一個vtable函數(shù)表
的調(diào)用。
// Subject.age.getter
sil hidden [lazy_getter] [noinline] @$s4main7SubjectC3ageSivg : $@convention(method) (@guaranteed Subject) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Subject):
debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
%4 = load %3 : $*Optional<Int> // user: %6
end_access %3 : $*Optional<Int> // id: %5
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
Subject.age.setter方法首先訪問我們的Subject.$__lazy_storage_$_age
地址ref_element_addr
儒老,然后將這個地址的值給到我們的常量寄存器%4
, switch_enum %4
進行一個枚舉模式的匹配蝴乔,如果有值,走bb1
代碼塊驮樊,沒值薇正,就走bb2
代碼塊。
延遲存儲屬性的初始值在其第一次使用時才進行計算囚衔,相當(dāng)于節(jié)省了我們的內(nèi)存空間挖腰。我們的延遲存儲屬性并不是線程安全的,這點大家要注意练湿。
五猴仑、類型屬性
- 類型屬性其實就是一個全局變量
class ZGTeacher {
static var age: Int = 18
}
類型屬性的調(diào)用方式是 類名+屬性,如上面代碼調(diào)用ZGTeacher.age
我們轉(zhuǎn)譯成sil文件看一下上面的代碼
class ZGTeacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// one-time initialization token for age
sil_global private @$s4main9ZGTeacherC3age_Wz : $Builtin.Word
// static ZGTeacher.age
sil_global hidden @$s4main9ZGTeacherC3ageSivpZ : $Int
我們看sil文件代碼,發(fā)現(xiàn)生成了一個token和一個static修飾的全局變量肥哎。
所以看到這里辽俗,我們應(yīng)該可以理解static本質(zhì)上就是一個全局變量。
- 類型屬性只會被初始化一次
// ZGTeacher.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main9ZGTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
%0 = global_addr @$s4main9ZGTeacherC3age_Wz : $*Builtin.Word // user: %1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref one-time initialization function for age
%2 = function_ref @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () // user: %3
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
%4 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %5
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
return %5 : $Builtin.RawPointer // id: %6
}
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () {
bb0:
alloc_global @$s4main9ZGTeacherC3ageSivpZ // id: %0
%1 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
}
builtin "once"
表示執(zhí)行一次
我們降級成IR文件再來看一下
once_not_done: ; preds = %entry
call void @swift_once(i64* @"$s4main9ZGTeacherC3age_Wz", i8* bitcast (void ()* @"$s4main9ZGTeacherC3age_WZ" to i8*), i8* undef)
br label %once_done
}
這里調(diào)用了@swift_once
篡诽,那么@swift_once
是做什么的哪崖飘,我們到Swift源碼搜一下。我們在once.cpp文件中看到如下代碼:
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
if (! *predicate) {
*predicate = true;
fn(context);
}
#elif defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
可以看出杈女,是調(diào)用了GCD朱浴,確保只被初始化一次吊圾。只初始化一次,就和我們的OC中的單例很像赊琳。
我們先來回顧一下OC單例:
+ (instancetype)sharedInstance {
static Test1 * t = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
t = [[Test1 alloc]init];
});
return t;
}
那么Swift中的單例又是怎么樣的哪?
class ZGTest {
static let sharedInstance = ZGTest()
private init(){}
}
六砰碴、屬性在Mach-o文件中的位置信息
在第一節(jié)課的過程中我們講到了 Metadata
的元數(shù)據(jù)結(jié)構(gòu)躏筏,我們回顧一下
struct Metadata {
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
上一節(jié)課講到方法調(diào)度的過程中我們認(rèn)識了 typeDescriptor
,這里面記錄了 V-Table
的相關(guān) 信息呈枉,接下來我們需要認(rèn)識一下 typeDescriptor 中的 fieldDescriptor
趁尼。
struct TargetClassDescriptor {
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
上節(jié)課我們提到,我們的typeDescriptor
存儲在__TEXT,__Swift_types
里,將自己項目的mach-o文件放到machoView應(yīng)用里猖辫,可以看到:
fieldDescriptor
記錄了當(dāng)前的屬性信息酥泞,其中fieldDescriptor
在源碼中的結(jié)構(gòu)如下:
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
我們用 0xFFFFFF2C+0x00003F4C
得到0x100003E78
,這個就是typeDescriptor
在當(dāng)前Mach-o文件中的地址啃憎,減去我們的虛擬內(nèi)存地址0x100000000
芝囤,得到一個0x3E78
,我們可以直接定位到該地址
看一下這個
typeDescriptor
結(jié)構(gòu)體辛萍,如果我們想要找到fieldDescriptor
地址悯姊,需要從當(dāng)前地址偏移4個4字節(jié)。這個9C存儲的是偏移信息贩毕,所以我們用
0x00003E88
加上偏移信息0x9C
得到0x3F24
這個地址0x3F24
就就代表了我們的fieldDescriptor
悯许,而它后面的地址就是這個結(jié)構(gòu)體存儲的內(nèi)容。
其中 NumFields
代表當(dāng)前有多少個屬性辉阶, FieldRecords
記錄了每個屬性的信息先壕,FieldRecords
的結(jié)構(gòu)體如下:
struct FieldRecord {
Flags uint32
MangledTypeName int32
FieldName int32
}
我們想要拿到FieldRecords
的地址,只需要0x3F24
偏移4個4字節(jié)谆甜。
我們想要拿到
FieldRecords
結(jié)構(gòu)體中的FieldName
來驗證一下拿到的對不對垃僚,那么我們只需要偏移2個4字節(jié)。用0x00003F34 + 0x8
得到0x00003F3C
再加上0xFFFFFFDD
规辱,得到0x100003F19
冈在,減去我們的虛擬內(nèi)存地址0x100000000
,得到0x3F19
是的,這里我們拿到并驗證了age和age1兩個屬性存放的地址按摘。