Swift--002:类、对象、属性
Swift内存分配过程
对象的内存分配过程,可以使用符号断点进行验证,下面演示如何为
__allocating_init添加断点:选择符号断点
添加__allocating_init
选择汇编模式
运行代码,来到
__allocating_init函数执行内部,发现它做了两件事:
- 调用
swift_allocObject函数- 执行
demo.LGTeacher.init方法,进行初始化变量
demo`LGTeacher.__allocating_init():
0x1000029c0 <+0>: pushq %rbp
0x1000029c1 <+1>: movq %rsp, %rbp
0x1000029c4 <+4>: pushq %r13
0x1000029c6 <+6>: pushq %rax
0x1000029c7 <+7>: movl $0x28, %esi
0x1000029cc <+12>: movl $0x7, %edx
-> 0x1000029d1 <+17>: movq %r13, %rdi
//1、调用swift_allocObject函数
0x1000029d4 <+20>: callq 0x100003be2 ; symbol stub for: swift_allocObject
0x1000029d9 <+25>: movq %rax, %r13
//2、执行demo.LGTeacher.init方法,进行初始化变量
0x1000029dc <+28>: callq 0x100002a20 ; demo.LGTeacher.init() -> demo.LGTeacher at main.swift:10
0x1000029e1 <+33>: addq $0x8, %rsp
0x1000029e5 <+37>: popq %r13
0x1000029e7 <+39>: popq %rbp
0x1000029e8 <+40>: retq
添加
swift_allocObject断点,发现其内部依次调用_swift_allocObject_和swift_slowAlloc两个函数
libswiftCore.dylib`swift_allocObject:
-> 0x7fff6cd73d00 <+0>: pushq %rbp
0x7fff6cd73d01 <+1>: movq %rsp, %rbp
0x7fff6cd73d04 <+4>: pushq %rbx
0x7fff6cd73d05 <+5>: pushq %rax
0x7fff6cd73d06 <+6>: movq %rdi, %rbx
0x7fff6cd73d09 <+9>: movq 0x26d55b98(%rip), %rax ; _swift_allocObject
//1、调用_swift_allocObject_t函数
0x7fff6cd73d10 <+16>: leaq 0x1d69(%rip), %rcx ; _swift_allocObject_
0x7fff6cd73d17 <+23>: cmpq %rcx, %rax
0x7fff6cd73d1a <+26>: jne 0x7fff6cd73d39 ; <+57>
0x7fff6cd73d1c <+28>: movq %rsi, %rdi
0x7fff6cd73d1f <+31>: movq %rdx, %rsi
//2、调用swift_slowAlloc函数
0x7fff6cd73d22 <+34>: callq 0x7fff6cd73c90 ; swift_slowAlloc
0x7fff6cd73d27 <+39>: movq %rbx, (%rax)
0x7fff6cd73d2a <+42>: movq $0x2, 0x8(%rax)
0x7fff6cd73d32 <+50>: addq $0x8, %rsp
0x7fff6cd73d36 <+54>: popq %rbx
0x7fff6cd73d37 <+55>: popq %rbp
0x7fff6cd73d38 <+56>: retq
0x7fff6cd73d39 <+57>: movq %rbx, %rdi
0x7fff6cd73d3c <+60>: addq $0x8, %rsp
0x7fff6cd73d40 <+64>: popq %rbx
0x7fff6cd73d41 <+65>: popq %rbp
0x7fff6cd73d42 <+66>: jmpq *%rax
0x7fff6cd73d44 <+68>: nopw %cs:(%rax,%rax)
0x7fff6cd73d4e <+78>: nop
添加
swift_slowAlloc断点,其内部最终调用了malloc函数
libswiftCore.dylib`swift_slowAlloc:
-> 0x7fff6cd73c90 <+0>: pushq %rbp
0x7fff6cd73c91 <+1>: movq %rsp, %rbp
0x7fff6cd73c94 <+4>: subq $0x10, %rsp
0x7fff6cd73c98 <+8>: movq %rdi, %rdx
0x7fff6cd73c9b <+11>: cmpq $0xf, %rsi
0x7fff6cd73c9f <+15>: ja 0x7fff6cd73cb0 ; <+32>
0x7fff6cd73ca1 <+17>: movq %rdx, %rdi
//调用了malloc函数
0x7fff6cd73ca4 <+20>: callq 0x7fff6cdf028c ; symbol stub for: malloc
0x7fff6cd73ca9 <+25>: testq %rax, %rax
0x7fff6cd73cac <+28>: jne 0x7fff6cd73cdb ; <+75>
0x7fff6cd73cae <+30>: jmp 0x7fff6cd73ce1 ; <+81>
0x7fff6cd73cb0 <+32>: incq %rsi
0x7fff6cd73cb3 <+35>: movl $0x10, %eax
0x7fff6cd73cb8 <+40>: cmovneq %rsi, %rax
0x7fff6cd73cbc <+44>: cmpq $0x8, %rax
0x7fff6cd73cc0 <+48>: movl $0x8, %esi
0x7fff6cd73cc5 <+53>: cmovaq %rax, %rsi
0x7fff6cd73cc9 <+57>: leaq -0x8(%rbp), %rdi
0x7fff6cd73ccd <+61>: callq 0x7fff6cdf038e ; symbol stub for: posix_memalign
0x7fff6cd73cd2 <+66>: movq -0x8(%rbp), %rax
0x7fff6cd73cd6 <+70>: testq %rax, %rax
0x7fff6cd73cd9 <+73>: je 0x7fff6cd73ce1 ; <+81>
0x7fff6cd73cdb <+75>: addq $0x10, %rsp
0x7fff6cd73cdf <+79>: popq %rbp
0x7fff6cd73ce0 <+80>: retq
0x7fff6cd73ce1 <+81>: callq 0x7fff6cdefb00 ; swift_slowAlloc.cold.1
0x7fff6cd73ce6 <+86>: nopw %cs:(%rax,%rax)
以上可以得出⼀个简单的结论:
swift内存分配过程:__allocating_init->swift_allocObject->_swift_allocObject_->swift_slowAlloc->malloc
实例对象的本质
使用VSCode打开Swift源码,找到
_swift_allocObject_函数,添加断点:_swift_allocObject_
_swift_allocObject_函数,负责创建当前实例对象,有下列3个参数。在其内部先后调用swift_slowAlloc、new HeapObject两个函数
metadata:元数据requiredSize:创建实例对象分配的实际内存大小,这里看到占用40字节requiredAlignmentMask:字节对齐方式,必须为8的倍速。不足会自动补齐,以空间换取时间,提高访问效率。可以看到占用7字节
找到
swift_slowAlloc函数:swift_slowAlloc
swift_slowAlloc函数内部,又调用malloc,负责在堆中创建size大小的内存空间,并进行内存的字节对齐
回到
_swift_allocObject_函数,当swift_slowAlloc完成创建内存的工作后,继续执行new HeapObject来进行初始化对象的工作,最终返回HeapObject结构体
了解一下
HeapObject结构体:HeapObject 实例对象初始化需要
metadata和refCounts两个参数:
metadata:元数据,类型为HeapMetadata,是指针类型,占8字节refCounts:引用计数,因为swift也是采用ARC进行内存管理。类型为InlineRefCounts,而InlineRefCounts是Class类型,占8字节
实例对象的本质:
oc:objc_object结构体,默认有class类型的isa指针,占8字节swift:HeapObject结构体,默认有元数据metadata、引用计数refCounts,占16字节
类结构的探索
找到
HeapMetadata定义:HeapMetadata
HeapMetadata针对TargetHeapMetadata类型取别名,而TargetHeapMetadata是模板类型,接收一个Inprocess参数,也是下文中的kind属性
找到
TargetHeapMetaData定义:TargetHeapMetaData
TargetHeapMetadata继承自TargetMetaData,TargetMetaData内部有kind属性,对于kind属性其实就是unsigned long类型,主要用于区分是哪种类型的元数据。
找到
MetadataKind.def文件,里面记录了所有元数据类型,包含Class、Struct、Enum等
| Name | Value |
|---|---|
| Class | 0x0 |
| Struct | 0x200 |
| Enum | 0x201 |
| Optional | 0x2027 |
| ForeignClass | 0x203 |
| Opaque | 0x300 |
| Tuple | 0x301 |
| Function | 0x302 |
| Existential | 0x303 |
| Metatype | 0x304 |
| ObjCClassWrapper | 0x305 |
| ExistentialMetatype | 0x306 |
| HeapLocalVariable | 0x400 |
| HeapGenericLocalVariable | 0x500 |
| ErrorObject | 0x501 |
| LastEnumerated | 0x7FF |
找到
TargetMetaData定义:TargetMetaData -getClassObject 除了包含
kind属性外,还有一个getClassObject方法。该方法有个返回类型TargetClassMetadata,其内部通过kind去匹配上述枚举值。一旦匹配成功,将this,也就是当前Metadata的指针,强转相应类型。当前枚举为Class,所以this会被强转为ClassMetadata并返回
下面我们通过
lldb进行验证
通过lldb验证
po metadata->getKind(),打印kind类型为Classpo metadata->getClassObject(),打印出元数据内存地址为0x000000010f4dfc88x/8g 0x000000010f4dfc88,可以看到元数据里记录的信息
由此得出结论,其实当前的
TargetMetadata就是TargetClassMetadata,因为在内存结构中,它们可以直接进行指针转换
找到
TargetClassMetadata定义:TargetClassMetadata
TargetClassMetadata继承自TargetAnyClassMetadata,所以拥有了父类的kind、superclass、cacheData等属性
找到
TargetAnyClassMetadata定义:TargetAnyClassMetadata
TargetAnyClassMetadata继承自TargetHeapMetadata,而TargetHeapMetadata又继承自TargetMetadata,所以拥有了父类的kind属性。
经过源码阅读,我们得出当前
metadata的数据结构体如下:
struct swift_class_t : NSObject {
void *kind; //8字节
void *superClass;
void *cacheData
void *data
uint32_t flags; //4字节
uint32_t instanceAddressOffset; //4字节
uint32_t instanceSize;//4字节
uint16_t instanceAlignMask; //2字节
uint16_t reserved; //2字节
uint32_t classSize; //4字节
uint32_t classAddressOffset; //4字节
void *description;
// ...
};
类结构的探索:
- 当
metadata的kind类型为Class,类结构继承关系如下:
TargetClassMetadata->TargetAnyClassMetadata->TargetHeapMetadata->TargetMetaDataTargetMetaData类似oc中的objc_object,内含kind属性HeapMetadata针对TargetHeapMetadata取别名,类似oc中的objc_classTargetHeapMetadata为模板类型,接收一个Inprocess参数,也就是kind属性kind属性为unsigned long类型,类似oc中的isa
Swift属性
存储属性:
- 占用存储空间
- 要么是
let修饰的常量- 要么是
var修饰的变量
class LGTeacher{
let age: Int = 18
var name: String = "Zang"
}
let t = LGTeacher()
上述代码中的
age和name,都是变量存储属性
通过SIL进行验证:
class LGTeacher {
//_hasStorage 表示是存储属性
@_hasStorage @_hasInitialValue final let age: Int { get }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
为什么说存储属性占用存储空间?
class LGTeacher{
let age: Int = 18
var name: String = "Zang"
}
let t = LGTeacher()
print("size of t.age: \(MemoryLayout.size(ofValue: t.age))")
print("size of t.name: \(MemoryLayout.size(ofValue: t.name))")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")
//输出以下内容:
//size of t.age: 8
//size of t.name: 16
//size of LGTeacher Class: 40
通过上面代码可以看出
LGTeacher Class共占40字节
metadata:元数据,占8字节refCounts:引用计数,占8字节age:Int类型存储属性,占8字节name:String类型存储属性,占16字节
再来使用
po、x/8g,查看HeapObject存储地址:HeapObject存储地址 可以看出
HeapObject存放了该类的元数据和引用计数,还存放了该类下的存储属性
计算属性
- 不占⽤存储空间
- 本质是
get/set⽅法
class Square{
var width: Double = 8.0
var area: Double{
get{
return width * width
}
set{
width = sqrt(newValue)
}
}
}
print("size of Square Class: \(class_getInstanceSize(Square.self))")
//输出以下内容:
//size of Square Class: 24
通过上面代码可以看出
Square Class共占24字节
metadata:元数据,占8字节refCounts:引用计数,占8字节width:Double类型存储属性,占8字节area:计算属性,本质是get/set⽅法,不占⽤存储空间
通过SIL进行验证:
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
上述代码中,只有
width属性被标记了_hasStorage,所以只有它是存储属性。而area属性有的只是get/set⽅法,所以它是计算属性
SIL中的
getter⽅法
// Square.width.getter
sil hidden [transparent] @$s4main6SquareC5widthSdvg : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self" // users: %2, %1
bb0(%0 : $Square):
debug_value %0 : $Square, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Square, #Square.width // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Double // users: %4, %5
%4 = load %3 : $*Double // user: %6
end_access %3 : $*Double // id: %5
return %4 : $Double // id: %6
} // end sil function '$s4main6SquareC5widthSdvg'
SIL中的
setter⽅法
// Square.width.setter
sil hidden [transparent] @$s4main6SquareC5widthSdvs : $@convention(method) (Double, @guaranteed 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 %1 : $Square, let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $Square, #Square.width // user: %5
%5 = begin_access [modify] [dynamic] %4 : $*Double // users: %6, %7
store %0 to %5 : $*Double // id: %6
end_access %5 : $*Double // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function '$s4main6SquareC5widthSdvs'
属性观察者
willSet:新值存储前调用,可获取即将被更新的新值newValuedidSet:新值存储后调用,可获取被更新前的原始值oldValue
class LGTeacher{
var name: String = "无"{
willSet{
print("willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
}
didSet{
print("didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
}
}
}
var t = LGTeacher()
t.name = "Zang"
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")
//输出以下内容:
//willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无
//size of LGTeacher Class: 32
init方法中修改属性,能否触发属性观察者的
willSet、didSet?
//父类LGTeacher
class LGTeacher{
var name: String = "无" {
willSet{
print("willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
}
didSet{
print("didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
}
}
init() {
self.name = "Teacher"
}
}
//子类LGChild
class LGChild : LGTeacher{
override init() {
super.init()
self.name = "Child"
}
}
var t = LGChild()
//输出以下内容:
//willSet-新值存储前调用,当前值:Teacher,即将被更新为:Child
//didSet-新值存储后调用,当前值:Child,被更新前的原始值:Teacher
对上述结果的打印,有些神奇的地方:
- 父类
init方法将name赋值为Teacher,但并没有触发willSet、didSet
因为此时父类的初始化还未完成- 子类
init方法将name赋值为Child,此时却触发了willSet、didSet
因为此时super.init,也就是父类的初始化已完成init方法会调用memset清理其他属性的内存空间(不包括metadata、refCounts),因为有可能是脏数据,被别人使用过,之后才会赋值。
能否在当前类的计算属性上,再添加属性观察者?
计算属性,能否添加属性观察者?
很明显,在当前类的计算属性上,无法再添加属性观察者。编译报错:“willSet cannot be provided together with a getter”
但我们可以通过类的继承,对父类的计算属性,通过子类添加属性观察者
class Square{
var width: Double = 8.0
var area: Double{
get{
return width * width
}
set{
width = sqrt(newValue)
}
}
}
class LGChild : Square{
override var area: Double{
willSet{
print("willSet-新值存储前调用,当前值:\(area),即将被更新为:\(newValue)")
}
didSet{
print("didSet-新值存储后调用,当前值:\(area),被更新前的原始值:\(oldValue)")
}
}
}
var t = LGChild()
t.area=16;
//输出以下内容:
//willSet-新值存储前调用,当前值:64.0,即将被更新为:16.0
//didSet-新值存储后调用,当前值:16.0,被更新前的原始值:64.0
子类和父类能否同时存在
willSet、didSet?
class LGTeacher{
var name: String = "无" {
willSet{
print("LGTeacher-willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
}
didSet{
print("LGTeacher-didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
}
}
}
class LGChild : LGTeacher{
override var name: String {
willSet{
print("LGChild-willSet-新值存储前调用,当前值:\(name),即将被更新为:\(newValue)")
}
didSet{
print("LGChild-didSet-新值存储后调用,当前值:\(name),被更新前的原始值:\(oldValue)")
}
}
}
var t = LGChild()
t.name="Zang";
//输出以下内容:
//LGChild-willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//LGTeacher-willSet-新值存储前调用,当前值:无,即将被更新为:Zang
//LGTeacher-didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无
//LGChild-didSet-新值存储后调用,当前值:Zang,被更新前的原始值:无
上述代码证明子类和父类,可以同时存在
willSet和didSet。
调用顺序:子类willSet->父类willSet->父类didSet->子类didSet
t是子类的实例对象,当name属性被修改,首先触发子类的willSet方法- 之后会调用父类的
setter方法- 然后触发父类的
willSet和didSet方法- 最后触发子类的
didSet方法
延迟存储属性
- 使⽤
lazy修饰的存储属性- 延迟存储属性必须有⼀个默认初始值
- 延迟存储属性在第⼀次访问的时候才会被赋值
- 延迟存储属性并不能保证线程安全
- 延迟存储属性会影响实例对象的⼤⼩
class LGTeacher{
lazy var age: Int = 18
}
var t = LGTeacher()
print("age: \(t.age)")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")
//输出以下内容:
//age: 18
//size of LGTeacher Class: 32
对上述结果的打印,有些神奇的地方:
正常来说LGTeacher应该由metadata、refCounts、Int属性age组成,共占24字节。但打印结果中的LGTeacher,为什么输出32字节?
继续通过SIL查看代码:
class LGTeacher {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
通过SIL可以看到
age属性,由于设置lazy关键字的原因,被加上了final修饰符,类型变为Optional<Int>可选类型
Optional<Int>:Optional本质是enum占1字节,Int占8字节,故此Optional<Int>占9字节。加上metadata、refCounts共计25字节,再经过内存对齐,需要8的倍数,最终LGTeacher输出32字节。
再来使用
po、x/8g,查看lazy属性首次访问的情况:lazy属性第一次访问 很明显首次访问之前,内存地址是
0x0,没有值。当使用t.age触发getter方法后,地址变成0x12,也就是18。
通过SIL的
getter方法验证:
// LGTeacher.age.getter
sil hidden [lazy_getter] [noinline] @main.LGTeacher.age.getter : Swift.Int : $@convention(method) (@guaranteed LGTeacher) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $LGTeacher):
debug_value %0 : $LGTeacher, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $LGTeacher, #LGTeacher.$__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
//这里在验证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
bb2: // Preds: bb0
//将初始化的默认值18赋值给age属性
%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 : $LGTeacher, #LGTeacher.$__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.LGTeacher.age.getter : Swift.Int'
- 在
getter方法中,读取age属性的值。再通过case #Optional.some!enumelt判断age属性是否有值,有值进入bb1流程,没值进入bb2流程。首次使用age属性没有值,进入bb2将初始化的默认值18赋值给age属性。- 这段代码也可以看出
lazy并不能保证线程安全。当多线程同时对age属性赋值时,getter方法中case #Optional.some!enumelt判断,很可能多次进入bb2流程,造成多次初始化的情况。
类型属性
- 使⽤
static来声明⼀个类型属性- 类型属性属于这个类的本身,不管有多少个实例,类型属性只有⼀份
- 类型属性必须有⼀个默认初始值
- 类型属性只会被初始化一次
- 类型属性是线程安全的
class LGTeacher{
static var age: Int = 18
}
var t = LGTeacher()
//print("age: \(t.age)")
print("age: \(LGTeacher.age)")
print("size of LGTeacher Class: \(class_getInstanceSize(LGTeacher.self))")
//输出以下内容:
//age: 18
//size of LGTeacher Class: 16
- 类型属性必须有初始值,否则编译报错:“static var declaration requires an initializer expression or getter/setter specifier”
- 类型属性必须通过
LGTeacher.age访问,不能通过t.age访问,后者编译报错:“Static member age cannot be used on instance of type LGTeacher”LGTeacher输出16字节,说明LGTeacher里不包含类型属性的存储空间
通过SIL进行验证:
class LGTeacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
sil_global private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $Builtin.Word
// static LGTeacher.age
sil_global hidden @static main.LGTeacher.age : Swift.Int : $Int
通过最后一行代码就能看出,
age属性变成全局变量,所以说类型属性其实就是一个全局变量
添加符号断点查看汇编代码,可以看到调用了一个
swift_once方法,它就是GCD的单例方法dispatch_once_f。所以说类型属性只会初始化一次,并且它是线程安全的swift_once
正确声明⼀个单利:
class LGTeacher{
static let shareInstance = LGTeacher.init()
private init(){ }
}
var t=LGTeacher.shareInstance
- 使用
static+let初始化实例对象- 设置
init方法为private私有访问权限
选择符号断点
添加__allocating_init
选择汇编模式
_swift_allocObject_
swift_slowAlloc
HeapObject
实例对象初始化需要
HeapMetadata
TargetHeapMetaData
TargetMetaData -getClassObject
除了包含
通过lldb验证
TargetClassMetadata
TargetAnyClassMetadata
HeapObject存储地址
可以看出
计算属性,能否添加属性观察者?
lazy属性第一次访问
很明显首次访问之前,内存地址是
swift_once