类与结构体的异同点
swift里面用的比较多的是类和结构体,咱们今天来初步探索下他们。 先来对比下他们的相同点,我们直接看代码:
protocol myProtocal {
func protocalFunc()
}
class SPClass: myProtocal {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
func test(a: Int) {
print(a);
}
subscript(index: Int) -> SPClass {
get { SPClass(age: 0, name: "aaa") }
}
func protocalFunc() {
}
}
extension SPClass {
func test2() {
print("test2")
}
}
这里面定义了一个类,给类加了存储属性,初始化器,方法,下标,拓展,协议。
接着我们把类改成结构体,代码如下:
struct SPStruct: myProtocal {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
func test(a: Int) {
print(a);
}
subscript(index: Int) -> SPStruct {
get { SPStruct(age: 0, name: "aaa") }
}
func protocalFunc() {
}
}
extension SPStruct {
func test2() {
print("test2")
}
}
编译一下没有问题,说明类和结构体都具有一下主要共同点:
- 定义存储值的属性
- 定义方法
- 定义下标以使用下标语法提供对其值的访问
- 定义初始化器
- 使用 extension 来拓展功能
- 遵循协议来提供某种功能
接着我们来对比下他们的不同的地方:
- 类有继承的特性,而结构体没有
- 类型转换使您能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对一个类实例有多个引用
我们简单的做下验证,果然编译器会报错
其实类和结构体最本质的区别是: 类是引用类型,结构体是值类型
- 类类型的变量不直接存储具体的实例对象,是对当前存储的实例内存地址的引用
- 值类型的变量存储的就是具体的实例变量,具体的值
- 类类型的变量好比在线Excel, 我们能看到或者修改同一份文件
- 值类型的变量好比本地的Excel, 别人本地的Excel我们是无法感知看到的,也是无法修改的 我们看下代码来验证下:
var t1 = SPClass(age: 1, name: "name1")
var t2 = t1
t2.age = 2
var t3 = SPStruct(age: 1, name: "name1")
var t4 = t3
t4.age = 3
控制台输出:
确实我们对引用类型t1的复制t2进行修改的同时也修改了t1,而对值类型t3的复制t4修改丝毫不影响t3本身
我们在借助几个命令来看下变量的内存结构:
- po : p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果 的引用名。
- x/8g: 读取内存中的值(8g: 8字节格式输出)
- frame varibale -L xxx 查看变量的内存分布
我们看到t1和t2引用的是同一个地址,修改t2等于修改了t1
而t3和t4失不同的地址,我们看到
0x0000000100008240,0x0000000100008258相差了24个字节正好是结构体SPStruct的大小
其实,一般情况下引用类型存储在堆上,值类型出现在栈上
我对比下类和结构体的性能github.com/knguyen2708… 得出结论:结构体的内存空间更小,内存分配回收更快,所以在我们的开发过程中尽量多使用结构体。 例如示例一:
enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage]()
func makeBalloon(_ balloon: String) -> UIImage {
if let image = cache[balloon] {
return image
}
}
cache一个Key类型是String的字典,String类型是堆区储存 将Key类型改为enum, enum是栈区储存,改写成:
enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [Balloon : UIImage]()
func makeBalloon(_ balloon: Balloon) -> UIImage? {
if let image = cache[balloon] {
return image
}
return nil
}
struct Balloon: Hashable {
var color: Color
var orientation: Orientation
var tail: Tail
}
示例二
struct Attachment {
let fileURL: URL
let uuid: String
let mineType: String
}
String类型是堆区储存,修改成值类型,改写成:
struct Attachment {
let fileURL: URL
let uuid: UUID
let mineType: MineType
}
enum MineType: String {
case jpeg = "image/jpeg"
}
类的初始化器
需要注意的一点是:当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译 器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!
- 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
- 默认初始化器总是类的指定初始化器
- 类偏向于少量指定初始化器,一个类通常只有一个指定初始化器
初始化器的相互调用规则
- 指定初始化器必须从它的直系父类调用指定初始化器
- 便捷初始化器必须从相同的类里调用另一个初始化器
- 便捷初始化器最终必须调用一个指定初始化器
我们需要记住:
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性 都要初始化完成。
- 指定初始化器必须先向上托父类初始化器,然后才能为继承的属性设置新值。如 果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其 它指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例 属性的值,也不能引用 self 作为值。
类的生命周期
其实OC和swift在后端编译都是通过llvm的,如下图:
OC是通过clang编译成IR文件,而swift是通过swift编译器编译成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文件。 我们写一段代码
import Foundation
class SPClass {
var age: Int = 20
var name: String = "SP"
}
let t = SPClass()
print("end")
通过命令生成sil文件swiftc -emit-sil main.swift -o main.sil
对于类SPClass有两个有初始值的存储属性age和name还有一个init,一个带有objc标记的deinit方法
其中
s4main1tAA7SPClassCvp是混淆后的变量,可以通过xcrun swift-demangle xxx来还原
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s4main1tAA7SPClassCvp // id: %2
%3 = global_addr @$s4main1tAA7SPClassCvp : $*SPClass // user: %7
%4 = metatype $@thick SPClass.Type // user: %6
// function_ref SPClass.__allocating_init()
%5 = function_ref @$s4main7SPClassCACycfC : $@convention(method) (@thick SPClass.Type) -> @owned SPClass // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick SPClass.Type) -> @owned SPClass // user: %7
store %6 to %3 : $*SPClass // 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是主函数入口,$@convention(c)代表这个是c函数,接受两个参数,参数一个是Int32类型,一个是UnsafeMutablePointer类型,返回Int32类型- %0,%1等是寄存器,但是是虚拟寄存器,不是真的寄存器,真的到执行是会分配到真的寄存器
alloc_global @$s4main1tAA7SPClassCvp创建一个SPClass的全局变量赋值给%2global_addr获取全局变量的地址赋值给%3metatype获取%3的原类型赋值给%4function_ref拿到SPClass.__allocating_init的函数地址赋值给%5apply调用SPClass.__allocating_init函数讲结果赋值给%6store %6 to %3将上述结果赋值给全局变量%3- 后面是构造结构体Int32(0)并作为main函数的返回值返回
对象的初始化流程
通过sil分析我们得到了swfit在给对象分配内存会调用__allocating_init函数,那么后面的流程是怎么样的,我们暂时不得而知,于是我们可以通过汇编代码来分析真个对象分配内存的过程:
于是我们得到了这样的流程:
- __allocating_init
- swift_allocObject
- swift_allocObject
- swift_slowAlloc
- Malloc流程
总结
- 我们初步探索了类和结构体的异同点,分析出他们的本质区别是类是引用类型,结构体是值类型,值类型在分配内存和执行效率更有,我们开发过程中推荐更多的使用值类型
- 类的初始化器的一些规则的制定主要是为了保住能初始化所有的存储属性
- 我们探索了swift的编译过程,分析了其中很重要的sil文件,通过案例分析sil文件里面的一些执行流程找到了__allocating_init的方法
- 最后我们结合汇编分析出了整个swift对象分配内空间的流程