Swift探索(三): 屬性

一. 存儲屬性

1. 存儲屬性定義

存儲屬性是一個(gè)作為特定類和結(jié)構(gòu)體實(shí)例一部分的常量或變量。存儲屬性要么是變量存儲屬性 (由 var 關(guān)鍵字引入)要么是常量存儲屬性(由 let 關(guān)鍵字引入)委刘。

var age = 18
let name = "小明"
  • var 用來聲明變量丧没,變量的值可以在將來設(shè)置為不同的值。
  • let 用來聲明常量钱雷,常量的值一旦設(shè)置好便不能再被更改

在創(chuàng)建類或結(jié)構(gòu)體的實(shí)例時(shí)骂铁,必須為所有的存儲屬性設(shè)置一個(gè)初始值≌挚梗可以在初始化器( init )里為存儲屬性設(shè)置一個(gè)初始值拉庵,可以分配一個(gè)默認(rèn)的屬性值作為定義的一部分。

struct PersonStruct {
    var age: Int = 10
    let name: String = "小明"
}

class PersonClass {
    var age: Int
    let name: String

    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
}

let person = PersonStruct()
let person1 = PersonClass(18, "小明")

2. 從SIL查看let和var的區(qū)別

var age = 18
let name = "小明"

生成sil文件后可以看到如下代碼

@_hasStorage @_hasInitialValue var age: Int { get set }

@_hasStorage @_hasInitialValue let name: String { get }

通過 sil 我們可以發(fā)現(xiàn)

  • var 修飾的屬性有 getset 方法
  • let 修飾的屬性只有 get 方法套蒂,所有 let 修飾的屬性不能修改钞支。

二. 計(jì)算屬性

1. 計(jì)算屬性定義

計(jì)算屬性并不存儲值茫蛹,他們提供 gettersetter 來修改和獲取值。對于存儲屬性來說可以是常量或變量烁挟,但計(jì)算屬性必須定義為變量婴洼。于此同時(shí)我們書寫計(jì)算屬性時(shí)候必須包含類型,因?yàn)榫幾g器需要知道期望返回值是什么

struct square {
    var width: Double
    var area: Double {
        get {
            width * width
//            return width * width
        }
        set {
            self.width = newValue
        }
//        set (newArea) {
//            self.width = newArea
//        }
    }
}

重寫一個(gè)實(shí)例的 settergetter 可以用 set撼嗓、get 來修飾柬采,setter 傳入新的默認(rèn)值叫做 newValue,也可以自定義且警。Swift 中粉捻,在擁有返回值的方法里,如果方法內(nèi)部是單表達(dá)式斑芜,那么可以直接省略 return肩刃。

2. 只讀計(jì)算屬性

2.1 方式一
struct square {
    var width: Double = 30.0
    var area: Double {
        get {
            width * width
        }
    }
    let height: Double = 20.0
}
2.2 方式二
struct square {
    var width: Double = 30.0
    private(set) var area : Double
    let height: Double = 20.0
}
2.3 兩種方式的區(qū)別

我們來看一看 square 結(jié)構(gòu)體在 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.0)
}

// 方式二
struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  @_hasStorage private(set) var area: Double { get set }
  @_hasStorage @_hasInitialValue let height: Double { get }
  init(width: Double = 30.0, area: Double)
}

我們可以看到方式二中的 area 實(shí)際上是存儲屬性,只是 set 方法被私有化了杏头。而方式一中的 area沒有任何修飾盈包,只有get方法。

3. 計(jì)算屬性本質(zhì)

我們來查看一下完整代碼的sil文件

struct square {
    var width: Double
    var area: Double {
        get {
            width
        }
        set {
            self.width = newValue
        }
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var s = square(width: 10.0)
        s.width = 20.0
        s.area = 30.0
        let w = s.width
        let a = s.area
    }
}

通過 swiftc ViewController.swift -emit-silViewController.swift 編譯成 sil 文件

// sil 文件中 square 的聲明
struct square {
  @_hasStorage var width: Double { get set }
  var area: Double { get set }
  init(width: Double)
}

我們可以看到存儲屬性 width 和計(jì)算屬性 area 都跟有 { get set } 醇王,那么就說明他們都是有 settergetter 方法的呢燥。我們接著往下看他們具體

3.1 width 屬性的 getter 和 setter 在 sil 的內(nèi)部實(shí)現(xiàn)

// square.width.getter
sil hidden [transparent] @$s4main6squareV5widthSdvg : $@convention(method) (square) -> Double {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $square):
  debug_value %0 : $square, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $square, #square.width // user: %3
  return %2 : $Double                             // id: %3
} // end sil function '$s4main6squareV5widthSdvg'

// square.width.setter
sil hidden [transparent] @$s4main6squareV5widthSdvs : $@convention(method) (Double, @inout square) -> () {
// %0 "value"                                     // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Double, %1 : $*square):
  debug_value %0 : $Double, let, name "value", argno 1 // id: %2
  debug_value_addr %1 : $*square, var, name "self", argno 2 // id: %3
  %4 = begin_access [modify] [static] %1 : $*square // users: %7, %5
  %5 = struct_element_addr %4 : $*square, #square.width // user: %6
  store %0 to %5 : $*Double                       // id: %6
  end_access %4 : $*square                        // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main6squareV5widthSdvs'

可以發(fā)現(xiàn)

  • 在調(diào)用 widthgetter 方法的時(shí)候,可以發(fā)現(xiàn)是直接獲取 squarewidth 的值進(jìn)行返回厦画。
  • 在調(diào)用 widthsetter 方法的時(shí)候疮茄,是直接獲取到 squarewidth 的地址,對地址所指向的內(nèi)容進(jìn)行修改根暑。

3.2 area 屬性的 getter 和 setter 在 sil 的內(nèi)部實(shí)現(xiàn)

// square.area.getter
sil hidden @$s4main6squareV4areaSdvg : $@convention(method) (square) -> Double {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $square):
  debug_value %0 : $square, let, name "self", argno 1 // id: %1
  %2 = struct_extract %0 : $square, #square.width // user: %3
  return %2 : $Double                             // id: %3
} // end sil function '$s4main6squareV4areaSdvg'

// square.area.setter
sil hidden @$s4main6squareV4areaSdvs : $@convention(method) (Double, @inout square) -> () {
// %0 "newValue"                                  // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Double, %1 : $*square):
  debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
  debug_value_addr %1 : $*square, var, name "self", argno 2 // id: %3
  %4 = begin_access [modify] [static] %1 : $*square // users: %7, %5
  %5 = struct_element_addr %4 : $*square, #square.width // user: %6
  store %0 to %5 : $*Double                       // id: %6
  end_access %4 : $*square                        // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function '$s4main6squareV4areaSdvs'

可以發(fā)現(xiàn)跟 width 屬性的 gettersetter 完全一樣力试。但是我們發(fā)現(xiàn)并沒有 area 相關(guān)的存儲變量。所以其實(shí)排嫌,計(jì)算屬性根本不會有存儲在實(shí)例的成員變量畸裳,那也就意味著計(jì)算屬性不占用內(nèi)存。

3.3 在匯編中計(jì)算屬性的實(shí)現(xiàn)

將上面的代碼編譯成匯編代碼淳地,并在 s.width = 20.0 打上斷點(diǎn)

計(jì)算屬性的匯編代碼.png

我們可以看到計(jì)算屬性 area 的賦值和取值的本質(zhì)就是 settergetter 方法怖糊。而存儲屬性 width 則是在進(jìn)行一系列的 movstr 操作颇象。

三. 屬性觀察者

1. 屬性觀察者定義

屬性觀察者用來觀察屬性值的變化伍伤, willSet 當(dāng)屬性將被改變調(diào)用,即使這個(gè)值與原有的值相同遣钳,而 didSet 在屬性已經(jīng)改變之后調(diào)用扰魂。它們的語法類似于 gettersetter

class Person{
    // 存儲屬性
    var  name: String = ""{
        willSet {
            print("name will set value \(newValue)")
        }
        didSet {
            print("name did set value \(oldValue)")
        }
    }
}

let p = Person()
p.name = "小明"

// 打印結(jié)果
name will set value 小明
name did set value 

我們通過 sil來看 willSetdidset是怎么被調(diào)用的

// Person.name.setter
sil hidden @$s4main6PersonC4nameSSvs : $@convention(method) (@owned String, @guaranteed Person) -> () {
// %0 "value"                                     // users: %22, %16, %12, %11, %2
// %1 "self"                                      // users: %20, %13, %11, %4, %3
bb0(%0 : $String, %1 : $Person):
  debug_value %0 : $String, let, name "value", argno 1 // id: %2
  debug_value %1 : $Person, let, name "self", argno 2 // id: %3
  %4 = ref_element_addr %1 : $Person, #Person.name // user: %5
  %5 = begin_access [read] [dynamic] %4 : $*String // users: %6, %8
  %6 = load %5 : $*String                         // users: %21, %9, %20, %7
  retain_value %6 : $String                       // id: %7
  end_access %5 : $*String                        // id: %8
  debug_value %6 : $String, let, name "tmp"       // id: %9
  // function_ref Person.name.willset
  %10 = function_ref @$s4main6PersonC4nameSSvw : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
  retain_value %0 : $String                       // id: %12
  %13 = ref_element_addr %1 : $Person, #Person.name // user: %14
  %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
  %15 = load %14 : $*String                       // user: %17
  store %0 to %14 : $*String                      // id: %16
  release_value %15 : $String                     // id: %17
  end_access %14 : $*String                       // id: %18
  // function_ref Person.name.didset
  %19 = function_ref @$s4main6PersonC4nameSSvW : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
  release_value %6 : $String                      // id: %21
  release_value %0 : $String                      // id: %22
  %23 = tuple ()                                  // user: %24
  return %23 : $()                                // id: %24
} // end sil function '$s4main6PersonC4nameSSvs'

我們可以看到在 Person.namesetter 方法中在賦值操作之前調(diào)用了 willSet 方法,然后進(jìn)行賦值操作劝评,最后再調(diào)用 didSet 方法姐直。

2. 初始化期間設(shè)置屬性時(shí)不會調(diào)用觀察者

在初始化期間設(shè)置屬性時(shí)不會調(diào)用 willSetdidSet 觀察者,只有在為完全初始化的實(shí)例分配新值時(shí)才會調(diào)用它們蒋畜。

class Person{
    var  name: String = "unnamed"{
        willSet {
            print("name will set value \(newValue)")
        }
        didSet {
            print("name did set value \(oldValue)")
        }
    }
    
    init(name: String) {
        self.name = name
    }
}

let p = Person.init(name: "小明")

運(yùn)行上述代碼声畏,可以發(fā)現(xiàn)沒有任何打印結(jié)果

初始化方法.png

可以發(fā)現(xiàn)在初始化方法里是拿到 Person.name 屬性的內(nèi)存地址,然后將字符串直接拷貝到 Person.name 屬性的內(nèi)存地址中姻成,并沒有調(diào)用 getset 方法插龄,是因?yàn)?Person.name 還沒有初始化完成。

3. 觀察者對計(jì)算屬性的觀察

上面的屬性觀察者只是對存儲屬性起作用佣渴,如果我們想對計(jì)算屬性起作用怎么辦?

class Square {
    var width: Double
    var area: Double {
        get {
            return width * width
        }
        set {
            print("do something \(newValue)")
            self.width = sqrt(newValue)
            print("do something \(oldValue)")
        }
    }
    init(width: Double) {
        self.width = width
    }
}

如果我們想對計(jì)算屬性設(shè)置值之前對他進(jìn)行操作辫狼,只需將相關(guān)代碼添加到屬性的 setter 方法中初斑。對于計(jì)算屬性我們沒有必要給他添加 willSetdidSet辛润,只會畫蛇添足。

4. 在繼承中的屬性觀察者

class Person {
    var age: Int {
        willSet {
            print("age will set value \(newValue)")
        }
        didSet {
            print("age did set value \(oldValue)")
        }
    }
    var height: Double
    
    init(_ age: Int, _ height: Double) {
        self.age = age
        self.height = height
    }
}

class subPerson :Person {
    override var age: Int {
        willSet {
            print("override age will set value \(newValue)")
        }
        didSet {
            print("override age did set value \(oldValue)")
        }
    }
    
    var name: String
    init(name: String) {
        self.name = name
        super.init(18, 185.0)
        self.age = 20
    }
}

let p = subPerson.init(name: "小明")

// 打印結(jié)果
override age will set value 20
age will set value 20
age did set value 18
override age did set value 18

我們可以看出對于繼承關(guān)系的屬性他的調(diào)用順序是先調(diào)用子類的 willSet见秤,再調(diào)用父類的 willSet砂竖,再進(jìn)行賦值操作,再調(diào)用父類的 didSet鹃答,最后調(diào)用子類的 didSet乎澄。值得注意的是打印結(jié)果是在執(zhí)行 self.age = 20 這段代碼的時(shí)候打印的,因?yàn)樯厦嫣岬竭^初始化期間設(shè)置屬性時(shí)不會調(diào)用觀察者测摔,因此這里只會打印一次置济。

subPerson.age的setter方法的sil文件.png

通過 sil 文件我們也可以看出 subPerson.agesetter 方法中的調(diào)用順序是先調(diào)用 subPerson.agewillSet 方法,在調(diào)用 Person.agesetter 方法锋八,在調(diào)用 subPerson.agedidSet 方法浙于。

四. 延遲存儲屬性

1. 延遲存儲屬性定義

延時(shí)加載存儲屬性是指當(dāng)?shù)谝淮伪徽{(diào)用的時(shí)候才會計(jì)算其初始值的屬性。在屬性聲明前使用 lazy 來標(biāo)示一個(gè)延時(shí)加載存儲屬性挟纱。

class Person {
    lazy var age: Int = 18
}

var p = Person()

print(p.age)

print("end")

我們通過觀察 p 的內(nèi)存來觀察延遲存儲屬性 age

p的內(nèi)存.png

這個(gè)時(shí)候羞酗,可以看到 p.age 還沒有調(diào)用時(shí)在 metadata + refcount 后面的內(nèi)存空間是沒有值的,接著我們在 print("end") 打個(gè)斷點(diǎn)紊服,讓 age 訪問到檀轨,并打印p 的內(nèi)存
p的內(nèi)存2.png

我們可以看到當(dāng) age 第一次被訪問的時(shí)候這個(gè)內(nèi)存空間有了值。

2. 延遲存儲屬性在sil中的實(shí)現(xiàn)

為了簡化 sil 文件欺嗤,將上述代碼替換成

class Person {
    lazy var age: Int = 18
}

var p = Person()

var t = p.age
class Person {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

通過在 sil 文件中 age 的聲明我們可以看出延遲存儲屬性是可選值参萄,因此在 age 還沒有加載時(shí)為空,在第一次加載后有值煎饼。

// variable initialization expression of Person.$__lazy_storage_$_age
sil hidden [transparent] @$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
bb0:
  %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
  return %0 : $Optional<Int>                      // id: %1
} // end sil function '$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'

$__lazy_storage_$_age 初始化表達(dá)式中可以看到默認(rèn)的空值是 #Optional.none 讹挎,就是一個(gè)空值。

// Person.age.getter
sil hidden [lazy_getter] [noinline] @$s4main6PersonC3ageSivg : $@convention(method) (@guaranteed Person) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $Person, #Person.$__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

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 18        // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  return %19 : $Int                               // id: %20
} // end sil function '$s4main6PersonC3ageSivg'

agegetter 方法中可以看到,首先執(zhí)行 bb0代碼塊淤袜,在 bb0 中首先獲取到 #Person.$__lazy_storage_$_age 的地址痒谴,并把內(nèi)存地址的值讀取到寄存器 %4,然后對 %4 進(jìn)行枚舉模式匹配铡羡,如果有值就走 bb1 的代碼塊积蔚,沒有值就走 bb2 代碼塊。bb1中的具體操作就是直接返回原有的值烦周。bb2中的具體的操作就是將 18 存儲到 #Person.$__lazy_storage_$_age 的內(nèi)存地址當(dāng)中尽爆,并返回這個(gè)值。

注意
如果一個(gè)被標(biāo)記為 lazy 的屬性在沒有初始化時(shí)就同時(shí)被多個(gè)線程訪問读慎,則無法保證該屬性只會被初始化一次漱贱。

五. 類型屬性

1. 類型屬性的定義

實(shí)例屬性屬于一個(gè)特定類型的實(shí)例,每創(chuàng)建一個(gè)實(shí)例夭委,實(shí)例都擁有屬于自己的一套屬性值幅狮,實(shí)例之間的屬性相互獨(dú)立。你也可以為類型本身定義屬性株灸,無論創(chuàng)建了多少個(gè)該類型的實(shí)例崇摄,這些屬性都只有唯一一份。這種屬性就是類型屬性慌烧。
類型屬性其實(shí)就是一個(gè)全局變量逐抑,并且只會被初始化一次。

class Person {
    // 多線程訪問時(shí)也只會被初始化一次
    static var age: Int = 18
}

Person.age = 20

2. 類型屬性的本質(zhì)

我們通過 swiftc main.swift -emit-sil 命令將 main.swift 編譯成 sil 文件來查看

class Person {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

// one-time initialization token for age
sil_global private @$s4main6PersonC3age_Wz : $Builtin.Word

// static Person.age
sil_global hidden @$s4main6PersonC3ageSivpZ : $Int

我們可以看到 age 屬性的聲明只是多了一個(gè) staic 關(guān)鍵字屹蚊。 在這下方我們可以看到多了一個(gè)全局變量 s4main6PersonC3age_Wz厕氨,并且 Person.age 屬性也變成了一個(gè)全局變量。

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = metatype $@thick Person.Type
  // function_ref Person.age.unsafeMutableAddressor
  %3 = function_ref @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
  %5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
  %6 = integer_literal $Builtin.Int64, 20         // user: %7
  %7 = struct $Int (%6 : $Builtin.Int64)          // user: %9
  %8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
  store %7 to %8 : $*Int                          // id: %9
  end_access %8 : $*Int                           // id: %10
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

在初始化的時(shí)候我們可以看到調(diào)用了一個(gè)函數(shù) Person.age.unsafeMutableAddressor 就是內(nèi)存地址的訪問汹粤,接下來我們定位到這個(gè)函數(shù)

// Person.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @$s4main6PersonC3age_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 @$s4main6PersonC3age_WZ : $@convention(c) () -> () // user: %3
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  %4 = global_addr @$s4main6PersonC3ageSivpZ : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s4main6PersonC3ageSivau'

在這個(gè)函數(shù)中命斧,顯示獲取到最開始 Person 聲明下方多出的全局變量 s4main6PersonC3age_Wz 的地址,然后轉(zhuǎn)換成 Builtin.RawPointer 類型玄括,然后又調(diào)用了一個(gè)函數(shù) @$s4main6PersonC3age_WZ : $@convention(c) () -> () 冯丙,接著執(zhí)行 builtin "once",最后返回的是全局變量的內(nèi)存地址遭京。接著定位到 @$s4main6PersonC3age_WZ : $@convention(c) () -> () 這個(gè)函數(shù)當(dāng)中胃惜。

// one-time initialization function for age
sil private [global_init_once_fn] @$s4main6PersonC3age_WZ : $@convention(c) () -> () {
bb0:
  alloc_global @$s4main6PersonC3ageSivpZ          // id: %0
  %1 = global_addr @$s4main6PersonC3ageSivpZ : $*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
} // end sil function '$s4main6PersonC3age_WZ'

這個(gè)函數(shù)當(dāng)中,首先創(chuàng)建全局變量 s4main6PersonC3ageSivpZ 也就是我們的 age哪雕,接著獲取這個(gè)全局變量的內(nèi)存地址船殉,接著構(gòu)建 18 并且將 18 存放到全局變量的內(nèi)存地址中。這個(gè)函數(shù)也就是在初始化 age 變量斯嚎。
在上一步中 builtin "once"是在干什么呢利虫?我們通過 swiftc main.swift -emit-ir 命令將 main.swift 編譯成 IR 文件來查看

define hidden swiftcc i8* @"$s4main6PersonC3ageSivau"() #0 {
entry:
  %0 = load i64, i64* @"$s4main6PersonC3age_Wz", align 8
  %1 = icmp eq i64 %0, -1
  %2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
  br i1 %2, label %once_done, label %once_not_done

once_done:                                        ; preds = %once_not_done, %entry
  %3 = load i64, i64* @"$s4main6PersonC3age_Wz", align 8
  %4 = icmp eq i64 %3, -1
  call void @llvm.assume(i1 %4)
  ret i8* bitcast (%TSi* @"$s4main6PersonC3ageSivpZ" to i8*)

once_not_done:                                    ; preds = %entry
  call void @swift_once(i64* @"$s4main6PersonC3age_Wz", i8* bitcast (void ()* @"$s4main6PersonC3age_WZ" to i8*), i8* undef)
  br label %once_done
}

s4main6PersonC3ageSivau.png

通過 xcrun swift-demangle s4main6PersonC3ageSivau命令我們可以看到 s4main6PersonC3ageSivau 就是在 sil 文件中的 Person.age.unsafeMutableAddressor 函數(shù)挨厚。那么在這里面我們可以看到有一個(gè) @swift_once,可能就是我們在 sil 中看到的 builtin "once"糠惫,我們?nèi)?Swift源碼 中查看 @swift_once 到底干了什么疫剃。

/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
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
}

Once.cpp文件中我們可以找到這個(gè)方法,我們發(fā)現(xiàn)這里使用的 GCDdispatch_once硼讽。所以類型屬性(static修飾的屬性)只會被初始化一次巢价。

3. 單例的寫法

class Person {
    
    static let sharedInstace = Person()
    
    private init(){}
}

Person.sharedInstace

六. 屬性在Mahco文件的位置信息

在之前的文章Swift探索(一): 類與結(jié)構(gòu)體(上)中我們了解到 Swift對象的內(nèi)存結(jié)構(gòu) Metadata 和在Swift探索(二): 類與結(jié)構(gòu)體(下) 方法調(diào)度探索當(dāng)中了解到 typeDescriptor 的結(jié)構(gòu)

struct MataData {
  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
}

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
}

通過Swift源碼我們了解到 Swift對象的屬性存儲在 fieldDescriptor 當(dāng)中,并且 fieldDescriptor 的結(jié)構(gòu)體如下

struct FieldDescriptor {
    var MangledTypeName: Int32  //回寫之后的類型名稱
    var Superclass: Int32 //  父類
    var Kind: uint16 // 標(biāo)識
    var FieldRecordSize: uint16 // 記錄當(dāng)前的大小
    var NumFields: uint32 //有多少個(gè)屬性
    var FieldRecords: [FieldRecord] // 每一個(gè)屬性的具體細(xì)節(jié)
}

struct FieldRecord {
    var Flags: uint32 //標(biāo)志位
    var MangledTypeName: Int32 // 這個(gè)屬性的類型信息
    var FieldName: Int32 // 屬性的名稱
}

我們通過 MachO 來驗(yàn)證

1. 驗(yàn)證流程

class Person {
    var age = 18
    var name = "小明"
}

前面部分跟在在Swift探索(二): 類與結(jié)構(gòu)體(下) 中查找 v-table 的流程一致固阁,首先Section64(_TEXT,__swift5_types)中存放的就是 Descriptor

typeDescriptor的地址.png

因此得到typeDescriptor的地址為:

0xFFFFFF54 + 0x3F44 = 0x100003E98

虛擬地址的首地址.png

Load CommandsLC_SEGMENT_64(__PAGEZERO) 中可以看到虛擬地址的首地址和大小壤躲,因此上一步得到的地址 0x100003E98 減去虛擬內(nèi)存的首地址 0x100000000 就是當(dāng)前typeDescriptor 在虛擬內(nèi)存中的偏移量( offset )。

0x100003E98 - 0x100000000 = 0x3E98

定位到 0x3E98

0x3E98.png

0x3E98 就是 TargetClassDescriptor 這個(gè)結(jié)構(gòu)體類的首地址备燃,后面存儲的就是相應(yīng)成員變量的內(nèi)容碉克,根據(jù)前面對源碼的分析我們得到了 TargetClassDescriptor結(jié)構(gòu)體中的 fieldDescriptor 前面有 4 個(gè) Int32 類型,也就是 4 個(gè) 4 字節(jié)并齐,于是我們向后偏移 4 個(gè) 4 字節(jié)

向后偏移4個(gè)4字節(jié).png

我們可以得到 fieldDescriptorMacho 中的偏移量漏麦,于是 fieldDescriptor的地址為

0x3EA8 + 0x74 = 0x3F1C

定位到 0x3F1C

0x3F1C.png

根據(jù)前面的分析我們知道 FieldDescriptor結(jié)構(gòu)體中的 FieldRecord 要向后偏移16個(gè)字節(jié),并且 FieldRecord 占用 12 個(gè)字節(jié)冀膝,于是可以看出 Section64(__TEXT,__swift5_fieldmd) 內(nèi)就是存儲的 fieldDescriptor 唁奢,因此
fieldDescriptor.png

那么 fieldRecords1FieldName 的地址和fieldRecords2FieldName 的地址就分別是

// fieldRecords1的fieldName
0x3F2C + 4 + 4 + 0xFFFFFFDF =  0x100003F13
// 減去虛擬內(nèi)存地址
0x100003F13  - 0x100000000 =  0x3F13

// fieldRecords2的fieldName
0x3F3C  + 4 + 0xFFFFFFD7 =  0x100003F17
// 減去虛擬內(nèi)存地址
0x100003F17  - 0x100000000 =  0x3F17

定位到 0x3F130x3F17

0x3F13和0x3F17.png

可以看到后面的 Value 就是 agename
通過兩篇文章的 MachO 分析我們大致的了解到

  • Section64(__TEXT,__swift5_types) : 存儲的 typeDescriptor
  • Section64(__TEXT,__swift5_fieldmd) : 存儲的 fieldDescriptor
  • Section64(__TEXT,__swift5_reflstr) : 存儲所有的屬性名稱
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市窝剖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酥夭,老刑警劉巖赐纱,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異熬北,居然都是意外死亡疙描,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門讶隐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來起胰,“玉大人,你說我怎么就攤上這事巫延⌒澹” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵炉峰,是天一觀的道長畏妖。 經(jīng)常有香客問我,道長疼阔,這世上最難降的妖魔是什么戒劫? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任半夷,我火速辦了婚禮,結(jié)果婚禮上迅细,老公的妹妹穿的比我還像新娘巫橄。我一直安慰自己,他們只是感情好茵典,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布嗦随。 她就那樣靜靜地躺著,像睡著了一般敬尺。 火紅的嫁衣襯著肌膚如雪枚尼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天砂吞,我揣著相機(jī)與錄音署恍,去河邊找鬼。 笑死蜻直,一個(gè)胖子當(dāng)著我的面吹牛盯质,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播概而,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼呼巷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赎瑰?” 一聲冷哼從身側(cè)響起王悍,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎餐曼,沒想到半個(gè)月后压储,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡源譬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年深寥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了么夫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖爽雄,靈堂內(nèi)的尸體忽然破棺而出觉渴,到底是詐尸還是另有隱情炮沐,我是刑警寧澤难咕,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站厚脉,受9級特大地震影響习寸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜傻工,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一霞溪、第九天 我趴在偏房一處隱蔽的房頂上張望孵滞。 院中可真熱鬧,春花似錦鸯匹、人聲如沸坊饶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匿级。三九已至,卻和暖如春染厅,著一層夾襖步出監(jiān)牢的瞬間痘绎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工肖粮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孤页,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓涩馆,卻偏偏與公主長得像行施,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子魂那,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容