一、类的方法调度
- 我们先添加如下代码
注意:这里我们用真机(
arm64)来调试
- 断点到汇编
ldr x8, [x0]:将寄存器 x0 的值放入寄存器x8中,这里x0其实是Teacher实例的首地址,x8中存的就是Teacher第一个8字节地址。
ldr x8, [x8, #0x50] ldr x9, [x9, #0x58] ldr x9, [x9, #0x60]:这里都是在地址偏移(其实是取三个teach方法的地址)
0x50 0x58 0x60:这在内存中是连续的空间,每个都相差8字节,其实相差的是函数指针的大小。
x8存的是什么?
这里我们通过register read x8可以发现,x8中其实是Teacher实例的metadata。
也就是说,teach函数的调用过程是这样的:找到metadata > 确定函数地址(metadata + 偏移量) > 执行函数。
- 分析
SIL
这里可以看到Teacher的方法声明,同时我们还可以看到方法在SIL文件中的另一种表现形式:
这个
sil_vtable就是每个类的的函数表。
二、函数表vtable探索
- 之前我们分析过类的数据结构是
Metadata,那么vtable存放在哪?
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,它是对类的一个详细描述。这个typeDescriptor在源码中其实可以找到实际类型是TargetClassDescriptor,我们通过继承关系可以还原它的数据结构:
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
//V-Table
}
-
源码中最后找到
ClassContextDescriptorBuilder类这个类是类描述的建立者。
-
在该类找到一个
layuout方法
很明显这里有该类一系列的初始化数据的操作,除了调super::layout我们还看到addVTable
-
先看
super::layout可以看出这里面的操作可以对应上
TargetClassDescriptor的属性 -
再看
addVTable这里我们居然看到了地址偏移,这个方法其实就是在配置函数表。
三、结构体的方法调度
-
这里我们仅仅是把
class修改为struct,其它不变 -
断点到汇编
很明显,这里bl是直接取地址调用,这被称为静态调用(静态派发)。
-
当方法为拓展方法时会怎样? 这里我们加个拓展:
断点到汇编:
很明显,这里
teach3仍然是静态调用。 -
如果是
class的拓展方法会怎样,还会是函数表派发吗? 这里我们把struct改为class直接调试:很明显,这里
teach3是静态调用。
四、总结
这里我们可以总结一下方法的调度方式,当然还有其它的情况就不一一分析,其实分析的步骤就是重复上述步骤,也比较简单。
| 类型 | 常规 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| 类 | 函数表派发 | 静态派发 |
| NSObject子类 | 函数表派发 | 静态派发 |
五、影响函数派发的方式
- final: 添加了
final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见。注意: 如果对象只在声明的文件中可见,可以用
private或fileprivate进行修饰。编译器会对private或fileprivate对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问;private: 定义的声明中访问)
- dynamic: 函数均可添加
dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。
- @objc: 该关键字可以将
Swift函数暴露给Objc运行时,依旧是函数表派发。
-
@objc + dynamic: 消息派发的方式(
objc_msgSend) -
static:静态派发
六、函数内联
函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优 化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作,
Swift编译器可能会自动内联函数作为优化。 - always - 将确保始终内联函数。通过在函数前添加
@inline(__always)来实现此行为 - never - 将确保永远不会内联函数。这可以通过在函数前添加
@inline(never)来实现。 - 如果函数很长并且想避免增加代码段大小,请使用
@inline(never)