阅读须知:
- 理解
值类型
、引用类型
两个概念;
- 了解
堆
、栈
、(lldb命令调试中的po
、x/8g
)、int8_t
、uint32_t
;- 3.在 Swift 的标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。比如
Bool、Int、Double、 String、Array、Dictionary
等常见类型都是结构体。
........................................................
目录:
- 一、类class与结构体struct的异同;
- 二、类的初始化器;
- a. 成员的默认初始化
- b. 指定初始化器&便捷初始化器
- c. 可失败初始化器
- d. 必要初始化器
- 三、类的生命周期;
- a. 了解Swift的编译过程
- b. SIL文件分析
- c. 汇编分析 类实例对象创建过程
- d. Swfit 源码 分析
- 四、类的结构探索
........................................................
一、类class与结构体struct的异同
- 主要相同点
1.定义存储值的属性
2.定义方法
3.定义下标以使用下标语法提供对其值的访问
4.定义初始化器
5.使用extension来拓展功能
6.遵循协议来提供某种功能
- 主要不同点
1.类有继承的特性,结构体没有
2.类型转换使您能够在运行时检查和解释类实例的类型
3.类有析构函数用来释放其分配的资源
4.引用计数允许对一个类实例有多个引用
对于类与结构体我们需要区分:
类class:是引用类型 | 结构体struct:是值类型 |
---|---|
引用类型 的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址 的引用 |
值类型 存储的就是具体的实例(或者说是具体的值),struct就是一个比较典型的值类型 |
使用lldb命令------ p和po、x8/g 证明:
po
只会输出对应的值,p
会返回值的类型以及命令结果的引用名。
x/8g
:读取内存中的值(8g
:8字节格式输出)
如下:我们创建一个名为YGTeacher的类,创建它的一个实例对象teacher,再给teacher_1赋值teacher;断点调试结果如图1
class YGTeacher {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var teacher = YGTeacher(age: 22, name: "XiaoMa")
var teacher_1 = teacher
我们可以看到teacher 和teacher_1 所引用的地址均为0x6000002dc120,
x8/g 输出此地址,得到的是一个YGTeacher的实例对象的值。
所以说明引用类型是对当前存储具体实例的内存地址
的引用。
当我们对结构体进行性同样的操作时候:如下
我们把上述示例代码中的 class 改成 struct其它代码不动,这样我们就得到了一个 YGTeacher的结构体;
struct YGTeacher {····}
通过po调试teacher、和teacher_1我们可以很直观的看到,输出的是我们内存当中存放的值
(lldb) po teacher
▿ YGTeacher
- age : 22
- name : "XiaoMa"
(lldb) po teacher_1
▿ YGTeacher
- age : 22
- name : "XiaoMa"
理解:
引用类型---就像在线的Excel,当我们把这个链接共享给别人的时候,别人的修改我们能够看到。
(因为我们的链接引用的都是存储在腾讯服务器上的的同一份文档,teacher和teacher_1引用的也全都是同一块内存上的值)
值类型---就相当于本地的Excel,当我们把本地的Excel传递给别人的时候,就相当于重新复制了一份给别人,至于他们对内容的修改我们是无法感知的。
(引用类型赋值,我理解成指针拷贝,值类型赋值的时候,指针和地址或者存储的值 都拷贝,不知道这种理解是否正确)
另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:
一般情况,值类型存储在栈
上,引用类型存储在堆
上;
有关内存区域的基本概念,看我这篇文章《Swift-内存区域的基本概念和认知》
lldb调试
“frame variable -L 变量名” 查看当前变量的地址
struct YGPerson {
var age = 30
var name = "XM"
}
func test(){
let per = YGPerson()
print("%@ = end",per);
}
test()
打印地址如上图所示:
0x00007ffee0221c00: 当前结构体的内存地址
0x00007ffee0221c00: age的内存地址
0x00007ffee0221c08: name的内存地址
其中结构体的第一个首地址就是age变量的地址,跳过了8个字节后,指向的是字符串变量name。都存放在
栈区
。
与数据结构中的栈有所不同,我们通过移动栈的指针来去分配和销毁我们的内存。
当编译器到test方法后,对
let per = YGPerson()(结构体)
操作,首先我们当前的栈指针
来在我们当前的内存
当中分配24字节的内存大小(age是int占8个字节,name是string,占16个字节,总共24个字节);
栈指针往下移动
24个字节的内存空间
(简称24b),用这24b用来存放我们当前的值类型
。这个值类型就把它当前的value拷贝到
我们当前分配好的值类型(也就是分配好的栈的内存空间上--- 24b);
当text方法作用域执行完成之后,当前的栈指针就会移动,移动完成之后就会销毁我们当前的栈内存。(回到栈顶,还原内存)
class YGPerson {}
当 struct YGPerson {} 换成 class YGPerson {}后,
我们的结构体变成了类
,再次调用 func test()函数方法后,
发现 per 实例的内存区域 在堆区
。
整个过程是,先在
栈区
上开辟8个字节存储当前的指针
,
然后再堆区
上查找合适的内存区域存储value
。
销毁:查找并且将内存块重新插入到堆空间当中。
- 对于堆区来说,始终要有
查找
的过程。同时再销毁栈上的指针。所以在分配内存的时间
上,类和结构体有着明显的区别。
总结:对于只有 不可变 变量的class,优先使用结构体。
结构体在内存的分配和回收上,需要的时间短;
class YGPerson {
let value :Int
let age :Int
}
//--------改成结构体-------//
struct YGPerson {
let value :Int
let age :Int
init(_ val :Int ,_ age :Int) {
self.value = val
self.age = age
}
}
二、类的初始化器
a. 成员的默认初始化
注意:当前
类
的编译器默认 不会 自动提供成员
初始化器;结构体
的编译器 会 给自己的成员
提供默认的初始化方法(前提是我们自己没有指定初始化器)
这一点其实也可以添加到类与结构体的不同点里。
我们创建一个YGPerson的结构体,和一个YGClass类
//结构体
struct YGPerson {
var age: Int
var name: String
}
//类
class YGClass {
var age:Int = 0
var name:String = ""
}
两者在不写构造器的情况下均可以编译项目
当时当我们实例化一个变量的时候,结构体YGPerson的构造器中拥有初始化成员的方法:如图
|
|
---|---|
有age 、name 的初始化方法 | 没有 |
b. 指定初始化器&便捷初始化器
Swift中创建类和结构体的实例时必须为所有的 存储属性 设置一个合适的初始值。
所以类YGPeople必须提供对应的 指定初始化器,同时我们也可以为当前的类提供 便捷初始化器。
(注意:便捷初始化器必须从相同的类里调用另一个初始化器。
)
如下所示
//创建一个YGPeople类
class YGPeople {
var age:Int
var name:String
//指定初始化器
init(_ age:Int, _ name:String) {
self.age = age
self.name = name
}
//convenience 便捷初始化器
convenience init(_ age:Int) {
//便捷初始化器必须从相同的在类里调用另一个初始化器
self.init(age,"XM")
self.age = age
}
convenience init(name:String) {
self.init(18,name)//便捷初始化器必须从相同的在类里调用另一个初始化器
self.name = name;
}
}
如果我们在便捷初始化器中,不先调用另一个初始化器,系统就会提示错误 |
---|
|
总结:(指定初始化器&便捷初始化器)
- 指定初始化器 必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器 必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器设置的新值将被父类的初始化器所覆盖。
- 便捷初始化器 必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构造初始化器赋予的新值将被自己类中其它指定初始化器所覆盖
- 初始化器 在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。
举个例子说明总结中的第一条(子类继承父类)
//子类(YGStudent)必须保证在向上委托给父类(YGPeople)初始化器之前,
//其所在类引入的所有属性都要初始化完成
class YGStudent:YGPeople {
var score:String
//初始化保证全部成员变量访问安全
init(_ score:String) {
self.score = score
//父类初始化
super.init(18, "XMG")
}
}
c.可失败初始化器
当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种Swift中可失败初始化器写 return nill 语句,来表明可失败初始化器在任何种情况下会触发初始化失败。代码示例如下
class YGAdultPerson {
var age:Int
var name:String
init?(age: Int, name: String) {
//如果age小于18,就不是一个成年人(返回nil)
if age < 18 {return nil}
self.age = age
self.name = name
}
convenience init ?() {
self.init(age: 18, name: "XMG")
}
}
d.必要初始化器
在
类
的初始化器前添加required
修饰符来表明所有该类的子类
都必须实现该初始化器
|
---|
三、类的生命周期
a. 了解Swift的编译过程
iOS开发语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
不同的是:
OC通过 clang 编译器,编译成IR,然后再生成可执行文件 .0(也就是我们的机器码)
Swift则是通过Swift编译器编译成IR,然后再生成可执行文件。
下面是Swift编译过程的图解:
简单总结:swift的代码
经过dump命令解析成抽象语法树AST
再经过语义分析
降级成SIL
(SIL是Swift的中间语言<中间代码>)一种是原生的SIL
Raw DIL,一种是优化的SIL
SIL Opt Canonical SIL。优化完成的SIL经过一系列编译后由LLVM
降级成IR
,后经过我们的后端代码编译成机器代码
(x86等)
这里给出了具体流程中每一步对应的终端命令行:
//分析输出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
b. SIL文件分析
代码如下所示:
class YGTeacher {
var age :Int = 10
var name :String = "YGJS"
}
var yg_t = YGTeacher()
|
||
---|---|---|
《Run Script 文件设置的具体操作步骤》 | ||
《Swift SIL基本语法GitHub》 |
main.sil文件内容解示例与解读:
//YGTeacher在SIL阶段的声明
class YGTeacher {
//有一个初始化过的存储属性 age 有get、set方法
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
//标识了 @objc
@objc deinit
//默认的初始化方法init()
init()
}
@_hasStorage @_hasInitialValue var yg_t: YGTeacher { get set }
// yg_t
sil_global hidden @$s4main4yg_tAA9YGTeacherCvp : $YGTeacher
// main @main 入口函数
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
//%0、%1、%3、%4.....SIL中的寄存器(虚拟的寄存器)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
//alloc_global -- 分配一个全局变量 (s4main4yg_tAA9YGTeacherCvp 是混写后的名称 其实就是 yg_t)
alloc_global @$s4main4yg_tAA9YGTeacherCvp // id: %2
//global_addr 拿到全局变量s4main4yg_tAA9YGTeacherCvp(也就是yg_t)的内存地址,给到%3这个寄存器
%3 = global_addr @$s4main4yg_tAA9YGTeacherCvp : $*YGTeacher // user: %7
//拿到YGTeacher.Type的元类型
%4 = metatype $@thick YGTeacher.Type // user: %6
//function_ref 引用一个函数。 引用__allocating_init()函数,即拿到这个函数的指针地址,并给到%5这个寄存器中
// function_ref YGTeacher.__allocating_init()
%5 = function_ref @$s4main9YGTeacherCACycfC : $@convention(method) (@thick YGTeacher.Type) -> @owned YGTeacher // user: %6
//使用%5寄存的指针地址对应的函数,函数的参数是%4也就是YGTeacher的元类型。并把结果的返回值给到%6
%6 = apply %5(%4) : $@convention(method) (@thick YGTeacher.Type) -> @owned YGTeacher // user: %7
//把%6寄存的值 存储给 %3(%3是全局变量yg_t);(也就是把实例变量的内存地址 %6 存储给这个全局变量里)
store %6 to %3 : $*YGTeacher // id: %7
//最后这三步 类似OC中 main函数结束时候的 return 0;
%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'
@ 标识符
%0 ,%1..... SIL中的虚拟寄存器 ,复制后不能更改
s4main4yg_tAA9YGTeacherCvp 混淆的变量名,在终端上通过xcrun swift-demangle s4main4yg_tAA9YGTeacherCvp 命令可以还原变量真实名称
我们如果不能理解
元类型
是什么,可以直接的参考我们的isa指针
。可以这么理解。
类的实例对象的创建 :
// %0 "$metatype"
bb0(%0 : $@thick YGTeacher.Type):
//初始化
%1 = alloc_ref $YGTeacher // user: %3
// function_ref YGTeacher.init()
%2 = function_ref @$s4main9YGTeacherCACycfc : $@convention(method) (@owned YGTeacher) -> @owned YGTeacher // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned YGTeacher) -> @owned YGTeacher // user: %4
return %3 : $YGTeacher // id: %4
} // end sil function '$s4main9YGTeacherCACycfC'
alloc_ref
申请内存 swift 调用 swift_allocObject oc 会调用 allocWithZone
通过 汇编代码 进入到__allocating_init() 可以得到
- git上的描述:
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:).
- 大概意思就是去内存中初始化一个引用计数为1的对象,如果是Objective-C的话,增加allocWithZone的类方法
c. 汇编分析 类实例对象创建过程
-
纯 swift 类
class YGTeacher {
var age :Int = 10
var name :String = "YGJS"
}
var yg_t = YGTeacher()
|
||
---|---|---|
断点在var yg_t = YGTeacher()处,然后选择Debug→DebugWorkflow→Always Show Disassembly |
可以看到如下
__allocating_init().处打断点,继续深入(这里没有M1的mac,得到的结果不太一样,先拿朋友的图了)
__allocating_init 内部会调用
swift_allocObject
和init
函数
-
如果继承自OC的NSObject
class YGTeacher :NSObject{
var age: Int = 10
var name: String = "YG"
}
var yg_t = YGTeacher()
同样的操作,我们可以看到
进入 __allocating_init 后可以看出 内部调用了allocWithZone
方法然后通过msgsend
发送init
消息
d. Swfit 源码 分析
Swift源码地址
搜索 HeapObject.cpp 文件 找到
swift_allocObject
函数 可以看到 内部调用了swift_slowAlloc
函数
进入swift_slowAlloc 函数 可以看到 调用了
malloc
由此可以得出 swift对象创建时经过如下的过程分配内存的(纯swift)
__allocating_init
→swift_allocObject
→_swift_allocObject_
→swift_slowAlloc
→malloc
(malloc申请堆区的内存空间,是8字节对齐的)
Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是Metadata
(HeapMetadata类型 、占8字节) ,一个是RefCount
(引用计数、占8字节) ,默认占用 16 字节大小
四、类的结构探索
在OC 中类的只有一个 isa
指针,但是在 swift 中有 HeapMetadata
和 refCounts
两个属性
Metadata 源码分析
Metadata 的继承关系及结构简述
查看 Metadata 的源码可以看到 Metadata 是 HeapMetadata
类型 这是 TargetHeapMetadata
定义的别名
在 swift 中
MetadataKind
有如下类型