Swift類型屬性底層研究

我們研究過成員屬性的一些具體實現細節,本文我們來研究下類型屬性的底層邏輯。

基本語法

  • 類型屬性的語法和成員屬性類似的地方包括:可以定義存儲屬性和計算屬性,也可以添加存儲屬性監聽器
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_ffn參數進行初始化調用

  • 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

通過查看內存地址,我們得到的結果是zeroSequence.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_beginAccessswift_endAccess等方法類控制的。

變量內存安全
swift_beginAccess
swift_beginAccess
AccessSet::insert

邏輯總結:

  1. 先將內存指針封裝成Access對象
  2. Access對象的封裝的內存指針如果在SwiftTLSContext::get().accessSet數組中不存在,說明目前沒有其他方法占用該內存地址,可以訪問,并且將Access對象保存起來;
  3. Access對象的封裝的內存指針如果在SwiftTLSContext::get().accessSet數組中存在,說明該內存地址已經有訪問存在了,如果所有的訪問都是讀訪問,則不認為是沖突,可以繼續訪問,否則就會報訪問沖突錯誤。
swift_endAccess
swift_endAccess

邏輯總結:
將當前的訪問從SwiftTLSContext::get().accessSet數組中移除,也就是將本次內存訪問移除。

總結

  • 類型屬性本質上是全局變量,只是訪問權限有所限制
  • 類型屬性和全局變量可以是存儲屬性,計算屬性,也可以添加屬性監聽器,但是不能添加懶加載的lazy關鍵字
  • 類型屬性是懶加載的,通過dispatch_once_f進行, 確保只會初始化一次,并且是線程安全的
  • 編譯器對類型屬性和全局變量添加了內存安全的控制,避免了訪問的讀寫沖突,使代碼更加安全
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容