在开篇之前,先了解一下Mach-O文件:Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格式, 类似于 windows 上的 PE格式 (Portable Executable ), linux 上的 elf 格式 (Executableand Linking Format) 。常见的 .o,.a .dylib Framework,dyld .dsym。
Mach-O文件的格式:
-
首先是文件头,表明该文件是
Mach-O格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
-
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等
-
Data 区主要就是负责代码和数据记录的。
Mach-O是以Segment这种结构来组织数据的,一个
Segment可以包含 0 个或多个Section。根据Segment是映射的哪一个LoadCommand,Segment中section就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据
Segment做内存映射的。
1、异变方法
通过初步的了解,已经对类class和结构体struct有了初步的了解,类和结构体都可以定义方法,不同的是,由于struct是值类型所以自身的属性不能被实例方法修改,通过代码来看一下
可以看到,如果要修改x或y的值就会有告警信息,提示:self是不可变的,那这个时候方法的前面加上 mutating关键字就可以解决这个问题了,这样就正常的输出结果
接下来通过SIL来对比一下,添加了mutating关键字和不添加的区别
添加mutating的代码
所以异变方法的本质就是:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法内部发生什么,都会影响外部依赖类型的一切。
我们知道,swift中的形式参数默认是用let,也就是默认是常量,是不可变的,那么我们可以通过添加 inout关键字,就可以定义一个输入输出形式参数
可以看到deltaX值是不可变的,而deltaY的值是可变的
2、方法调度
2.1、函数表调度
在类中,函数的调度方式是基于函数表的调度V-Table,通过汇编代码验证一下,首先了解一下ram64常见的几个汇编指令
-
mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:
mov x1, x0 //将寄存器 x0 的值复值到 寄存器x1中
-
add:将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:mov x0, x1, x2 //将寄存器 x1 和 x2 的值相加后保存到 寄存器 x0 中
-
sub:将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:sub x0, x1, x2 //将寄存器 x1 和 x2 的值相减后保存到 寄存器 x0 中
-
and:将某一寄存器的值和另一寄存器的值 按位与运算 并将结果保存到另一寄存器中,如:
and x0, x0, #0x1 //将寄存器 x0 的值和常量 1 按位与运算后保存到寄存器 x0 中
-
orr:将某一寄存器的值和另一寄存器的值 按位或运算 并将结果保存到另一寄存器中,如:
orr x0, x0, #0x1 //将寄存器 x0 的值和常量 1 按位或运算后保存到寄存器 x0 中
-
str:将寄存器中的值写入到内存中,如:str x0, [x0, x8] //将寄存器 x0 中的值保存到栈内存 [x0 + x8]处
-
ldr:将内存中的值读取到寄存器中,如:ldr x0, [x1, x2] //将寄存器 x1 和 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中
-
cbz:和 0 比较,如果结果为零就转移(只能跳到后面的指令) -
cbnz:和非 0 比较,如果结果非零就转移(只能跳到后面的指令) -
cmp:比较指令 -
b:(branch)跳转到某地址(无返回) -
bl:跳转到某地址(有返回) -
ret:子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
再从SIL的角度去看一下,在SIL文件中的sil_vtable就是对应的这个类的函数表:
从源码的角度看一下,首先需要了解class在源码中的数据结构:
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
}
要研究V-Table,我们需要看一下 typeDescriptor的数据结构,打开源码,来到GenMeta.cpp文件,找到 ClassContextDescriptorBuilder类,这个类中有个layout的函数
还有它的父类的layout函数
据此可知道typeDescriptor的数据结构
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
var size: UInt32
var methDescriptor<TargetMethodDescriptor>
}
2.1.1 mach-o中验证数据结构
打开Mach-O文件
来到Section64(__TEXT,__swift5_types),
用Data LO加上 pFile 得到 0x10000B764 这个地址 再减去0x10000000基地址得到0x00000B764
接下来来到Section64(__TEXT,__const),找到0000B760这一列,找到对应的B764对应的首地址
再向后偏移12个4字节来到
对应的偏移地址就是0x0000B798,再向后偏移4字节,是Flags得到0xB79C
再加上方法对应的首地址 0xFFFFC150
再加上程序运行的基地址,程序运行基地址获取方式 0x0000000100c2c000
最后得到
再减0x10000000就是方法的地址 0x0000000100c338ec
2.2、静态调用
直接派发是最快的,不止是因为需要调用的指令集少,并且编译器还有很大的优化空间(比如:函数内联)。直接派发也称为静态调用。
然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。
struct XLPerson {
func eat() {
print("eat")
}
func play() {
print("play")
}
func stady() {
print("stady")
}
func test() {
print("test")
}
}let p = XLPerson()
p.eat()
p.play()
p.stady()
p.test()
在结构体中调用方法就是静态调用,汇编代码可以看出来,静态调用直接获取的方法的内存地址调用的
方法调度方式总结:
3、影响函数调用的方式
-
final: 添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见。 -
dynamic: 函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
-
@objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。 -
@objc + dynamic: 消息派发的方式