- 原文博客地址: 淺談Swift的屬性(Property)
- 今年期待已久的
Swift5.0
穩(wěn)定版就已經(jīng)發(fā)布了, 感興趣的小伙伴可看我的這篇博客:Swift 5.0新特性更新 - 這篇博客可主要分享
Swift
的屬性的相關(guān)介紹和剖析, 測試環(huán)境:Xcode 11.2.1
,Swift 5.1.2
屬性分類
在Swift
中, 嚴(yán)格意義上來講屬性可以分為兩大類: 實(shí)例屬性和類型屬性
- 實(shí)例屬性(
Instance Property
): 只能通過實(shí)例去訪問的屬性- 存儲實(shí)例屬性(
Stored Instance Property
): 存儲在實(shí)例的內(nèi)存中, 每個實(shí)例都只有一份 - 計算實(shí)例屬性(
Computed Instance Property
)
- 存儲實(shí)例屬性(
- 類型屬性(
Type Property
): 只能通過類型去訪問的屬性- 存儲類型屬性(
Stored Type Property
): 整個程序運(yùn)行過程中就只有一份內(nèi)存(類似全局變量) - 計算類型屬性(
Computed Type Property
) - 類型屬性可以通過
static
關(guān)鍵字定義; 如果是類也可以通過class
關(guān)鍵字定義
- 存儲類型屬性(
- 實(shí)例屬性屬于一個特定類型的實(shí)例鸭栖,每創(chuàng)建一個實(shí)例,實(shí)例都擁有屬于自己的一套屬性值,實(shí)例之間的屬性相互獨(dú)立
- 為類型本身定義屬性椰棘,無論創(chuàng)建了多少個該類型的實(shí)例,這些屬性全局都只有唯一一份漂佩,這種屬性就是類型屬性
實(shí)例屬性
上面提到Swift
中跟實(shí)例相關(guān)的屬性可以分為兩大類:存儲屬性和計算屬性
- 存儲屬性(
Stored Property
)- 類似于成員變量咒锻,系統(tǒng)會為其分配內(nèi)存空間,存儲屬性存儲在實(shí)例的內(nèi)存中
- 存儲屬性可以是變量存儲屬性(用關(guān)鍵字
var
定義)吱窝,也可以是常量存儲屬性(用關(guān)鍵字let
定義) - 結(jié)構(gòu)體和類可以定義存儲屬性, 枚舉不可以定義存儲屬性
- 計算屬性(
Computed Property
)- 計算屬性其本質(zhì)就是方法(函數(shù)), 系統(tǒng)不會為其分配內(nèi)存空間, 所以計算屬性不會占用實(shí)例對象的內(nèi)存
- 計算屬性不直接存儲值,而是提供一個
getter
和一個可選的setter
迫靖,來間接獲取和設(shè)置其他屬性或變量的值 - 枚舉院峡、絕構(gòu)體和類都可以定義計算屬性
存儲屬性
- 在
Swift
中存儲屬性可以是var
修飾的變量, 也可以是let
修飾的常量 - 但是在創(chuàng)建類或結(jié)構(gòu)體的實(shí)例時, 必須為所有的存儲屬性設(shè)置一個合適的初始值, 否則會報錯的
- 可以在定義屬性的時候, 為其設(shè)置一個初始值
- 可以在
init
初始化器里為存儲實(shí)行設(shè)置一個初始值
struct Person {
// 定義的時候設(shè)置初始值
var age: Int = 24
var weight: Int
}
// 使用init初始化器設(shè)置初始值
var person1 = Person(weight: 75)
var person2 = Person(age: 25, weight: 80)
- 上面兩個屬性是會占用實(shí)例的內(nèi)存空間的
- 可以使用
MemoryLayout
獲取數(shù)據(jù)類型占用的內(nèi)存大小
// Person結(jié)構(gòu)體實(shí)際占用的內(nèi)存大小
MemoryLayout<Person>.size // 16
// 系統(tǒng)為Person分配的內(nèi)存大小
MemoryLayout<Person>.stride // 16
// 內(nèi)存對其參數(shù)
MemoryLayout<Person>.alignment // 8
還有一種使用方式, 輸出結(jié)果一致
var person = Person(weight: 75)
MemoryLayout.size(ofValue: person)
MemoryLayout.stride(ofValue: person)
MemoryLayout.alignment(ofValue: person)
計算屬性
- 枚舉、絕構(gòu)體和類都可以定義計算屬性
- 計算屬性不直接存儲值系宜,而是提供一個
getter
和一個可選的setter
照激,來間接獲取和設(shè)置其他屬性或變量的值 - 計算屬性其本質(zhì)就是方法(函數(shù)), 系統(tǒng)不會為其分配內(nèi)存空間, 所以計算屬性不會占用實(shí)例對象的內(nèi)存
struct Square {
var side: Int
var girth: Int {
set {
side = newValue / 4
}
get {
return side * 4
}
}
}
// 其中set也可以使用下面方式
set(newGirth) {
side = newGirth / 4
}
下面我們先看一下Square
所占用的內(nèi)存大小, 這里方便查看都去掉了print
函數(shù)
var squ = Square(side: 4)
MemoryLayout.size(ofValue: squ) // 8
MemoryLayout.stride(ofValue: squ) // 8
MemoryLayout.alignment(ofValue: squ) // 8
從上面輸出結(jié)果可以看出, Square
只占用8個內(nèi)存大小, 也就是一個Int
占用的內(nèi)存大小, 如果還是看不出來, 可以看一下下面這個
struct Square {
var girth: Int {
get {
return 4
}
}
}
// 輸出結(jié)果0
print(MemoryLayout<Square>.size) // 0
- 從上面兩個輸出結(jié)果可以看出, 計算屬性并不占用內(nèi)存空間
- 此外, 計算屬性雖然不直接存儲值, 但是卻需要
get、set
方法來取值或賦值 - 其中通過
set
方法修改其他相關(guān)聯(lián)的屬性的值; 如果該計算屬性是只讀的, 則不需要set
方法, 傳入的新值默認(rèn)值newValue
, 也可以自定義 - 通過
get
方法獲取該計算屬性的值, 即使是只讀的, 計算屬性的值也是可能發(fā)生改變的 - 定義計算屬性只能使用
var
, 不能使用let
- 下面我們通過匯編的方式來看一下執(zhí)行過程, 在下圖中勾上
Always Show Disassembly
, 右斷點(diǎn)時Xcode
就會在運(yùn)行過程中自動跳到斷電的匯編代碼中
var squ = Square(side: 4)
var c = squ.girth // 在此處加上斷點(diǎn)時
上述代碼的執(zhí)行流程, 通過匯編的方式看, 核心代碼如下所示
下面是在iOS模擬器環(huán)境下一些匯編常用的指令
// 將rax的值賦值給rdi
movq %rax, %rdi
// 將rbp-0x18這個地址值賦值給rsi
leaq -0x18(%rbp), %rsi
// 函數(shù)跳轉(zhuǎn)指令
callq 0x100005428
從上圖可以看到上面代碼對應(yīng)的匯編代碼, 其核心代碼大概可以分為四部分
-
Square
調(diào)用init
初始化器, 即Square
的初始化(詳細(xì)匯編代碼可進(jìn)入callq 0x100001300
中查看) - 講已經(jīng)出初始化的
Square
的對象的內(nèi)存地址賦值給一個全局變量, 即squ
- 調(diào)用
Square
對象里面girth
計算屬性的getter
方法, 獲取girth
的值 - 把獲取的
girth
的值賦值給一個全局變量
如上圖中中斷點(diǎn)位置, 當(dāng)斷電執(zhí)行到此處時, 執(zhí)行
si
命令即可查看getter
函數(shù)的的執(zhí)行過程, 如下圖所示, 其中imulq
是執(zhí)行乘法指令
// 把rdx和rax的相乘的結(jié)果在賦值給rax
imulq %rdx, %rax
下面再看一下, 計算屬性的賦值操作, 代碼如下
var squ = Square(side: 4)
squ.girth = 12;
print(squ.side) // 3
對應(yīng)的匯編代碼如下, 執(zhí)行流程和上面的取值操作類似, 不同的是賦值操作最后執(zhí)行的是girth
的setter
方法
0x1000010c9 <+25>: callq 0x100001300 ; SwiftLanguage.Square.init(side: Swift.Int) -> SwiftLanguage.Square at main.swift:11
0x1000010ce <+30>: leaq 0x6123(%rip), %rsi ; SwiftLanguage.squ : SwiftLanguage.Square
0x1000010d5 <+37>: xorl %ecx, %ecx
0x1000010d7 <+39>: movq %rax, 0x611a(%rip) ; SwiftLanguage.squ : SwiftLanguage.Square
0x1000010de <+46>: movq %rsi, %rdi
0x1000010e1 <+49>: leaq -0x20(%rbp), %rsi
0x1000010e5 <+53>: movl $0x21, %edx
0x1000010ea <+58>: callq 0x10000540a ; symbol stub for: swift_beginAccess
0x1000010ef <+63>: movl $0xc, %edi
0x1000010f4 <+68>: leaq 0x60fd(%rip), %r13 ; SwiftLanguage.squ : SwiftLanguage.Square
0x1000010fb <+75>: callq 0x100001200 ; SwiftLanguage.Square.girth.setter : Swift.Int at main.swift:14
- 只讀計算屬性, 只有
get
沒有set
- 只讀計算屬性的值, 則是根據(jù)關(guān)聯(lián)值的變化而變化, 不可被賦值
// 你可以這樣寫
struct Square {
var side: Int
var girth: Int {
get {
return side * 4
}
}
}
// 也可以這樣寫
var girth: Int {
return side * 4
}
// 還可以這樣寫
var girth: Int { side * 4 }
var squ = Square(side: 4)
// 不可賦值修改
//squ.girth = 12;
print(squ.girth)
枚舉的rawValue
枚舉的rawValue
的本質(zhì)就是計算屬性, 而且是只讀的計算屬性
enum Test: Int {
case test1 = 1
case test2 = 2
}
var c = Test.test1.rawValue
print(c) // 1
至于如何確定, 那么久簡單粗暴點(diǎn), 看匯編
- 上圖中可以看到獲取
rawValue
的值, 其實(shí)就是調(diào)用的rawValue
的getter
方法 - 另外如下所示, 我們對
rawValue
進(jìn)行重新賦值, 會報錯
Test.test1.rawValue = 2
// 這里報錯: Cannot assign to property: 'rawValue' is immutable
那么我們就可以根據(jù)rawValue
的計算屬性修改rawValue
的值
enum Test: Int {
case test1 = 1
case test2 = 2
var rawValue: Int {
switch self {
case .test1:
return 10
case .test2:
return 20
}
}
}
var c = Test.test1.rawValue // 10
延遲存儲屬性
- 使用
lazy
可以定義一個延遲存儲屬性(Lazy Stored Property
), 延遲存儲屬性只有在第一次使用的時候才會進(jìn)行初始化 -
lazy
屬性修飾必須是var
, 不能是let
-
let
修飾的常量必須在實(shí)例的初始化方法完成之前就擁有值
class Car {
init() {
print("Car init")
}
func run() {
print("Car is runing")
}
}
class Person {
lazy var car = Car()
init() {
print("Person init")
}
func goOut() {
car.run()
}
}
let person = Person()
print("--------")
person.goOut()
// 輸出結(jié)果
// Person init
// --------
// Car init
// Car is runing
上述代碼, 在初始化car
的時候如果沒有lazy
, 則輸出結(jié)果如下
/*
Car init
Person init
--------
Car is runing
*/
- 這也就證明了延遲存儲屬性只有在第一次使用的時候才會被初始化
- 此外還有一種復(fù)雜的延遲存儲屬性, 有點(diǎn)類似于
OC
中的懶加載 - 下面代碼中實(shí)際上是一個閉包, 可以吧相關(guān)邏輯處理放在閉包中處理
class Preview {
lazy var image: Image = {
let url = "https://titanjun.oss-cn-hangzhou.aliyuncs.com/swift/square3.png"
let data = Data.init(contentsOf: url)
return Image(data: data)
}()
}
屬性觀察器
在Swift
中可以為非lazy
的并且只能是var
修飾的存儲屬性設(shè)置屬性觀察器, 形式如下
struct Person {
var age: Int {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, age)
}
}
init() {
self.age = 3
print("Person init")
}
}
var p = Person()
p.age = 10
print(p.age)
/* 輸出結(jié)果
Person init
willSet 10
didSet 3 10
10
*/
- 在存儲屬性中定義
willSet
或didSet
觀察者盹牧,來觀察和響應(yīng)屬性值的變化, 從上述輸出結(jié)果我們也可以看到-
willSet
會傳遞新值, 在存儲值之前被調(diào)用, 其默認(rèn)的參數(shù)名是newValue
-
didSet
會傳遞舊值, 在存儲新值之后立即被調(diào)用, 其默認(rèn)的參數(shù)名是oldValue
-
- 當(dāng)每次給存儲屬性設(shè)置新值時俩垃,都會調(diào)用屬性觀察者,即使屬性的新值與當(dāng)前值相同
- 在初始化器中設(shè)置屬性和在定義屬性是設(shè)置初始值都不會觸發(fā)
willSet
或didSet
類型屬性
- 存儲類型屬性(
Stored Type Property
): 整個程序運(yùn)行過程中就只有一份內(nèi)存(類似全局變量) - 計算類型屬性(
Computed Type Property
): 不占用系統(tǒng)內(nèi)存 - 類型屬性可以通過
static
關(guān)鍵字定義; 如果是類也可以通過class
關(guān)鍵字定義 - 存儲類型屬性可以聲明為變量或常量汰寓,計算類型屬性只能被聲明為變量
- 存儲類型屬性必須設(shè)置初始值, 因?yàn)榇鏀?shù)類型屬性沒有
init
初始化器去設(shè)置初始值的方式 - 存儲類型屬性默認(rèn)就是延遲屬性(
lazy
), 不需要使用lazy
修飾符標(biāo)記, 只會在第一次使用的時候初始化, 即使是被多個線程訪問, 也能保證只會被初始化一次
// 在結(jié)構(gòu)體中只能使用static
struct Person {
static var weight: Int = 30
static let height: Int = 100
}
// 取值
let a = Person.weight
let b = Person.height
// 賦值
Person.weight = 12
// let修飾的不可被賦值
//Person.height = 10
在類中可以使用static
和class
class Animal {
static var name: String = "name"
class var age: Int {
return 10
}
}
// 取值
let a1 = Animal.name
let a2 = Animal.age
// 賦值
Animal.name = "animal"
// class定義的屬性是只讀的
// Animal.age = 20
static
- 可以修飾
class
口柳、struct
、enum
類型的屬性或者方法 - 被修飾的
class
中的屬性和方法不可以在子類中被重寫, 重寫會報錯 - 修飾存儲屬性
- 修飾計算屬性
- 修飾類型方法
struct Person {
// 存儲屬性
static var weight: Int = 30
// 計算屬性
static var height: Int {
get { 140 }
}
// 類型方法
static func goShoping() {
print("Person shoping")
}
}
class
- 只能修飾類的計算屬性和方法
- 不能修飾類的存儲屬性
- 修飾的計算屬性和方法可以被子類重寫
class Animal {
// 計算屬性
class var height: Int {
get { 140 }
}
// 類型方法
class func running() {
print("Person running")
}
}
內(nèi)存分析
先看下下面這行代碼的內(nèi)存地址
var num1 = 3
var num2 = 5
var num3 = 7
- 看到的核心匯編代碼如下所示, 就是把3, 5, 7分別賦值給了三個全局變量
- 在匯編語言中,
rip
作為指令指針, -
rip
中存儲著CPU
下一條要執(zhí)行的指令的地址 - 一旦
CPU
讀取一條指令,rip
會自動指向下一條指令(存儲下一條指令的地址) - 比如下面代碼中第二條指令中的
rip
存儲的地址就是第三條指令的地址0x10000138c
0x10000137f <+15>: xorl %ecx, %ecx
// $0x3賦值給num1, 則num1的地址值就是: 0x10000138c + 0x5e6c = 0x1000071F8
0x100001381 <+17>: movq $0x3, 0x5e6c(%rip) ; lazy cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
// $0x5賦值給num2, 則num2的地址值就是: 0x100001397 + 0x5e69 = 0x100007200
0x10000138c <+28>: movq $0x5, 0x5e69(%rip) ; SwiftLanguage.num1 : Swift.Int + 4
// $0x7賦值給num3, 則num3的地址值就是: 0x1000013a2 + 0x5e66 = 0x100007208
0x100001397 <+39>: movq $0x7, 0x5e66(%rip) ; SwiftLanguage.num2 : Swift.Int + 4
0x1000013a2 <+50>: movl %edi, -0x1c(%rbp)
從上面三個內(nèi)存地址可以看出三個全局變量的內(nèi)存地址是相鄰的, 并且彼此相差8個字節(jié), 因?yàn)槊恳粋€
Int
就占用8個字節(jié); 下面再看一下類型屬性和全局變量的內(nèi)存地址
class Animal {
static var age: Int = 10
}
var num1 = 3
Animal.age = 7
var num2 = 5
相關(guān)匯編代碼如圖所示
根據(jù)圖中的相關(guān)核心代碼, 分別計算出num1
, age
和num2
的內(nèi)存地址如下
// $0x3賦值給num1, 則num1的地址值就是: 0x100000fd3 + 0x6785 = 0x100007330
// 通過register命令得到rax的地址為0x100007338, 即為age所在的內(nèi)存地址
// $0x5賦值給num2, 則num2的地址值就是: 0x100001027 + 0x6319 = 0x100007340
/*
0x100007330
0x100007338
0x100007340
*/
// 上述三個內(nèi)存地址同樣也是相鄰, 并且彼此相差8個字節(jié)
所以, 類型屬性也可以理解為全局變量, 不同的是全局變量可以直接訪問, 類型屬性必須通過類名訪問, 有一定的訪問限制而已
線程安全
- 上面有提到, 存儲類型屬性默認(rèn)就是延遲屬性(
lazy
), 不需要使用lazy
修飾符標(biāo)記, 只會在第一次使用的時候初始化 - 即使是被多個線程訪問, 也能保證只會被初始化一次, 是線程安全的
- 從圖中可以看出, 在斷點(diǎn)處給類型屬性
age
賦值之前, 執(zhí)行了很多匯編代碼 - 其中最重要的一條函數(shù)跳轉(zhuǎn)指令
callq
// 進(jìn)入查看具體執(zhí)行的那些操作
0x100000fda <+26>: callq 0x1000010d0 ; SwiftLanguage.Animal.age.unsafeMutableAddressor : Swift.Int at main.swift
將斷點(diǎn)加在此處, 執(zhí)行si
指令即可進(jìn)入該模塊
- 這里看到
swift_once
, 自然就能夠聯(lián)想到dispatch_once
和OC
中的單例模式 - 那就繼續(xù)向下看, 看看
swift_once
里面到底是如何操作的, 還是在swift_once
加上斷點(diǎn), 并執(zhí)行si
指令, 如下圖所示
- 所以, 類型屬性的線程安全最終就是通過
dispatch_once
實(shí)現(xiàn)的 - 屬性的賦值操作相當(dāng)于就是放在
dispatch_once
里面執(zhí)行的, 保證age
的初始化操作永遠(yuǎn)只被執(zhí)行一次
歡迎您掃一掃下面的微信公眾號有滑,訂閱我的博客跃闹!