6. 方法的调度

154 阅读4分钟

一、类的方法调度

  1. 我们先添加如下代码

408c246910fcdcb3a82f41e84f3fda64.png 注意:这里我们用真机(arm64)来调试

  1. 断点到汇编

149cd31a2ec14e8c8381b961f8bfb07d.png

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字节,其实相差的是函数指针的大小。

  1. x8存的是什么? 1e2dd111ec110399ce2aabbd90159c7c.png

这里我们通过register read x8可以发现,x8中其实是Teacher实例的metadata。 也就是说,teach函数的调用过程是这样的:找到metadata > 确定函数地址(metadata + 偏移量) > 执行函数

  1. 分析SIL 3709b008cb063819816dcb825f102245.png

这里可以看到Teacher的方法声明,同时我们还可以看到方法在SIL文件中的另一种表现形式:

6d6e63dd7fd68ba3ed7474c556461b17.png 这个sil_vtable就是每个类的的函数表。

二、函数表vtable探索

  1. 之前我们分析过类的数据结构是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
}
  1. 源码中最后找到ClassContextDescriptorBuilder0623d3efdf0e0dbcca414f2565816c40.png 这个类是类描述的建立者。

  2. 在该类找到一个layuout方法

1a13a10c0cf4e70b2d431b3bec338ce7.png

很明显这里有该类一系列的初始化数据的操作,除了调super::layout我们还看到addVTable

  1. 先看super::layout c81ffc7f77156f75a3a3c49242a0c3e1.png 可以看出这里面的操作可以对应上TargetClassDescriptor的属性

  2. 再看addVTable 55ded9a3f7a40d26d35280f703e78b36.png 这里我们居然看到了地址偏移,这个方法其实就是在配置函数表。

三、结构体的方法调度

  1. 这里我们仅仅是把class修改为struct,其它不变 53421a7d69f0d7e46124fdf1902a7c92.png

  2. 断点到汇编 e58e2f336876a5f3b96bfd9eb12aed36.png

很明显,这里bl是直接取地址调用,这被称为静态调用(静态派发)。

  1. 当方法为拓展方法时会怎样? 这里我们加个拓展: af258ab8efdbc300a567d97194c23561.png 断点到汇编: 59a724c3a88eeca3ab93f4bd3bff8389.png 很明显,这里teach3仍然是静态调用。

  2. 如果是class的拓展方法会怎样,还会是函数表派发吗? 这里我们把struct改为class直接调试: 2c15dfbc163d821eb9a7996f70082076.png 很明显,这里teach3是静态调用。

四、总结

这里我们可以总结一下方法的调度方式,当然还有其它的情况就不一一分析,其实分析的步骤就是重复上述步骤,也比较简单。

类型常规extension
值类型静态派发静态派发
函数表派发静态派发
NSObject子类函数表派发静态派发

五、影响函数派发的方式

  1. final: 添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见。 379d2287e0b40555c51bfa065e86fcb8.png 8ac6c5f32b597b2ebed9ff86fec519e2.png 注意: 如果对象只在声明的文件中可见,可以用 privatefileprivate 进行修饰。编译器会对 privatefileprivate对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问;private : 定义的声明中访问)

image-20220102111034710

image-20220102111153727

  1. dynamic: 函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。 6d9a0eac68a277c16de4394639284fcc.png

6d7b4194238b44bc38ea8ce73f817402.png

  1. @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。 083adfb3c8261b771a7d919c8e5371d8.png

59e96884e4e63a902c12ef3173e4ea38.png

  1. @objc + dynamic: 消息派发的方式(objc_msgSend1c4364c6065c457bc541d27847b90929.png

  2. static:静态派发 image-20220102175048340 image-20220102175117851

六、函数内联

函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优 化性能。

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作,Swift编译器可能会自动内联函数作为优化。
  • always - 将确保始终内联函数。通过在函数前添加@inline(__always)来实现此行为
  • never - 将确保永远不会内联函数。这可以通过在函数前添加@inline(never)来实现。
  • 如果函数很长并且想避免增加代码段大小,请使用@inline(never)