初识类与结构体
Swift中用class标识类,用struct标识结构体。
class ClassTeacher{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
deinit{
}
}
struct StructTeacher{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
}
类与结构体的主要同异点
相同点
- 定义存储值的属性
- 定义方法
- 定义下标以使用下标语法提供对其值的访问
- 定义初始化器
- 可以使用 extension 来拓展功能
- 遵循协议来提供某种功能
不同点
- 类有继承的特性,而结构体没有
- 类型转换使您能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对一个类实例有多个引用
类与结构体的根本区别
类是引用类型,结构体是值类型
我们可以通过以下lldb指令进行变量的内存结构分析
po: 输出对应对象
p: 输出值的类型以及命令结果的引用名
x/4g 0x00000ff: 读取指定内存中的值(4g:按格式(8字节)输出4个单位的内存地址)
frame variable -L 打印对象的内存布局(带内存地址)
class类型的变量并不直接存储具体的实例对象,是对当前存储具体实例的内存地址的引用。
从代码角度理解:
struct类型是值类型,直接存储具体的实例(或者说具体的值)。
从代码上看:
内存存储位置的不同
一般情况下,值类型存储在栈上,引用类型存储在堆上。
首先通过下面的图堆内存区域有个基本认识:
我们重点关注其中堆区和栈区
栈区(Stack):局部变量和函数运行过程中的上下文,内存地址较高
堆区(Heap):存储所有对象,内存地址较低
在函数中创建两个局部变量并打印他们的地址,可以发现类对象的地址较小,结构体对象的地址较大。
创建对象耗时的差异
类的内存分配相较于结构体更加复杂,创建对象时耗时也更久。
通过git上的项目可以进行一个简单的性能对比。github.com/knguyen2708…
对象内有1 or 10个成员变量,创建10000000万次耗时。
初始化器
初始化器会为所有的存储属性设置一个合适的初始值。
默认初始化器
默认初始化器即代码中没有定义init函数时,编译器自动提供的初始化器,需要注意的是,编译器不会为类提供默认的成员初始化器,但会在成员变量都有初始化值时提供不带参数的默认初始化器。
结构体内自定义初始化器后,编译器不再提供默认的成员初始化器。
指定初始化器
不带修饰符的init就是指定初始化器,每个类至少有一个指定初始化器。下面代码中两个init都是指定初始化器。
class classObjWithInit {
var age: Int
var name: String
// 指定初始化器
init(age: Int) {
self.age = age
self.name = "default"
}
// 指定初始化器
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
需要注意的点:
- 子类必须向上委托父的初始化器。
- 子类必须先向上委托父类初始化器,然后才能为继承的属性设置新值。
- 子类在向上委托父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 初始化器在所有属性初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。
类的便捷初始化器
便捷初始化器用convenience修饰,struct对象无法使用便捷初始化器。
class classObjWithInit {
var age: Int
var name: String
// 指定初始化器
init(age: Int, name: String) {
self.age = age
self.name = name
}
// 便捷初始化器
convenience init() {
self.init(age: 12, name: "test")
}
}
需要注意的点:
- 便捷初始化器必须从相同的类里调用另一个初始化器。
- 便捷初始化器必须先委托同类中其他初始化器,然后再为任意属性赋新值。
可失败初始化器
即可以初始化失败的初始化器,可在初始化器中使用return nil语句
必要初始化器
在类的初始化器前添加required修饰符,表示该类的子类都必须实现该初始化器。
类的创建(内存分配)
我们从源码层面来解析类的创建过成。
Swift编译
iOS开发的语言不管是OC还是Swift,后端都是通过LLVM进行编译的,如下图所示:
LLVM IR(LLVM Intermediate Representation),它是一种 low-level languange,是一个像RISC的指令集。
Swift通过Swfit编译器生成IR,然后再生成可执行文件,分步流程如下图。
以下命令对应上图各步骤
// 分析输出AST
swiftc main.swift -dump-parse
// 分析并且检查类型输出AST
swiftc main.swift -dump-ast
// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen
// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil
// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir
// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc
// 生成汇编
swiftc main.swift -emit-assembly
// 编译生成可执行.out文件
swiftc -o main.o main.swift
SIL文件分析
SIL是Swift和LLVM IR之间的中间语言。我们可以通过SIL文件分析Swift中类的构建过程。
生成SIL文件
示例代码
class Father {
var age: Int = 12
var name: String = "default"
}
var obj = Father()
在main.swift文件目录下执行命令,会生成main.sil文件,打开后分析关键代码。
swiftc main.swift -emit-sil > ./main.sil
SIL文件分析
在分析SIL前补充一些相关知识:
%0 %1 %2:虚拟寄存器
@main: 函数入口
s4main3objAA6FatherCvp:经过加密的文本,可由命令 xcrun swift-demangle [string]还原
@_hasInitialValue:表示这个属性有初始化值
class Father {
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
init()
@objc deinit
}
@_hasStorage @_hasInitialValue var obj: Father { get set }
// obj
sil_global hidden @$s4main3objAA6FatherCvp : $Father
// main 函数入口
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// 声明一个全局变量 obj:Father
alloc_global @$s4main3objAA6FatherCvp // id: %2
// 获取全局变量obj的地址
%3 = global_addr @$s4main3objAA6FatherCvp : $*Father // user: %15
// 获取Father类的源类型 可以理解为ISA
%4 = metatype $@thick Father.Type // user: %14
// 获取s4main6FatherCACycfC即Father.__allocating_init()的函数指针
// function_ref Father.__allocating_init()
%5 = function_ref @$s4main6FatherCACycfC : $@convention(method) (@thick Father.Type) -> @owned Father // user: %6
// 调用Father.__allocating_init()并传入参数Father的源类型 得到一个实例变量
%6 = apply %5(%4) : $@convention(method) (@thick Father.Type) -> @owned Father // user: %7
// 存储实例变量到全局变量obj中
store %6 to %3 : $*Father // id: %7
// Int是结构体 构建一个Int类型的0
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
// main函数返回0
return %9 : $Int32 // id: %10
} // end sil function 'main'
...
// Father.__allocating_init()
// 需要一个源类型的参数@thick Father.Type
sil hidden [exact_self_class] @$s4main6FatherCACycfC : $@convention(method) (@thick Father.Type) -> @owned Father {
// %0 "$metatype"
bb0(%0 : $@thick Father.Type):
// alloc_ref:申请内存空间 Allocates an object of reference type `T`. The object will be initialized with retain count 1;
%1 = alloc_ref $Father // user: %3
// 获取方法 Father.init()的地址
%2 = function_ref @$s4main6FatherCACycfc : $@convention(method) (@owned Father) -> @owned Father // user: %3
// 调用Father.init()获得初始化后的变量
%3 = apply %2(%1) : $@convention(method) (@owned Father) -> @owned Father // user: %4
return %3 : $Father // id: %4
} // end sil function '$s4main6FatherCACycfC'
从上面的SIL文件中可以发现,最为关键的就是__allocating_init()这个函数,其中最为关键的alloc_ref的关键解释如下:
Allocates an object of reference type
T. The object will be initialized with retain count 1; its state will be otherwise uninitialized. The optionalobjcattribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).
下面会从汇编的角度进一步解析__allocating_init()以及alloc_ref。
汇编分析 对象内存分析
纯Swift类
- 断点,看汇编代码如下,可以看到程序确实会执行
__allocating_init()。 - 进入
__allocating_init(),发现程序会执行swift_allocObject和init()。
继承自NSObject类
重复上述步骤,从
__allocating_init()处断点进入,执行的代码变成了objc_allocWithZone,并通过objc_msgSend发送了init消息。
通过以上的表现对应了
alloc_ref的官方解释,当swift类继承自NSObject时,会使用OC的初始化方法。
The optional
objcattribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).
Swift源码分析
接下来我们通过Swift源码分析swift_allocObject函数。
- 首先在HeapObject.cpp文件中搜索
swift_allocObject, 函数内发送消息调用了_swift_allocObject_函数。'
2. 找到
_swift_allocObject_函数,其主要调用了swift_slowAlloc()函数。
3. 找到
swift_slowAlloc函数,其中通过malloc分配了内存。
综上所述,Swift对象内存分配可以总结为下面的流程。
__allocating_init ----> swift_allocObject ----> _swift_allocObject_ ----> swift_slowAlloc ----> malloc
类的结构
Swift对象的内存结构是HeapObject(OC是 objc_object),在源码中找到对应代码。
可以发现
HeapObject有两个属性:一个是metadata,一个是refCounts。两者加在一起默认占用16字节。
即一个类的实例对象的结构如下:
struct HeapObject {
var metadata: HeapMetadata // 8 bytes
var refCounts: InlineRefCounts // 8 bytes
};
// 对比OC对象的结构
struct objc_object {
isa; // 8bytes
};
HeapMetadata源码分析
- 搜索
HeapMetadata的定义,发现它是TargetHeapMetadata的别名。 - 查看
TargetHeapMetadata,发现它继承自TargetMetadata,同时知道其初始化时可传入MetadataKind(纯Swift类)或ISA(与OC交互的类)类型。补充一下MetadataKind的相关信息
查看MedataKind的定义发现是uint32_t类型变量,总结其种类如下。
3. 查看
TargetMetadata,发现其只有一个私有成员Kind。
4. 我们耐心的查看
TargetHeapMetadata中的代码,发现下面这段代码,里面讲this转为了TargetClassMetadata类型,说明该类型可能是类的最终结构。
5. 查看
TargetClassMetadata类型,结合官方注释,我们得到以下信息:
- 该类就是Swift的
class的metadata - 如果runtime时支持与OC交互,则该类继承自
TargetAnyClassMetadataObjCInterop
- 继续查看
TargetAnyClassMetadataObjCInterop,发现其继承自TargetAnyClassMetadata,自身有两个成员变量:CacheData和Data - 查看
TargetAnyClassMetadata,发现其继承自TargetHeapMetadata,有一个成员变量:Superclass - 结合5-7中的信息,可以得出结论:
类的实例对象中的metadata变量具体类型为TargetClassMetadata,其数据结构大致如下:
struct Metadata {
var Kind: StoredPointer
var SuperClass: TargetClassMetadata*
var CacheData: (Int, Int)
var Data: StoredSize
var Flags: ClassFlags // uint32_t
var InstanceAddressPoint: uint32_t
var InstanceSize: uint32_t
var InstanceAlignmentMask: uint16_t
var Reserved: uint16_t
var ClassSize: uint32_t
var ClassAddressPoint: uint32_t
var Descriptor: TargetClassDescriptor*
var IVarDestroyer: ClassIVarDestroyer*
};
通过类型转换验证
验证类的实例对象
我们自定义HeapObject结构体,如果能将类的实例对象强转成自定义结构体,则说明数据结构正确。
结果证明确实可以强转。
验证Metadata
同理,我们验证TargetClassMetadata的数据结构分析的是否正确。
成功转换,并且能看到其中一些字段的具体值,比如InstanceSize:40(16+24),也能对照上。