我們研究過成員屬性的一些具體實現細節,本文我們來研究下類型屬性的底層邏輯。
基本語法
- 類型屬性的語法和成員屬性類似的地方包括:可以定義存儲屬性和計算屬性,也可以添加存儲屬性監聽器
struct Sequence {
static var first: Int = 1 // 存儲屬性
static var second: Int { // 計算屬性
get {
return first
}
set {
first = newValue
}
}
static var third: Int = 3 { // 存儲添加屬性監聽器
didSet {
print("third didSet")
}
willSet {
print("third willSet")
}
}
}
區別是類型屬性要用
static
進行修飾
- 類型屬性不能用
lazy
修飾,因為類型屬性默認就是already-lazy global
不能用lazy修飾
swift_once
實現分析
類型屬性默認是懶加載,我們來看看底層的實現邏輯。
<!-- 測試代碼 -->
struct Sequence {
static var first: Int = 1
}
func test() {
Sequence.first = 2
}
test()
- 獲取
Sequence.first
的內存地址:Sequence.first.unsafeMutableAddressor
Sequence.first.unsafeMutableAddressor
- 如果地址不存在,利用
swift_once
進行變量的初始化
swift_once
-
swift_once
底層調用的是dispatch_once_f
dispatch_once_f
我們得知:編譯器會封裝一個初始化函數,作為
dispatch_once_f
的fn
參數進行初始化調用
-
fn
函數封裝
// one-time initialization function for first
sil private [global_init_once_fn] @$s4main8SequenceV5first_WZ : $@convention(c) () -> () {
bb0:
alloc_global @$s4main8SequenceV5firstSivpZ // id: %0
%1 = global_addr @$s4main8SequenceV5firstSivpZ : $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 1 // 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
}
通過SIL分析,我們得知:編譯器會封裝一個初始化函數,大體的實現邏輯是:
- 得到變量的內存地址, 類似于:
var ptr = withUnsafePointer(to: &Sequence.first) { $0 }
- 將1賦值給這個內存地址上,類似于:
ptr.pointee = 2
總結
- 編譯器會將
var first: Int = 1
封裝成一個函數,函數體是先獲取變量指針,然后給指針所指的內存地址賦值為初始值 - 類型屬性底層是通過
dispatch_once_f
進行初始化,確保只會初始化一次,并且是線程安全的
全局變量
從圖一的編譯器錯誤提示我們可以得知,類型屬性本質就是全局變量,只是有訪問權限限定。
let zero: Int = 0
struct Sequence {
static var first: Int = 1
}
我們利用實例代碼進行分析。
SIL分析
@_hasStorage @_hasInitialValue var zero: Int { get set }
struct Sequence {
@_hasStorage @_hasInitialValue static var first: Int { get set }
init()
}
// zero
sil_global hidden @$s4main4zeroSivp : $Int
// static Sequence.first
sil_global hidden @$s4main8SequenceV5firstSivpZ : $Int
我們看到
SIL
語法中,全局變量和類型屬性的定義是完全相同的。
內存驗證
func test() {
let ptr1 = withUnsafePointer(to: &zero) { UnsafeRawPointer($0) }
let ptr2 = withUnsafePointer(to: &Sequence.first) { UnsafeRawPointer($0) }
print("\(ptr1) \(ptr2)")
}
// 0x100008000 0x100008008
通過查看內存地址,我們得到的結果是
zero
和Sequence.first
的內存地址是連續挨在一起的。zero
肯定是全局變量,所以Sequence.first
本質上也是一個全局變量。
全局變量的更多用法
既然類型屬性是全局變量,那全局變量應該也可以是計算屬性等。其實確實也是可以這樣寫的:
var zero: Int = 0
var one: Int {
get {
zero
}
set {
zero = newValue
}
}
var two: Int = 2 {
willSet {
}
didSet {
}
}
全局變量的語法和類型屬性的語法也是一致的。
變量內存安全(參考地址)
前面我們看到了類型屬性本質是通過swift_once
得到了變量內存地址指針。Swift
編譯器可以(也僅僅只有編譯器可以)獲取到全局變量的內存地址指針。
為什么需要獲取變量的內存地址指針呢?這涉及到內存安全的部分
Swift
會保證同時訪問同一塊內存時不會沖突,通過約束代碼里對于存儲地址的寫操作,去獲取那一塊內存的訪問獨占權。避免了讀寫沖突。
變量內存安全是通過swift_beginAccess
和swift_endAccess
等方法類控制的。
變量內存安全
swift_beginAccess
swift_beginAccess
AccessSet::insert
邏輯總結:
- 先將內存指針封裝成
Access
對象Access
對象的封裝的內存指針如果在SwiftTLSContext::get().accessSet
數組中不存在,說明目前沒有其他方法占用該內存地址,可以訪問,并且將Access
對象保存起來;Access
對象的封裝的內存指針如果在SwiftTLSContext::get().accessSet
數組中存在,說明該內存地址已經有訪問存在了,如果所有的訪問都是讀訪問,則不認為是沖突,可以繼續訪問,否則就會報訪問沖突錯誤。
swift_endAccess
swift_endAccess
邏輯總結:
將當前的訪問從SwiftTLSContext::get().accessSet
數組中移除,也就是將本次內存訪問移除。
總結
- 類型屬性本質上是全局變量,只是訪問權限有所限制
- 類型屬性和全局變量可以是存儲屬性,計算屬性,也可以添加屬性監聽器,但是不能添加懶加載的
lazy
關鍵字 - 類型屬性是懶加載的,通過
dispatch_once_f
進行, 確保只會初始化一次,并且是線程安全的 - 編譯器對類型屬性和全局變量添加了內存安全的控制,避免了訪問的讀寫沖突,使代碼更加安全