Swift内存分配过程
对象的内存分配过程,可以使用符号断点进行验证,下面演示如何为
__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_
函数,负责创建当前实例对象,有下列3个参数。在其内部先后调用swift_slowAlloc
、new HeapObject
两个函数
metadata
:元数据requiredSize
:创建实例对象分配的实际内存大小,这里看到占用40字节requiredAlignmentMask
:字节对齐方式,必须为8的倍速。不足会自动补齐,以空间换取时间,提高访问效率。可以看到占用7字节
找到
swift_slowAlloc
函数:swift_slowAlloc
函数内部,又调用malloc
,负责在堆中创建size大小的内存空间,并进行内存的字节对齐
回到
_swift_allocObject_
函数,当swift_slowAlloc
完成创建内存的工作后,继续执行new 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
针对TargetHeapMetadata
类型取别名,而TargetHeapMetadata
是模板类型,接收一个Inprocess
参数,也是下文中的kind
属性
找到
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
定义:除了包含kind
属性外,还有一个getClassObject
方法。该方法有个返回类型TargetClassMetadata
,其内部通过kind
去匹配上述枚举值。一旦匹配成功,将this
,也就是当前Metadata
的指针,强转相应类型。当前枚举为Class
,所以this
会被强转为ClassMetadata
并返回
下面我们通过
lldb
进行验证
po metadata->getKind()
,打印kind
类型为Class
po metadata->getClassObject()
,打印出元数据内存地址为0x000000010f4dfc88
x/8g 0x000000010f4dfc88
,可以看到元数据里记录的信息
由此得出结论,其实当前的
TargetMetadata
就是TargetClassMetadata
,因为在内存结构中,它们可以直接进行指针转换
找到
TargetClassMetadata
定义:TargetClassMetadata
继承自TargetAnyClassMetadata
,所以拥有了父类的kind
、superclass
、cacheData
等属性
找到
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
->TargetMetaData
TargetMetaData
类似oc中的objc_object
,内含kind
属性HeapMetadata
针对TargetHeapMetadata
取别名,类似oc中的objc_class
TargetHeapMetadata
为模板类型,接收一个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
存放了该类的元数据和引用计数,还存放了该类下的存储属性
计算属性
- 不占⽤存储空间
- 本质是
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
:新值存储前调用,可获取即将被更新的新值newValue
didSet
:新值存储后调用,可获取被更新前的原始值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
属性首次访问的情况:很明显首次访问之前,内存地址是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
。所以说类型属性只会初始化一次,并且它是线程安全的
正确声明⼀个单利:
class LGTeacher{
static let shareInstance = LGTeacher.init()
private init(){ }
}
var t=LGTeacher.shareInstance
- 使用
static
+let
初始化实例对象- 设置
init
方法为private
私有访问权限