Swift属性

1,148 阅读6分钟

存储属性

我们先创建一个LYPerson对象,并声明两个存储属性

class LYPerson {
    var age : Int = 19
    var height: Int = 188
}

let p = LYPerson()

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

(lldb) po p
<LYPerson: 0x10062fcc0>

在这里 0x10062fcc0就是我们 实例对象p的HeapObject的地址。 我们来读取 HeapObject的内存分布

(lldb) x/8gx 0x10062fcc0
0x10062fcc0: 0x0000000100008170 0x0000000200000003
0x10062fcd0: 0x0000000000000013 0x0000000000000014 // 19 , 20
0x10062fce0: 0x0000000000000000 0x0000000000000000
0x10062fcf0: 0x0000000000000006 0x0000000000000000

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

存储属性是会占用当前实例对象的内存。

计算属性

我们先来看如下代码

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

let s = Square()

print(s.area)

Square实例对象占用的内存空间大小为24,

class_getInstanceSize(Square.self) // 24

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

计算属性既然不占用实例对象的内存空间,那计算属性存放在哪里呢? 我们通过它的SIL文件来看下

swiftc -emit-sil main.swift > ./main.sil && open main.sil
class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}

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

属性观察者 willset didset

属性观察者可以监听属性值改变时候的值变化

class Square {
    var area: Double = 0.0 {
        willSet {
            print("newValue -- \(newValue) , value --\(area)")
        }
        
        didSet {
            print("oldValue -- \(oldValue), value --\(area)")
        }
    }
}

let s = Square()
s.area = 18

newValue -- 18.0 , value --0.0
oldValue -- 0.0, value --18.0

延迟存储属性 lazy

初始值

使用lazy修饰的变量,必须有一个默认的初始值,否则,编译会报Lazy properties must have an initializer错。

赋值时机

接下来,我们来观察 lazy 属性的值变化

(lldb) po p
<Person: 0x104004240> // 1
(lldb) x/4gx 0x104004240 // 2
0x104004240: 0x0000000100008160 0x0000000200000003
0x104004250: 0x0000000000000000 0x0000000000000000
(lldb) x/4gx 0x104004240 // 3
0x104004240: 0x0000000100008160 0x0000000200000003
0x104004250: 0x0000000000004c4c 0xe200000000000000
  • 1,变量p的内存地址
  • 2,查看name属性未赋值时,p的内存空间。name 值为0000。
  • 3,当第一次获取name属性后,p的内存空间,可以看到 name的值为 4C4CLL。 从上面我们可以看出,延迟存储在第一次访问的时候才被赋值

对对象大小的影响

class Person {
     lazy var age: Int = 0
}

class Man {
    var age: Int = 0
}


var p = Person()
print(class_getInstanceSize(Person.self)) // 32

var m = Man()
print(class_getInstanceSize(Man.self)) // 24

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

swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.sil && open main.sil
class Person {
  lazy var age: Int { get set }
  // 1
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

class Man {
// 2
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @objc deinit
  init()
}
  • lazy修饰Int之后,就变成了 Int?类型,变成了可选型,从 1 和 2 的对比中,我们就可以看出。 在 Person 对象age属性的setter方法中
// 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
} 

我们也可以看到是将 Optional<Int>类型的数据赋值给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

通过 switch_enum,来获取属性值。

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

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

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

小结:

  • 延迟存储必须有一个默认的初始值。
  • 延迟存储在第一次访问的时候才被赋值。
  • 延迟存储属性对实例对象的大小有影响。
  • 延迟存储属性不能保证线程安全。

类型属性

static关键字

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

class Person {
     static var age: Int = 0
}


let age = Person.age

print(age)

我们先将它转化为 SIL

class Person {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}
// static Person.age
sil_global hidden @static main.Person.age : Swift.Int : $Int // 1
  • 1,使用 static修饰后,变成了一个全局属性。
// Person.age.unsafeMutableAddressor
sil hidden [global_init] @main.Person.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
  %2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
  // 1
  %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
}
  • 1,在 age属性赋值时,使用了 builtin "once",在 源码当中,实际调用了 swift_once函数,其源码如下
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if 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函数,保证变量只被初始化一次,并且是线程安全的。

swift单例

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

class Animal {
    static let sharedInstance = Animal()
    private init() {
        
    }
}

let a = Animal.sharedInstance

总结

存储属性会占用当前实例对象的内存空间。 计算属性不会占用当前实例对象的内存空间,其存放在元数据中。 属性观察者用来监测属性的值变化。 延迟存储属性lazy对实例对象的大小有影响,并不能保证线程安全。 类型属性static是线程安全的,只被初始化一次。