对于swift而言,类和结构体都是最基础的结构,那么今天我们研究一下他们, 首先看一下他们的区别
类与结构体的相同点
- 都可以实现方法,都可以定义计算属性和存储属性,都支持属性监听。
- 都支持扩展(extension)。
- 都可以遵守协议。
- 都可以定义下标subscript以使用下标语法提供对其值的访问。
- 都要定义初始化器,指定初始化和快捷初始化器。
类与结构体的不同点
- 结构体属于值类型,类属于引用类型;
- 结构体不可以继承,可以继承;
- 值类型赋值给let var 或者函数传参的时候完,全是深拷贝;
- 结构体的方法修改属性的时候需要用@mutating修饰(枚举也需要);
- required关键字只支持Class, Class可以用static和Class 关键字修饰静态方法;Struct 只能用Static 修饰;
- 值类型在代码中使用要比引用类型使用占用内存少,读写少一步,也没有引用计数的内存占用;
- 类有析构函数用来释放其分配的资源;
- 引用计数允许对一个类实例有多个引用;
- 类型转换使您能够在运行时检查和解释类实例的类型;
类是引用类型,一个类类型的变量存储在栈中,这个变量的值是当前类实例内存地址的引用,而实例的内存地址则在堆中存储。 结构体是值类型,一个结构体类型的变量则直接存储在栈中
首先我们对内存区域来一个基本概念的认知,大家看下面这张图
栈区(stack): 局部变量和函数运行过程中的上下文, 0x00000FF 堆区(Heap): 存储所有对象 Global: 存储全局变量;常量;代码区
例如:
func test(){
//我们在函数内部声明的age变量是不是就是一个局部变量
var age: Int = 10
print(age)
}
test()
打印内存发现,address:0x00007ffeefbff418, stack address age变量是处于栈区。
Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的 Section
例如如下section
TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明
结构体中的属性,如果有引用类型属性,类的实例仍然存储到内存的堆中,栈中的该属性变量,仍然是实例内存的引用
值类型的内存分配,以及释放效率都比较高,并且处于栈中,所以比较安全,引用类型的内存堆内存分配和释放步骤较多,所以效率相比较值类型较低,所以苹果建议尽量使用结构体。
初始化器
初始化器是类和结构体分配内存后,要使用的必经之路。这里有指定初始化器和便捷初始化器两个概念。
指定初始化器和便捷初始化器(convenince)
为了让我们类的初始化时候,必须按照指定的方式,避免一些必要初始化逻辑的丢失,所以一个类最好要有一个指定初始化器,其余可作为便利初始化器,并且便利初始化器的实现里面,必须从相同类里调用一个其他的初始化器。
class LGPerson{
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")
}
}
这里我们记住: 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。 说白了,super.init()之前,自己的属性都要初始化完成。
指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。
可失败初始化器
这是为了满足,提供的参数可能存在初始化失败的情况下,来使用可失败初始化器,例如:
必要初始化器
在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器
类的生命周期
前面只是叙述了类和结构体的一些语法和特性的区别以及相同点,那么想分析本质,就得有一些渠道和方法,下面我们说说swift底层分析的三板斧,SIL,Mach-O和源码
SIL
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
- OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机器码)
- Swift则是通过Swift编译器编译成IR,然后在生成可执行文件。
下面是swift编译过程:
编译器首先对代码进行语法分析,解析成AST抽象语法树,然后语义分析,类型检查是否正确,是否安全,降级生成SIL( Swift Intermediate Language)SIL原生(SLL),优化(SIL)两种,最后降级成LLVM IR ,最终编译成为Machine Code。
swift编译一个很大的特点就是生成SIL,下面看一段简单的SIL
从字面意思就能看懂。
然后,SIL中以@作为标识符,文件中有一个@main,这个是入口函数的标识符。
我们可以看到,本main程序运行了一个LGTeacher()的实例创建,关键看__allocating_init()这个方法,经过查找这个方法的实现为:
这个方法的参数thick LGTeacher.Type源类型,我们猜测是不是可以理解为OC的isa,继续跟踪,发现主要是alloc_ref $LGTeacher这个方法,然后去查SIL的官方文档,找到alloc_ref的描述:
红框中文字说明,这个方法就是创建一个T的实例对象,并且这个对象引用计数会初始化为1,也就是我们先前的去堆申请内存空间,特殊的,如果这类标识是OC的,会走OC的初始化方式。
例如:
上面代码debug调试,观看汇编,发现__allocating_init方法,该方法执行如下俩个方法,swift_allocObject 和 LGTeacher.init(), 就是先分配内存,然后实例初始化
如果LGTeacher继承的是NSObject,那么会怎么样呢?
看到了吧,很经典的OC创建和初始化方式(objc_allocwithZone)。和SIL的说明是一致的。
这里不研究OC的流程,看纯swift的swift_allocObjec会做什么?
这里呢就是swift分析的第二把板斧-源码。
源码
查看swift c++源码。我们找到swift_allocObjec方法
它主要调用的是_swift_allocObjec
先执行swift_slowAlloc,malloc(size)开辟内存,
最后转为HeapObject结构体,返回
我们总结一下: Swift 对象内存分配:
- __allocating_init
- swift_allocObject
- swift_allocObject
- swift_slowAlloc 调 Malloc
- 返回HeapObject结构
现在我们知道,实例对象是不是就是一个HeapObject的结构体,它里面是什么呢?
我们得出一个结论:
Swift实例对象的内存结构就是HeapObject(OC就是objc_object),
HeapObject有两个属性: 一个是HeapMetadata 8字节 ,一个是RefCount 64位位域信息,默认占用16字节大小(2个8字节)。
接下来分析,HeapMetadata的内容。
通过如下源码swift源码发现,HeapMetadata就是TargetHeapMetadata的别名,继承自Metadata,
那么我们看看TargetHeapMetadata的初始化方法,
如果是OBJC-swift交互,初始化时候TargetHeapMetadata需要一个isa,
如果是纯swift,初始化时候则需要一个MetadataKind kind。
MetadataKind是一个uint32类型,所以我们可以推测,MetadataKind也是一个类似isa的东东。类型呗,kind
再看看TargetHeapMetadata的父类TargetMetadata。
我们在里面发现一个getKind(), kind判断了值类型和class类型
所有class类型都转成TargetClassMetadata
所有value类型都转成TargetValueMetadata
进入TargetClassMetadata就比较清晰了。
所以swift 类的结构,可以总结为下面的结构:
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
}
那么我们结论是什么呢 Swift类和结构体的本质就是Metadata(TargetClassMetadata或TargetValueMetadata)