swift进阶3. 属性

324 阅读13分钟

属性

主要介绍存储属性、计算属性、属性观察者、延迟存储属性、类型属性、属性在Mach-o文件的位置信息

一、存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。

存储属性,又分两种:

  • 要么是常量存储属性,即let修饰
  • 要么是变量存储属性,即var修饰

存储属性这里没有什么特别要强调的,因为随处可⻅

class Teacher {
    let age: Int
    var name: String

    init(_ age: Int, _ name: String) {
        self.age = age
        self.name = name
    }
}
struct Student {
    let age: Int
    var name: String
}

比如这里的 agename 就是我们所说的存储属性,这里我们需要加以区分的是 let var 两者的区别:

从定义上:

  • let 用来声明常量,常量的值一旦设置好便不能再被更改;
  • var 用来声明变量,变量的值可以在将来设置为不同的值。

这里我们来看几个案例:

let t = Teacher(18, "Hello")
t.age = 20
t.name = "tony"
let t = Teacher(30, "mark")

var t1 = Teacher(18, "Hello")
t1.age = 20
t1.name = "tony"
t1 = Teacher(30, "mark")

let s = Student(age: 18, name: "Hello")
s.age = 25
s.name = "tony"
s = Student(age: 19, name: "mark")

var s1 = Student(age: 18, name: "Hello")
s1.age = 25
s1.name = "tony"
s1 = Student(age: 19, name: "mark")

1049

let 和 var 的区别:

class Person {
    let age : Int = 18
    var height: Int = 175
    var name : String = "ab"
}
var p = Person()
print(p) //断点
  • 从汇编的⻆度

    我们通过lldb指令来分析p的属性分布, 首先,我们先获取对象p的内存地址

    1050

    在这里0x100682f30就是我们实例对象p的HeapObject的地址。

    我们来读取 HeapObject的内存分布, 其中0x0000000000000012:18, 0x00000000000000af:175, 0x0000000000006261:ab 61=a,62=b

    我们可以得出其内存分布如下:

    1051

  • 从 SIL的⻆度

class Person {
  //_hasStorage 表示是存储属性
  @_hasStorage @_hasInitialValue final let age: Int { get }
  @_hasStorage @_hasInitialValue var height: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

height、name有get、set方法,age没有set方法

1052

存储属性特征:会占用占用分配实例对象的内存空间

二、计算属性

存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 gettersetter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。

struct Square {
    var width: Double
    
    var area: Double {
        get {
            return width * width
        }
        set {
            self.width = newValue
        }
    }
}

class Square {
    var width: Double = 8.0
    var area: Double {
        get {
            return width * width
        }
        set (newValue) {
            width = sqrt(newValue)
        }
    }
}

let s = Square()
print(class_getInstanceSize(Square.self))  //输出24
print(s.area)

Square实例对象占用的内存空间大小为24,我们知道,swift对象默认的大小为16,在Square类中,width属性为Int类型为8字节,对于计算属性来说,它是不占用实例对象内存空间的。

计算属性既然不占用实例对象的内存空间,那计算属性存放在哪里呢?

swiftc -emit-sil main.swift >> ./main.sil

我们通过它的SIL文件来看下, 对于存储属性,有_hasStorage的标识符

struct Square {
  //width有存储属性,实例中占据内存,占8字节
  @_hasStorage var width: Double { get set }
  // area没有存储属性,不占据内存
  var area: Double { get set }
  init(width: Double)
}

class Square {
  // width有存储属性,实例中占据内存,占8字节
  @_hasStorage @_hasInitialValue var width: Double { get set }
  // area没有存储属性,不占据内存
  var area: Double { get set }
  @objc deinit
  init()
}

对于计算属性area 来说,它的本质是get和set方法,存放在元数据(Metadata)中 ,不占用实例对象的内存空间。

// Square.area.setter
sil hidden @main.Square.area.setter : Swift.Double : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "newValue"                                  // users: %5, %2
// %1 "self"                                      // users: %7, %6, %3
bb0(%0 : $Double, %1 : $Square):
  debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
  debug_value %1 : $Square, let, name "self", argno 2 // id: %3
  // function_ref sqrt
  %4 = function_ref @sqrt : $@convention(c) (Double) -> Double // user: %5
  %5 = apply %4(%0) : $@convention(c) (Double) -> Double // user: %7
  %6 = class_method %1 : $Square, #Square.width!setter : (Square) -> (Double) -> (), $@convention(method) (Double, @guaranteed Square) -> () // user: %7
  %7 = apply %6(%5, %1) : $@convention(method) (Double, @guaranteed Square) -> ()
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function 'main.Square.area.setter : Swift.Double'

// Square.area.getter
sil hidden @main.Square.area.getter : Swift.Double : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self"                                      // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
  debug_value %0 : $Square, let, name "self", argno 1 // id: %1
  %2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
  %4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
  %5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
  %6 = struct_extract %3 : $Double, #Double._value // user: %8
  %7 = struct_extract %5 : $Double, #Double._value // user: %8
  %8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
  %9 = struct $Double (%8 : $Builtin.FPIEEE64)    // user: %10
  return %9 : $Double                             // id: %10
} // end sil function 'main.Square.area.getter : Swift.Double'

再比如:

struct Square {
    var width: Double = 10
    let height: Double = 20

    // 内部只读,外部不让访问
    private(set) var area: Double = 30
    
    func test() {
        print(self.area)
    }
}
var s = Square(width: 10.0)
s.area = 20.0

1053

观察它的SIL文件

struct Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  @_hasStorage @_hasInitialValue let height: Double { get }
  @_hasStorage @_hasInitialValue private(set) var area: Double { get set }
  func test()
  init()
  init(width: Double = 10, area: Double = 30)
}

总结:

计算属性:是指不占用内存空间,本质是set/get方法的属性

三、属性观察者

属性观察者会观察用来观察属性值的变化,一个 willSet 当属性将被改变调用,即使这个值与原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 gettersetter

下面我们来验证

class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)")
        }
    }

    init(subjectName: String) {
        self.subjectName = subjectName
    }
}

let s = SubjectName(subjectName: "object-c")
s.subjectName = "swift"

// 输出结果
subjectName will set value swift
subjectName has been changed object-c

也可以通过编译来验证,将main.swift编译成mail.sil,在sil文件中找subjectName的set方法

// SubjectName.subjectName.setter
sil hidden @main.SubjectName.subjectName.setter : Swift.String : $@convention(method) (@owned String, @guaranteed SubjectName) -> () {
// %0 "value"                                     // users: %22, %16, %12, %11, %2
// %1 "self"                                      // users: %20, %13, %11, %4, %3
bb0(%0 : $String, %1 : $SubjectName):
  debug_value %0 : $String, let, name "value", argno 1 // id: %2
  debug_value %1 : $SubjectName, let, name "self", argno 2 // id: %3
  // 访问当前subjectName的值。最终赋值给%6
  %4 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // 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 SubjectName.subjectName.willset
  // 新值赋值前,编译器底层调用了willSet,其中%0是newValue
  %10 = function_ref @main.SubjectName.subjectName.willset : Swift.String : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
  retain_value %0 : $String                       // id: %12
  // %14存储的是%13的内容,%13存储的是%1地址的内容(即SubjectName.subjectName)
  %13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
  %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
  %15 = load %14 : $*String                       // user: %17
  // 将%0(newValue)存储到%14
  store %0 to %14 : $*String                      // id: %16
  release_value %15 : $String                     // id: %17
  end_access %14 : $*String                       // id: %18
  // function_ref SubjectName.subjectName.didset
  // 新值设置完成,调用didSet,其中%6是oldValue
  %19 = function_ref @main.SubjectName.subjectName.didset : Swift.String : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
  release_value %6 : $String                      // id: %21
  release_value %0 : $String                      // id: %22
  %23 = tuple ()                                  // user: %24
  return %23 : $()                                // id: %24
} // end sil function 'main.SubjectName.subjectName.setter : Swift.String'

总结

  • willSet:新值存储之前调用 newValue
  • didSet:新值存储之后调用 oldValue

3.1 init方法中是否会触发属性观察者?

以下代码中,init方法中设置subjectName,是否会触发属性观察者?

class SubjectName {
    var subjectName: String = "object-c" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)")
        }
    }

    init() {
        self.subjectName = "swift"
    }
}

let s = SubjectName()
s.subjectName = "C++"

运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:

  • 在init方法中,如果调用属性,是不会触发属性观察者的
  • 初始化器(即init方法设置)和定义时设置默认值(即在didSet中调用其他属性值)都不会触发

3.2 哪里可以添加属性观察者?

主要有以下三个地方可以添加:

  • 类中定义的存储属性

  • 通过类继承的存储属性

    class SubjectName {
        var subjectName: String = "object-c"
    }
    
    class InheritSubjectName: SubjectName {
        override var subjectName: String {
            willSet{
                print("willSet newValue \(newValue)")
            }
            didSet{
                print("didSet oldValue \(oldValue)")
            }
        }
    }
    
  • 通过类继承的计算属性

class Square {
    var with: Int = 10
    var height: Int {
        get {
            return with
        }
        set {
            self.with = newValue
        }
    }
}
var s = Square()

class Rectangle: Square {
    override var with: Int {
        willSet{
            print("Rectangle with willSet newValue \(newValue)")
        }
        didSet{
            print("Rectangle with didSet oldValue \(oldValue)")
        }
    }
    override var height: Int {
        willSet{
            print("Rectangle height willSet newValue \(newValue)")
        }
        didSet{
            print("Rectangle height didSet oldValue \(oldValue)")
        }
    }
}
let r = Rectangle()
r.with = 20
r.height = 30

// 输出结果
Rectangle with willSet newValue 20
Rectangle with didSet oldValue 10
Rectangle height willSet newValue 30
Rectangle with willSet newValue 30
Rectangle with didSet oldValue 20
Rectangle height didSet oldValue 20

3.3 子类和父类的计算属性同时存在didset、willset时,其调用顺序是什么?

有以下代码,其调用顺序是什么?

class Square {
    var with: Int = 10 {
        willSet{
            print("Square with willSet newValue \(newValue)")
        }
        didSet{
            print("Square with didSet oldValue \(oldValue)")
        }
    }
    var height: Int {
        get {
            return with
        }
        set {
            self.with = newValue
        }
    }
}

class Rectangle: Square {
    override var with: Int {
        willSet{
            print("Rectangle with willSet newValue \(newValue)")
        }
        didSet{
            print("Rectangle with didSet oldValue \(oldValue)")
        }
    }
}
let r = Rectangle()
r.with = 20

// 输出结果
Rectangle with willSet newValue 20
Square with willSet newValue 20
Square with didSet oldValue 10
Rectangle with didSet oldValue 10

结论

对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,在父类didset, 子类的didset

3.4 子类调用了父类的init,是否会触发观察属性?

在问题3.3的基础,修改Rectangle

class Rectangle: Square {
    override var with: Int {
        willSet{
            print("Rectangle with willSet newValue \(newValue)")
        }
        didSet{
            print("Rectangle with didSet oldValue \(oldValue)")
        }
    }
    override init() {
        self.with = 15
    }
}

// 输出结果
Rectangle with willSet newValue 15
Square with willSet newValue 15
Square with didSet oldValue 10
Rectangle with didSet oldValue 10
Rectangle with willSet newValue 20
Square with willSet newValue 20
Square with didSet oldValue 15
Rectangle with didSet oldValue 15

从打印结果发现,会触发属性观察者,主要是因为子类调用了父类init,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init 确保变量初始化完成了),所以会触发观察属性

四、延迟存储属性

4.1 使用lazy修饰的存储属性

class Person {
    lazy var age: Int = 18
}

4.2 延迟属性必须有一个默认的初始值

1054

4.3 延迟存储在第一次访问的时候才被赋值

1055

  • 第一个断点处, 查看age属性未赋值时,p的内存空间地址0x10054e160,age值为0000。

  • 当第一次获取name属性后,p的内存空间地址0x10054e160,可以看到age的值为0x0000000000000012, 即18

从上面我们可以看出,延迟存储在第一次访问的时候才被赋值。

我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle)

swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil
  • lazy修饰的存储属性在底层是一个Optional类型

    class Person {
      lazy var age: Int { get set }
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init()
    }
    
    
    // variable initialization expression of Person.$__lazy_storage_$_age
    sil hidden [transparent] @variable initialization expression of main.Person.($__lazy_storage_$_age in _12232F587A4C5CD8B1EEDF696793A4FC) : Swift.Int? : $@convention(thin) () -> Optional<Int> {
    bb0:
      %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
      return %0 : $Optional<Int>                      // id: %1
    } // end sil function 'variable initialization expression of main.Person.($__lazy_storage_$_age in _12232F587A4C5CD8B1EEDF696793A4FC) : Swift.Int?'
    
  • setter+getter:从getter方法中可以验证,在第一次访问时,就从没值变成了有值的操作

// Person.age.getter
sil hidden [lazy_getter] [noinline] @main.Person.age.getter : Swift.Int : $@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
  // 读取age地址,存储到%3
  %2 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %3
  // 通过%3,读取age值
  %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
  // 此时实际走的流程是bb2,即age是没有值的 (有值走bb1,没有值就走bb2)
  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

// 将默认值20赋值给了可选类型,从这里可以证明lazy修饰的存储属性是第一次访问时才会被赋值
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 'main.Person.age.getter : Swift.Int'



// Person.age.setter
sil hidden @main.Person.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Person) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $Person):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Person, let, name "self", argno 2 // id: %3
  // 根据传入的%0判断是否有值,将对应enum存储到%4
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  // 读取age地址,存储到%5
  %5 = ref_element_addr %1 : $Person, #Person.$__lazy_storage_$_age // user: %6
  // 修改age值,存储到%6
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  // 将enum值存储到%6
  store %4 to %6 : $*Optional<Int>                // id: %7
  end_access %6 : $*Optional<Int>                 // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function 'main.Person.age.setter : Swift.Int'

通过sil,有以下两点说明:

  • lazy修饰的属性,在底层默认是optional,在没有被访问时,默认是nil,在内存中的表现就是0x0。在第一次访问过程中,调用的是属性的getter方法,其内部实现是通过当前enum的分支,来进行一个赋值操作
  • 可选类型是16字节吗?可以通过MemoryLayout打印
    • size:实际大小
    • stride:分配大小(主要是由于内存对齐)
print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

// 输出结果
16
9

为什么实际大小是9?Optional其本质是一个enum,其中Int占8字节,另一个字节主要用于存储case值

4.4 延迟存储属性并不能保证线程安全

继续分析4.3中sil文件,主要是查看age的getter方法,如果此时有两个线程:

  • 线程1此时访问age,其age是没有值的,进入bb2流程
  • 然后时间片将CPU分配给了线程2,对于optional来说,依然是none,同样可以走到bb2流程
  • 所以,在此时,线程1会走一遍赋值,线程2也会走一遍赋值,并不能保证属性只初始化了一次

4.5延迟存储属性对实例对象大小的影响

class Person {
    lazy var age: Int = 18
}
class Teacher {
    var age: Int = 30
}
var p = Person()
var t = Teacher()

print(class_getInstanceSize(Person.self))
print(class_getInstanceSize(Teacher.self))

// 输出结果
32
24

我们可以看出 Teacher对象占用 24 字节 ,Person对象占用 32 字节,为什么使用 lazy修饰Int变量后,对象变大了呢? 我们先使用 swiftc将其转化为SIL文件:

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

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

lazy修饰Int之后,就变成了Int?类型,变成了可选型,从Person和Teacher的对比中,我们就可以看出。

// Person.age.setter
sil hidden @main.Person.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Person) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $Person):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Person, let, name "self", argno 2 // id: %3
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  %5 = ref_element_addr %1 : $Person, #Person.$__lazy_storage_$_age // user: %6
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  store %4 to %6 : $*Optional<Int>                // id: %7
  end_access %6 : $*Optional<Int>                 // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function 'main.Person.age.setter : Swift.Int'

我们也可以看到是将 Optional类型的数据赋值给age属性。

在age属性的 getter方法中

// Person.age.getter
sil hidden [lazy_getter] [noinline] @main.Person.age.getter : Swift.Int : $@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 'main.Person.age.getter : Swift.Int'

通过switch_enum来获取属性值。

Optional<Int>的大小为多少呢?

print(MemoryLayout<Optional<Int>>.size) // 9: 实际大小
print(MemoryLayout<Optional<Int>>.stride) // 16:字节对齐,实际占用的空间大小

所以 age:Int 属性在使用lazy修饰后变为了Int?,对象大小也随之发生了改变。在 getter方法中,由于没有加锁,在多线程同时访问时,并不能保证线程安全。

总结

  • 延迟存储必须有一个默认的初始值。

  • 延迟存储在第一次访问的时候才被赋值。

  • 延迟存储属性对实例对象的大小有影响。

  • 延迟存储属性不能保证线程安全。

五、类型属性

5.1 static关键字

使用static关键字修饰的属性是类型属性。

class Person {
    static var age: Int = 18
}
Person.age = 30

我们先将它转化为 SIL

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

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

// static Person.age
// age变成了全局变量
sil_global hidden @static main.Person.age : Swift.Int : $Int

查看定义,发现多了一个全局变量,说明使用static修饰后,类型属性是一个全局变量

// Person.age.unsafeMutableAddressor
sil hidden [global_init] @main.Person.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @one-time initialization token for age : $*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 @one-time initialization function for age : $@convention(c) () -> () // user: %3
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  %4 = global_addr @static main.Person.age : Swift.Int : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function 'main.Person.age.unsafeMutableAddressor : Swift.Int'
  • 在age属性赋值时,使用了builtin "once",在源码当中,再将它转化为 LL

    swiftc -emit-ir ${SRCROOT}/SwiftTest/main.swift | xcrun swift-demangle > ./main.ll && open main.ll
    
    define hidden swiftcc i8* @"main.Person.age.unsafeMutableAddressor : Swift.Int"() #0 {
    entry:
      %0 = load i64, i64* @"one-time initialization token for age", 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* @"one-time initialization token for age", align 8
      %4 = icmp eq i64 %3, -1
      call void @llvm.assume(i1 %4)
      ret i8* bitcast (%TSi* @"static main.Person.age : Swift.Int" to i8*)
    
    once_not_done:                                    ; preds = %entry
      call void @swift_once(i64* @"one-time initialization token for age", i8* bitcast (void ()* @"one-time initialization function for age" to i8*), i8* undef)
      br label %once_done
    }
    

实际调用了swift_once函数,其源码如下

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
}
  • swift_once本质上调用了GCD的dispatch_once_f函数,保证变量只被初始化一次,并且是线程安全的。

5.2 swift单例

static关键字是线程安全的,并只初始化一次,那我们就可以使用static来创建单例,以下就是swift中单例的创建方法了。

class Person {
    static let sharedInstance = Person()
    private init() {}
}
let instance = Person.sharedInstance

// oc单例
+ (instancetype)sharedInstance{
    static Person *sharedInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[Person alloc] init];
    });
    return sharedInstance;
}
  • 使用关键字static修饰,是一个全局变量
  • 类型属性必须有一个默认的初始值
  • 类型属性只会被初始化一次

六、总结

  • 存储属性会占用当前实例对象的内存空间。
  • 计算属性不会占用当前实例对象的内存空间,其存放在元数据中, 其本质是set/get方法。
  • 属性观察者用来监测属性的值变化。
    • willset:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父
    • didSet:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子
    • 类中的init方法赋值不会触发属性观察
    • 属性可以添加在类定义的存储属性、继承的存储属性、继承的计算属性中
    • 子类调用父类的init方法,会触发观察属性
  • 延迟存储属性lazy对实例对象的大小有影响,并不能保证线程安全。
    • 使用lazy修饰存储属性,且必须有一个默认值
    • 只有在第一次被访问时才会被赋值,且是线程不安全的
    • 使用lazy和不使用lazy,会对实例对象的内存大小有影响,主要是因为lazy在底层是optional类型,optional的本质是enum,除了存储属性本身的内存大小,还需要一个字节用于存储case
  • 类型属性static是线程安全的,只被初始化一次。
    • 使用static 修饰,且必须有一个默认初始值
    • 是一个全局变量,只会被初始化一次,是线程安全的
    • 用于创建单例对象:
      • 使用static + let创建实例变量
      • init方法的访问权限为private

七、属性在Mach-o文件的位置信息

第一节课的过程中我们讲到了 Metadata 的元数据结构,我们回顾一下

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

第二节课讲到方法调度的过程中我们认识了typeDescriptor,这里面记录了 V-Table 的相关信息,接下来我们需要认识一下typeDescriptor 中的fieldDescriptor

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
}

fieldDescriptor 记录了当前的属性信息,其中 fieldDescriptor 在源码中的结构如下:

type FieldDescriptor struct {
    MangledTypeName int32
    Superclass      int32
    Kind            uint16
    FieldRecordSize uint16
    NumFields       uint32
    FieldRecords    []FieldRecord
}

其中NumFields代表当前有多少个属性, FieldRecords 记录了每个属性的信息, FieldRecord的结构体如下:

type FieldRecord struct {
    Flags           uint32
    MangledTypeName int32
    FieldName       int32
}

举例说明:

class Person {
    var age: Int = 18
    var age1: Int = 20
}

运行后,此时你打开MachOView,查看__swift5_types的二进制数据

1056

  • Section64(_TEXT,__swift5_types) 中存放的就是 Descriptor

    计算 Descriptor 在 Mach-O 的内存地址:

    0x3F28 + 0xFFFFFF58 = 0x100003E80
    

    0x10000 是虚拟地址的开端,3E80 就是 Descriptor 在 Mach-O 中的偏移量,定位位置如下

  • Section64(_TEXT,__const)中存放了value witness tablesfull type metadata

如下图最上面红圈就是 Descriptor 的首地址,后面就是 Descriptor 结构体里面的内容,偏移4个 UInt32,找到fieldDescriptor的首地址。

1057

0x3E90 + 0x70 = 0x3F00
  • Section64(_TEXT,__swift5_fieldmd)中存放的就是一组字段描述符(field descriptors)

1058

 0x3F18 + 0xFFFFFFDF = 0x100003EF7

0x10000是虚拟地址的开端,3EF7 就是FieldNameMach-O中的偏移量

  • Section64(_TEXT,__swift5_reflstr)中存放的是所有的属性名称(field names for the properties of the metadata)

1059