存储属性
我们先创建一个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 来说,它的本质是 get
和set
方法,存放在元数据(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的值为4C4C
即LL
。 从上面我们可以看出,延迟存储在第一次访问的时候才被赋值
。
对对象大小的影响
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
是线程安全的,只被初始化一次。