- 原文博客地址: 淺談Swift的屬性(Property)
- 今年期待已久的
Swift5.0
穩定版就已經發布了, 感興趣的小夥伴可看我的這篇博客:Swift 5.0新特性更新 - 這篇博客可主要分享
Swift
的屬性的相關介紹和剖析, 測試環境:Xcode 11.2.1
,Swift 5.1.2
屬性分類
在Swift
中, 嚴格意義上來講屬性可以分爲兩大類: 實例屬性和類型屬性
- 實例屬性(
Instance Property
): 只能通過實例去訪問的屬性- 存儲實例屬性(
Stored Instance Property
): 存儲在市裏的內存中, 每個實例都只有一份 - 計算實例屬性(
Computed Instance Property
)
- 存儲實例屬性(
- 類型屬性(
Type Property
): 只能通過類型去訪問的屬性- 存儲類型屬性(
Stored Type Property
): 整個程序運行過程中就只有一份內存(類似全局變量) - 計算類型屬性(
Computed Type Property
) - 類型屬性可以通過
static
關鍵字定義; 如果是類也可以通過class
關鍵字定義
- 存儲類型屬性(
- 實例屬性屬於一個特定類型的實例,每創建一個實例,實例都擁有屬於自己的一套屬性值,實例之間的屬性相互獨立
- 爲類型本身定義屬性,無論創建了多少個該類型的實例,這些屬性全局都只有唯一一份,這種屬性就是類型屬性
實例屬性
上面提到Swift
中跟市裏相關的屬性可以分爲兩大類:存儲屬性和計算屬性
- 存儲屬性(
Stored Property
)- 類似於成員變量,系統會爲其分配內存空間,存儲屬性存儲在實例的內存中
- 存儲屬性可以是變量存儲屬性(用關鍵字
var
定義),也可以是常量存儲屬性(用關鍵字let
定義) - 結構體和類可以定義存儲屬性, 枚舉不可以定義存儲屬性
- 計算屬性(
Computed Property
)- 計算屬性其本質就是方法(函數), 系統不會爲其分配內存空間, 所以計算屬性不會佔用實例對象的內存
- 計算屬性不直接存儲值,而是提供一個
getter
和一個可選的setter
,來間接獲取和設置其他屬性或變量的值 - 枚舉、絕構體和類都可以定義計算屬性
存儲屬性
- 在
Swift
中存儲屬性可以是var
修飾的變量, 也可以是let
修飾的常量 - 但是在創建類或結構體的實例時, 必須爲所有的存儲屬性設置一個合適的初始值, 否則會報錯的
- 可以在定義屬性的時候, 爲其設置一個初始值
- 可以在
init
初始化器裏爲存儲實行設置一個初始值
struct Person {
// 定義的時候設置初始值
var age: Int = 24
var weight: Int
}
// 使用init初始化器設置初始值
var person1 = Person(weight: 75)
var person2 = Person(age: 25, weight: 80)
- 上面兩個屬性是會佔用實例的內存空間的
- 可以使用
MemoryLayout
獲取數據類型佔用的內存大小
// Person結構體實際佔用的內存大小
MemoryLayout<Person>.size // 16
// 系統爲Person分配的內存大小
MemoryLayout<Person>.stride // 16
// 內存對其參數
MemoryLayout<Person>.alignment // 8
還有一種使用方式, 輸出結果一致
var person = Person(weight: 75)
MemoryLayout.size(ofValue: person)
MemoryLayout.stride(ofValue: person)
MemoryLayout.alignment(ofValue: person)
計算屬性
- 枚舉、絕構體和類都可以定義計算屬性
- 計算屬性不直接存儲值,而是提供一個
getter
和一個可選的setter
,來間接獲取和設置其他屬性或變量的值 - 計算屬性其本質就是方法(函數), 系統不會爲其分配內存空間, 所以計算屬性不會佔用實例對象的內存
struct Square {
var side: Int
var girth: Int {
set {
side = newValue / 4
}
get {
return side * 4
}
}
}
// 其中set也可以使用下面方式
set(newGirth) {
side = newGirth / 4
}
下面我們先看一下Square
所佔用的內存大小, 這裏方便查看都去掉了print
函數
var squ = Square(side: 4)
MemoryLayout.size(ofValue: squ) // 8
MemoryLayout.stride(ofValue: squ) // 8
MemoryLayout.alignment(ofValue: squ) // 8
從上面輸出結果可以看出, Square
只佔用8個內存大小, 也就是一個Int
佔用的內存大小, 如果還是看不出來, 可以看一下下面這個
struct Square {
var girth: Int {
get {
return 4
}
}
}
// 輸出結果0
print(MemoryLayout<Square>.size) // 0
- 從上面兩個輸出結果可以看出, 計算屬性並不佔用內存空間
- 此外, 計算屬性雖然不直接存儲值, 但是卻需要
get、set
方法來取值或賦值 - 其中通過
set
方法修改其他相關聯的屬性的值; 如果該計算屬性是隻讀的, 則不需要set
方法, 傳入的新值默認值newValue
, 也可以自定義 - 通過
get
方法獲取該計算屬性的值, 即使是隻讀的, 計算屬性的值也是可能發生改變的 - 定義計算屬性只能使用
var
, 不能使用let
- 下面我們通過彙編的方式來看一下執行過程, 在下圖中勾上
Always Show Disassembly
, 右斷點時Xcode
就會在運行過程中自動跳到斷電的彙編代碼中
var squ = Square(side: 4)
var c = squ.girth // 在此處加上斷點時
上述代碼的執行流程, 通過彙編的方式看, 核心代碼如下所示
下面是在iOS模擬器環境下一些彙編常用的指令
// 將rax的值賦值給rdi
movq %rax, %rdi
// 將rbp-0x18這個地址值賦值給rsi
leaq -0x18(%rbp), %rsi
// 函數跳轉指令
callq 0x100005428
從上圖可以看到上面代碼對應的彙編代碼, 其核心代碼大概可以分爲四部分
Square
調用init
初始化器, 即Square
的初始化(詳細彙編代碼可進入callq 0x100001300
中查看)- 講已經出初始化的
Square
的對象的內存地址賦值給一個全局變量, 即squ
- 調用
Square
對象裏面girth
計算屬性的getter
方法, 獲取girth
的值 - 把獲取的
girth
的值賦值給一個全局變量
如上圖中中斷點位置, 當斷電執行到此處時, 執行
si
命令即可查看getter
函數的的執行過程, 如下圖所示, 其中imulq
是執行乘法指令
// 把rdx和rax的相乘的結果在賦值給rax
imulq %rdx, %rax
下面再看一下, 計算屬性的賦值操作, 代碼如下
var squ = Square(side: 4)
squ.girth = 12;
print(squ.side) // 3
對應的彙編代碼如下, 執行流程和上面的取值操作類似, 不同的是賦值操作最後執行的是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
- 只讀計算屬性的值, 則是根據關聯值的變化而變化, 不可被賦值
// 你可以這樣寫
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
的本質就是計算屬性, 而且是隻讀的計算屬性
enum Test: Int {
case test1 = 1
case test2 = 2
}
var c = Test.test1.rawValue
print(c) // 1
至於如何確定, 那麼久簡單粗暴點, 看彙編
- 上圖中可以看到獲取
rawValue
的值, 其實就是調用的rawValue
的getter
方法 - 另外如下所示, 我們對
rawValue
進行重新賦值, 會報錯
Test.test1.rawValue = 2
// 這裏報錯: Cannot assign to property: 'rawValue' is immutable
那麼我們就可以根據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
), 延遲存儲屬性只有在第一次使用的時候纔會進行初始化 lazy
屬性修飾必須是var
, 不能是let
let
修飾的常量必須在實例的初始化方法完成之前就擁有值
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()
// 輸出結果
// Person init
// --------
// Car init
// Car is runing
上述代碼, 在初始化car
的時候如果沒有lazy
, 則輸出結果如下
/*
Car init
Person init
--------
Car is runing
*/
- 這也就證明了延遲存儲屬性只有在第一次使用的時候纔會被初始化
- 此外還有一種複雜的延遲存儲屬性, 有點類似於
OC
中的懶加載 - 下面代碼中實際上是一個閉包, 可以吧相關邏輯處理放在閉包中處理
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
修飾的存儲屬性設置屬性觀察器, 形式如下
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)
/* 輸出結果
Person init
willSet 10
didSet 3 10
10
*/
- 在存儲屬性中定義
willSet
或didSet
觀察者,來觀察和響應屬性值的變化, 從上述輸出結果我們也可以看到willSet
會傳遞新值, 在存儲值之前被調用, 其默認的參數名是newValue
didSet
會傳遞舊值, 在存儲新值之後立即被調用, 其默認的參數名是oldValue
- 當每次給存儲屬性設置新值時,都會調用屬性觀察者,即使屬性的新值與當前值相同
- 在初始化器中設置屬性和在定義屬性是設置初始值都不會觸發
willSet
或didSet
類型屬性
- 存儲類型屬性(
Stored Type Property
): 整個程序運行過程中就只有一份內存(類似全局變量) - 計算類型屬性(
Computed Type Property
): 不佔用系統內存 - 類型屬性可以通過
static
關鍵字定義; 如果是類也可以通過class
關鍵字定義 - 存儲類型屬性可以聲明爲變量或常量,計算類型屬性只能被聲明爲變量
- 存儲類型屬性必須設置初始值, 因爲存數類型屬性沒有
init
初始化器去設置初始值的方式 - 存儲類型屬性默認就是延遲屬性(
lazy
), 不需要使用lazy
修飾符標記, 只會在第一次使用的時候初始化, 即使是被多個線程訪問, 也能保證只會被初始化一次
// 在結構體中只能使用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")
}
}
內存分析
先看下下面這行代碼的內存地址
var num1 = 3
var num2 = 5
var num3 = 7
- 看到的核心彙編代碼如下所示, 就是把3, 5, 7分別賦值給了三個全局變量
- 在彙編語言中,
rip
作爲指令指針, rip
中存儲着CPU
下一條要執行的指令的地址- 一旦
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)
從上面三個內存地址可以看出三個全局變量的內存地址是相鄰的, 並且彼此相差8個字節, 因爲每一個
Int
就佔用8個字節; 下面再看一下類型屬性和全局變量的內存地址
class Animal {
static var age: Int = 10
}
var num1 = 3
Animal.age = 7
var num2 = 5
相關彙編代碼如圖所示
根據圖中的相關核心代碼, 分別計算出num1
, age
和num2
的內存地址如下
// $0x3賦值給num1, 則num1的地址值就是: 0x100000fd3 + 0x6785 = 0x100007330
// 通過register命令得到rax的地址爲0x100007338, 即爲age所在的內存地址
// $0x5賦值給num2, 則num2的地址值就是: 0x100001027 + 0x6319 = 0x100007340
/*
0x100007330
0x100007338
0x100007340
*/
// 上述三個內存地址同樣也是相鄰, 並且彼此相差8個字節
所以, 類型屬性也可以理解爲全局變量, 不同的是全局變量可以直接訪問, 類型屬性必須通過類名訪問, 有一定的訪問限制而已
線程安全
- 上面有提到, 存儲類型屬性默認就是延遲屬性(
lazy
), 不需要使用lazy
修飾符標記, 只會在第一次使用的時候初始化 - 即使是被多個線程訪問, 也能保證只會被初始化一次, 是線程安全的
- 從圖中可以看出, 在斷點處給類型屬性
age
賦值之前, 執行了很多彙編代碼 - 其中最重要的一條函數跳轉指令
callq
// 進入查看具體執行的那些操作
0x100000fda <+26>: callq 0x1000010d0 ; SwiftLanguage.Animal.age.unsafeMutableAddressor : Swift.Int at main.swift
將斷點加在此處, 執行si
指令即可進入該模塊
- 這裏看到
swift_once
, 自然就能夠聯想到dispatch_once
和OC
中的單例模式 - 那就繼續向下看, 看看
swift_once
裏面到底是如何操作的, 還是在swift_once
加上斷點, 並執行si
指令, 如下圖所示
- 所以, 類型屬性的線程安全最終就是通過
dispatch_once
實現的 - 屬性的賦值操作相當於就是放在
dispatch_once
裏面執行的, 保證age
的初始化操作永遠只被執行一次
歡迎您掃一掃下面的微信公衆號,訂閱我的博客!