对于上篇我们讲了类与结构体的相同和区别,这篇在开始我们再讲一个结构体和类的区别。
一、mutating @intout 输入输出参数 --> 方法异变
何为方法异变?
例如:
struct Teacher {
var name: String
var age: Int
func changeName(name strName: String) {
name = strName
}
}
我们知道上面的 Struct 是值类型,但是我们在 changeName方法中去修改了 name 变量,编译时会提示self 是 immutable 的,不允许修改。
若我们在实际的开发中需要修改这个值咋办呢?
swift 给我们提供了 mutating 属性。我们只需要把函数前加上 mutating 关键字就行了
mutating func changeName(name strName: String) {
self.name = strName
}
为啥添加了 mutating 就可以了呢? 我们进一步通过 SIL(swiftc xx.swift -emit-silgen 指令)来看下编译器为我们做了啥操作!
例如:
struct Teacher {
var name: String
var age: Int
func getAge() {
let temp = self.age
print("Age \(temp)")
}
mutating func changeName(name strName: String) {
self.name = strName
}
init(_ name: String, _ age: Int) {
self.name = name
self.age = age
}
}
通过SIL 后,我们在文件中找到
这两个方法的区别:在加了 mutating 关键字后,默认传参中Teacher(也即self) 前加了 @inout 关键字。 通过后续的流程观察,没加 mutating 关键字时,self = Teacher,加了后 变成 self = *Teacher (即指针传递)。
固我们可以得出这样的结论:加了mutating 关键字后,函数里 self 被标记成 inout 参数,也即输入输出参数, 这样无论函数里如何改变,都会影响外部依赖类型的一切。
输入输出参数:
如果我们想要一个函数能修改一个形式参数的值(Swift函数传参默认都是形参),同时需要在函数结束后,这个修改依然生效,那么我们就应该把 形式参数定义成 输入输出参数(和C语言类比就是指针传递)。在形式参数定义开始的时候在前边添加一个 inout 关键字即可满足要求
例如
二、方法调度
在OC 语言中 我们的方法是通过 objc_mgSend 消息转发来实现的
在通过跑真机查看汇编代码时是 arm64 的汇编。
函数表调度
例如:
class Teacher {
func teach () {
print("teach")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Teacher()
t.teach()
}
}
汇编后显示如下:
在这个里面我们要熟悉几个指令
mov x1, x0 将寄存器 x0 的值复制到寄存器 x1中
str x0, [x0, x8] 将寄存器 x0 中的值保存到栈内存 [x0,x8] 处
ldr x0, [x1, x2] 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该地址的值放入寄存器 x0 中.
b: (branch)跳转到某地址(无返回)
blr: 跳转到某地址(有返回)
通过上图,我们看到在(1)处是__allocating_init() 创建对象方法,在(3)处时 swift_release 函数,结合 汇编的指令,我们猜测 (2) 处就是 Teacher() 函数的入口。 如何验证呢,我们这在(2)处打个断点,然后 按住control 键,并点击Xcode上的 setp into 按钮即进入 如下流程
可以看到 正是我们需要的teach()函数。
我们再增加几个函数再来看下汇编代码
class Teacher {
func teach () {
print("teach")
}
func teach1 () {
print("teach")
}
func teach2 () {
print("teach")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Teacher()
t.teach()
t.teach1()
t.teach2()
}
}
通过汇编我们看到 在 __allocating_init() 和 swift_release 中间是 出现 3处 blr x8 指令。 固验证了blr x8 就是执行函数的指令,同时 看到 ldr 指令后面 +0x50 +0x58 +0x60 表明了函数是顺序放置的, 我们在 ldr x8, [x8, #0x50] 加入断点, 并LLDB 输入 register read x8
通过输出,我们看到显示了 metadata for Person.Teacher 术语。 再下一步
由上流程 我们得知 teach() 函数就是放在 metadata 地址后面的。
固可以得出 Swift 中函数调用 就是先找到 metadata 然后在其地址后加上偏移量的到函数地址,然后进行执行的。
我们再执行 SIL 来看下
在这里 我们看到 vtable 即所谓的函数表, sil_vtable 就是每个类维护的函数表。
在 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
}
在这个结构体中,我们重点看 typeDescriptor , 不管 Class、Struct、Enum都有Descriptor, 我们以Class的 TageClassDescriptor来看
同时 TageClassDescriptor 还有个别名 ClassDescriptor
并通过整理得到这样的结构体
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
}
同时 我们在 GenMeta.cpp 文件中 看到 ClassContextDescriptorBuilder 这个函数,其实这个函数就是 Descriptor的创建者。
在ClassContextDescriptorBuilder 中我们看到了 这样的函数
首先调用了 父类方法,super::layout(); 我们继续找到父类
在父类 layout 中
我们看到有 addName、addReflectionFieldDescriptor、maybeAddMetadataInitialization 等 ,我们可以得知这是创建 struct TargetClassDescriptor 里的信息
在ClassContextDescriptorBuilder 的layout() 中,我们还看到上面提到的 vtable -> addVTable();
在这个 函数里我们看到了添加offset 后,又添加了size 和 method,由此我们看出,TargetClassDescriptor 在offset 后就是方法列表V-Table。
下面我们通过 MacO文件来验证证明。
Mahco: Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常⻅的 .o,.a .dylib Framework,dyld .dsym。
MacO文件格式:
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
- Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
| 字段 | 描述 |
|---|---|
| LC_SEGMENT_64 | 将文件中(32位或64位)的段映射到进程地 址空间中 |
| LC_DYLD_INFO_ONLY | 动态链接相关信息 |
| LC_SYMTAB | 符号地址 |
| LC_DYSYMTAB | 动态符号表地址 |
| LC_LOAD_DYLINKER | dyld加载 |
| LC_UUID | 文件的UUID |
| LC_VERSION_MIN_MACOSX | 支持最低的操作系统版本 |
| LC_SOURCE_VERSION | 源代码版本 |
| LC_MAIN | 设置程序主线程的入口地址和栈大小 |
| LC_LOAD_DYLIB | 依赖库的路径,包含三方库 |
| LC_FUNCTION_STARTS | 函数起始地址表 |
| LC_CODE_SIGNATURE | 代码签名 |
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
在我们的Demo的MacO文件 我们知道 OC的类在 __objc_classlist 分类里
而我们Swift的类是放在__swift5_types里
在这个表里,
1和2相加其实就是 Teacher的 Descriptor。
0xFFFFFB8C + 0X0000BB7C = 0x10000B708
我们看下MacO的虚拟地址。
我们把虚拟地址减除后的
0x10000B708 - 0x100000000 = 0xB708
然后来到 __const 表里
这个
50 00 00 80 开始就是Descriptor 的类容。
然后我们根据TargetClassDescriptor 类 算出我们需要偏移12 个4字节,所有得到 B730这个地址
我们先运行我们的Demo
并在LLDB 输入 image list 得到 程序运行起来的 基地址 0x0000000104944000
然后 我们加上上面偏移计算的 B730 地址
0x0000000104944000 + 0xB730 = 0x10494F730 (函数运行地址)
顾 10 00 00 00这个地址就是第一个 teach 函数结构 地址信息。
我们再看下 swift 源码中 函数的结构体信息
这个里 flag 是4字节, 然后就是 offset 信息。 由于 0xB730 在 10 00 00 00 Flags执行后,来到了0xB740 偏移地址。固 teach 函数的地址信息为
0x10494F740 + 0xFFFFABB4 = 0x20494A2F4
0x20494A2F4 - 0x100000000(虚拟地址) = 0x10494A2F4
我们通过运行Demo 并在 LLDB 命令里打印出运行起来的地址
看~~~ 和我们计算的一模一样,固得到结论上述的寻找过程是正确的,同时也得到我们Swift的函数就是在V-table 函数表的结论。这种方法也叫函数表派发
我们再来看下 结构体 方法的调用
struct Teacher {
func teach () {
print("teach")
}
func teach1 () {
print("teach")
}
func teach2 () {
print("teach")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Teacher()
t.teach()
t.teach1()
t.teach2()
}
}
在汇编语言中
我们看到时直接静态地址调用,这种也是叫
静态派发
下面我在 extension 中加入函数
extension Teacher {
func teach3 () {
print("teach3")
}
}
通过汇编能看出我们新加的teach3()也被优化成静态调用,也即
静态派发
继续我们把 teach() 改成final修饰
final func teach () {
print("teach")
}
通过汇编能看出被
final 修饰的 teach()也被优化成静态调用,也即静态派发
在实际开发中,若我们不允许我们的函数被重写,就可以加上 final 给改成静态调用。
对于 dynamic 关键字,在Swift里,其实就是为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
如何理解动态性。看图
我们在 teach()前加了
dynamic 在 teach3() 前加了@_dynamicReplacement(for: teach) ,然后运行后,看到调用了teach()函数 其实走的 是teach3()的函数体,这就是Swift函数的动态性。
在实际开发中 我们 使用比较多的是 @objc + dynamic模式,
@objc 标签就是能将Swift函数暴露给Objc运行时,依旧是函数表派发 但是有个前提是 类集成自NSObject类。
若@objc + dynamic 不是继承自 NSObject类,那么就不能供Objc使用,但是可以使用 Runtime的 Method-swizzing来调用。
方法调度方式总结:
| 类型 | 调度方式 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| 类 | 函数表派发 | 静态派发 |
| NSObject子类 | 函数表派发 | 静态派发 |