一、结构体
在swift中,结构体是非常重要的数据结构,swift的标准库中,绝大多数的公开类型都是结构体,例如Bool、Int、Double、 String、Array、Dictionary 等常见类型都是结构体。
结构体的定义方式如下:
struct FYPerson {
var age: Int
var name: String
}
1.结构体的初始化
所有的结构体都有一个有编译器自动生成的一个初始化器,以保证结构体中所有的成员都有初始值。
如果你自定义了一个初始化器,那么编译器就不会给你生成一个初始化器。
我们在定义结构体的初始化时,一定要保证结构体中的所有成员都有初始值,如果我们对结构体的某个成员变量赋初始值,那么生成的初始化器可以不用给该成员变量作为参数传值。
二、类
类和结构体基本类似,主要有以下不同点:
- 类有继承的特性,而结构体没有
- 类型转换使您能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对一个类实例有多个引用
1.类的初始化
和结构体不同,编译器默认不会为类自动提供成员初始化器。所以,如果你的类里面没有为成员变量提供初始值,那么你必须为类提供一个指定的初始化器,保证你创建的类中所有的成员都有初始值
class FYPerson {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
我们也可以为类创建一个便捷初始化器,便捷初始化器必须从相同的类里调用另一个初始化器。
class FYPerson {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
convenience init() {
self.init(age: 18, name:"Kody")
}
}
在swift中,构造指定初始化器和便捷初始化器有一系列的规则:
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
可失败初始化器: 我们在对类进行初始化时,有可能会遇到外部参数输入不合法,或者没有满足可以初始化的条件,最终导致初始化失败的情况出现。因此,我们可以使用可失败初始化器,当出现初始化失败的情况时,我们允许类返回nil值,用来表示类初始化失败。可失败初始化器的定义如下:
class FYPerson {
var age: Int
var name: String
init?(age: Int, name: String) {
if age < 18 { return nil }
self.age = age
self.name = name
}
}
var p = FYPerson(age: 16, name: "FY") //将会返回一个nil
必要初始化器: 我们可以在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器。
如上代码所示,继承了FYPerson类的FYMan类,如果没有实现init(age:, name:)初始化器,那么编译器就会报错。
三、值类型和引用类型
Swift里面的类型分为两种:值类型和引用类型。
- 值类型:每个实例都保留了一分独有的数据拷贝,一般以结构体(struct)、枚举(enum)或者 元组(tuple)的形式出现。
- 引用类型:每个实例共享同一份数据来源,一般以类(class)的形式出现。
1.值类型和引用类型的区别
值类型的变量在赋值时是深拷贝,当一个实例变量都会有一个数据副本。如果把一个struct类型的实例变量赋值给另外一个变量时,当修改这个实例变量的属性值时,不会影响另外一个实例变量的值。
通过查看变量p的内存结构,我们发现struct类型的实例变量存储的是值。当p赋值给p1,变量p1得到的是变量p的拷贝,修改p1变量的age属性,p变量的age属性值保持不变。
引用类型的变量不会直接存储具体的实例对象。
我们查看p和p1的内存结构,可以看见变量p和p1存储的是通一个具体实例的内存地址。
我们再次查看p和p1变量对应的地址的值,发现当p1的age值改变以后,p对应的age值也发生了改变。
其实引用类型就相当于在线的Excel,当我们把这个链接共享给别人的时候,别人的修改我们是能够看到的;值类型就相当于本地的Excel,当我们把本地的Excel传递给别人的时候,就相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的。
四、类和结构体的存储位置
引用类型和值类型中还有一个不同的地方就是存储位置不同,值类型一般存储在栈(stack)上,而引用类型一般存储在堆(heap)上。我们通过class和struct的存储位置来做一个对比。
使用
frame varibale -L xxx获取内存地址,然后通过cat address命令可以得到实例对象在内存的位置。
-
对于类的内存分配
- 在栈上开辟 8 字节内存空间存储
t变量,t变量中存储LGTeacher的地址 - 在堆上,会寻找合适的内存区域,开辟内存,存储
LGTeacher的实例对象 - 函数结束时,会销毁栈上的指针,查找并回收堆上的示例变量。
- 在栈上开辟 8 字节内存空间存储
-
对于结构体的内存分配
在栈上直接分配结构体内存大小的空间存储结构体的值
四、如何使用结构体和类
结构体和类的存储位置不同,结构体存储在栈(stack)上,而类存储在堆(heap)上,因此在创建和销毁上,类消耗的时间和内存会比较多,同时因为类是引用类型,在赋值时会有引用计数,这样也会造成消耗成本。因此,创建结构体会比创建类所消耗的时间少,使用的内存也会比较少。
当我们想要建立一个新的类型的时候,怎么决定用值类型还是引用类型呢?当你使用 Cocoa 框架的时候,很多 API 都要通过 NSObject 的子类使用,所以这时候必须要用到引用类型 class。在其他情况下,有下面几个准则:
- 什么时候该用值类型:
- 要用==运算符来比较实例的数据时
- 你希望那个实例的拷贝能保持独立的状态时
- 数据会被多个线程使用时
- 什么时候该用引用类型(class):
- 要用==运算符来比较实例身份的时候
- 你希望有创建一个共享的、可变对象的时候
五、类的生命周期
Swift的编译过程
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
- OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机器码)
- Swift则是通过Swift编译器编译成IR,然后在生成可执行文件。
Swift编译的相关命令
// 分析输出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文件
我们先在工程里面输入实例代码
class ClassTeacher{
var age: Int = 10
var name: String = "FY"
}
var t = ClassTeacher()
然后我们在工程里面输入生成SIL文件命令
swiftc -emit-sil ${SRCROOT}/SwiftTest/main.swift > ./main.sil && open main.sil
在工程里面执行就会生成SIL文件
生成的SIL文件如下:
解析SIL文件
现在我们根据上面的SIL文件具体分析一下SIL中间语言。因为代码比较多,我这边针对main函数来进行解析。
首先是
class LGTeacher {
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
其中@_hasStorage和@_hasInitialValue这两个是标识符,说明LGTeacher类里面的age和name是一个拥有了初始值的存储属性。LGTeacher类里面还有deinit和init方法。
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s4main1tAA9LGTeacherCvp // id: %2
%3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher // user: %7
%4 = metatype $@thick LGTeacher.Type // user: %6
// function_ref LGTeacher.__allocating_init()
%5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %7
store %6 to %3 : $*LGTeacher // id: %7
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
接下来我们看@main函数,这个函数是一个入口函数。
像%0、 %1、%2是虚拟寄存器,一旦赋值就不可以更改,所以值会一直累加。
我们接下来看alloc_global @$s4main1tAA9LGTeacherCvp,分配一个全局变量。而这个变量名s4main1tAA9LGTeacherCvp是经过混写的变量名。我们可以通过xcrun swift-demangle命令可以得到实际名称main.t: main. LGTeacher
%3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher 这个命令是把这个全局变量的内 存地址给保存下来。
%4 = metatype $@thick LGTeacher.Type这个命令是获取LGTeacher.Type的元类型。
%5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher这个命令是获取LGTeacher.__allocating_init()这个函数的指针地址。
%6 = apply %5(%4) : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher 这个命令是调用了LGTeacher.__allocating_init()函数,以LGTeacher.Type的元类型作为参数,然后把函数返回值存储到%6的寄存器中,也就是创建了LGTeacher类的实例对象。
store %6 to %3 : $*LGTeacher这个命令是把实例对象的内存地址存到了全局变量的地址中。
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
这两个命令是在构建一个Int32类型的变量,并赋值0。在Swift中Int类型是一个结构体。
return %9 : $Int32 这个是return 0,也就是在OC中main函数里面经常看到的结束标志。
接下来我们分析一下LGTeacher类的构建函数__allocating_init()
// LGTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher {
// %0 "$metatype"
bb0(%0 : $@thick LGTeacher.Type):
%1 = alloc_ref $LGTeacher // user: %3
// function_ref LGTeacher.init()
%2 = function_ref @$s4main9LGTeacherCACycfc : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %4
return %3 : $LGTeacher // id: %4
} // end sil function '$s4main9LGTeacherCACycfC'
首先,我们要先获取LGTeacher的元类型metatype,这个元类型可以理解为OC中的isa指针。
接下来我们要alloc_ref $LGTeacher,这个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 optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).
这个函数在Swift中通过调用swift_allocObject来申请内存,而在OC中则使用+allocWithZone:。
最后通过调用LGTeacher.init()方法,生成一个实例对象,并作为返回值返回出来。
分析类实例变量创建过程。
首先我们先创建一个纯Swift类
class LGTeacher {
var age: Int = 18
var name: String = "FUYU"
}
var t = LGTeacher()
运行起来后我们先把断点打在__allocating_init函数上
深入到
__allocating_init函数里面我们会发现调用了swift_allocObject和init两个方法。
我们再把这个
LGTeacher类修改成继承NSObject类
class LGTeacher: NSObject {
var age: Int = 18
var name: String = "FUYU"
}
var t = LGTeacher()
进入到__allocating_init函数里面我们会发现这次调用了OC里面的objc_allocWithZone和objc_msgSend方法。
接下来我们从Swift源码里面去寻找实例变量是怎么创建的。
首先,我们先去从HeapObject.cpp 文件中搜索
swift_allocObject 函数,可以看到内部调用了 swift_slowAlloc 函数
进入
swift_slowAlloc函数我们可以看到调用了malloc
由此可以得出swift对象创建时经过如下的过程分配内存的
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc
从刚才的对象创建分析过程中,我们可以看到Swift的对象内存结构是HeapObject。而在HeapObject中,我们可以看到有两个属性,一个是metadata,一个是refCounts。默认占了16个字节大小。
Swift类结构分析。
在OC中类的只有一个isa指针,但是在swift中有 metadata和refCounts两个属性
- metadata结构分析
通过查看
HeapObject源码,我们知道metadata是HeapMetadata类型,而这是TargetHeapMetadata定义的别名。我们接下来查看
TargetHeapMetadata的源码,通过TargetHeapMetadata的初始化代码我们可以知道,如果是一个纯Swift类,那么会传入一个MetadataKind类型的kind参数。而如果是OC交互的类,则传入的是isa。我们继续看一下
MetadataKind类型的代码,发现它是一个uint32_t类型在Swift中,
MetadataKind类型有多个定义,如下所示 - 类的metadata结构猜想
我们从
TargetHeapMetadata类的源码中可以知道,TargetHeapMetadata继承自TargetMetadata,因此我们可以知道MetaData最终继承自TargetMetadata。我们从TargetMetadata源码中去寻找TargetMetadata的创建函数。函数如下:通过这个函数我们可以看到,通过Kind属性的不同类型,
TargetMetadata会转成其它类型的Metadata。因此TargetMetadata是所有类型的元类型的基类。
通过以上函数方法,我们可以知道,当kind是Class类型时,TargetMetadata被转换成TargetClassMetadata类,因此,我们可以通过TargetClassMetadata类去寻找类的MetaData结构。
查看TargetClassMetadata类的源码,我们看到这个类继承了TargetAnyClassMetadata类,而TargetAnyClassMetadata又继承了TargetHeapMetadata类。
通过源码分析,我们可以得到类的
MetaData属性的继承链。即TargetClassMetadata -> TargetAnyClassMetadata -> TargetHeapMetadata -> TargetMetadata
同时,我们通过查看这些类的属性我们可以得到Swift类的MetaData的数据结构
struct Metadata{
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
- 验证类的数据结构
上面分析得到类的实例对象结构是
HeapObject有两个属性: 一个是Metadata,一个是RefCount
所以我们自定义一个结构体用来将我们的实例对象强转为该类型,如果转换成功则说明分析正确。代码如下:
转换结果成功,说明我们的分析正确。
接下来我们验证metadata的数据结构是否正确
从以上结果显示,类的数据结构正确。