swift类,结构体方法的调度方式以及影响因素

1,008 阅读8分钟

异变方法

上面的文章我们讲到类和结构体都能定义方法,但是需要注意的是值类型的属性不能被自身方法修改。比如下面的例子:

class SPClass {
  var x: Double = 0
  var y: Double = 0
  
  func modify(x deltaX: Double, y deltaY: Double) {
    x += deltaX
    y += deltaY
  }
}

struct SPStruct {
  var x: Double = 0
  var y: Double = 0
  
  func modify(x deltaX: Double, y deltaY: Double) {
    x += deltaX
    y += deltaY
  }
}

image.png 对于结构体的值类型会报错’Left side of mutating operator isn't mutable: 'self' is immutable‘,而类确可以,这是为什么呢?

我们知道值类型的对象就是其成员变量本身,我们知道,结构体SPStruct的首地址也就是第一个成员变量x的地址,那么在方法modify里面修改x相当于修改结构体本身,这当然是不允许的,因为“不安全”。那么想要在结构体里面修改成本变量的值有什么办法呢,答案是使用mutating关键字。

那么使用mutating和不使用mutating的方法到底有什么区别呢?我们使用sil来观察一下。 不使用mutating:

// SPStruct.modify(x:y:)
sil hidden @$s4main8SPStructV6modify1x1yySd_SdtF : $@convention(method) (Double, Double, SPStruct) -> () {
// %0 "deltaX"                                    // user: %3
// %1 "deltaY"                                    // user: %4
// %2 "self"                                      // user: %5
bb0(%0 : $Double, %1 : $Double, %2 : $SPStruct):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
  debug_value %2 : $SPStruct, let, name "self", argno 3, implicit // id: %5
  %6 = tuple ()                                   // user: %7
  return %6 : $()                                 // id: %7
} // end sil function '$s4main8SPStructV6modify1x1yySd_SdtF'

关键是

  • $@convention(method) (Double, Double, SPStruct)
  • debug_value %2 : $SPStruct, let, name "self", argno 3

使用mutating:

// SPStruct.modify(x:y:)
sil hidden @$s4main8SPStructV6modify1x1yySd_SdtF : $@convention(method) (Double, Double, @inout SPStruct) -> () {
// %0 "deltaX"                                    // user: %3
// %1 "deltaY"                                    // user: %4
// %2 "self"                                      // user: %5
bb0(%0 : $Double, %1 : $Double, %2 : $*SPStruct):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
  debug_value %2 : $*SPStruct, var, name "self", argno 3, implicit, expr op_deref // id: %5
  %6 = tuple ()                                   // user: %7
  return %6 : $()                                 // id: %7
} // end sil function '$s4main8SPStructV6modify1x1yySd_SdtF'

关键是

  • $@convention(method) (Double, Double, @inout SPStruct)
  • debug_value %2 : $*SPStruct, var, name "self", argno 3

我们看到不同点是对于默认的参数会多出一个inout的关键字,查看inout的sil文档解释:An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址) 所以异变方法的本质是:对于异变方法,传入的self会被标记为inout参数。无论mutating内部发生什么,都会影响外部依赖类型的一切。

swift类的结构

源码分析

根据源码分析不难看出

using ClassMetadata = TargetClassMetadata<InProcess>;

ClassMetadata是TargetClassMetadata的别名

TargetClassMetadata继承自TargetAnyClassMetadata; TargetClassMetadat有属性

  • ClassFlags(uint32_t) Flags类标志
  • uint32_t InstanceAddressPoint实例地址指针
  • uint32_t InstanceSize该类型实例的所需大小
  • uint16_t InstanceAlignMask此类型实例地址的对齐掩码
  • uint16_t Reserved保留字段
  • uint32_t ClassSize类对象总大小
  • uint32_t ClassAddressPoint类对象的偏移量
  • ConstTargetMetadataPointer<Runtime, TargetClassDescriptor> Description;类描述
  • TargetPointer<Runtime, ClassIVarDestroyer> IVarDestroyer;

TargetAnyClassMetadata继承自TargetHeapMetadata TargetAnyClassMetadata有属性

  • ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;执行父类的指针
  • TargetPointer<Runtime, void> CacheData[2];缓存数据,缓存一些动态查找,用于OC运行时
  • StoredSize Data;元数据头:元数据头

TargetHeapMetadata继承自TargetMetadata TargetMetadata中有一个属性StoredPointer Kind;哪种元类型 总结如下,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都有自己的typeDescriptor的构造方法,对于类我们看到有这样的函数ClassContextDescriptorBuilder 阅读源码不难得出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 
 }

image.png 其中有addVtable比较显眼,点击进去

image.png 我们大胆猜测fieldOffsetVectorOffset后面就是offsetsize的字段,然后就是我们的v-table的内容了,我们用sil来看下有没有所谓的vtable

sil分析

import Foundation

class SPClass {
  func func1() {
    print("func1")
  }
  func func2() {
    print("func2")
  }
  func func3() {
    print("func3")
  }
}

var p = SPClass()
p.func1()
p.func2()
p.func3()

我们对上述代码做sil得到文件

class SPClass {
  func func1()
  func func2()
  func func3()
  @objc deinit
  init()
}
// SPClass.func1()
sil hidden @$s4main7SPClassC5func1yyF : $@convention(method) (@guaranteed SPClass) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $SPClass):
  debug_value %0 : $SPClass, let, name "self", argno 1, implicit // id: %1
  ........//省略部分代码
 } // end sil function '$s4main7SPClassC5func1yyF'
sil_vtable SPClass {
  #SPClass.func1: (SPClass) -> () -> () : @$s4main7SPClassC5func1yyF	// SPClass.func1()
  #SPClass.func2: (SPClass) -> () -> () : @$s4main7SPClassC5func2yyF	// SPClass.func2()
  #SPClass.func3: (SPClass) -> () -> () : @$s4main7SPClassC5func3yyF	// SPClass.func3()
  #SPClass.init!allocator: (SPClass.Type) -> () -> SPClass : @$s4main7SPClassCACycfC	// SPClass.__allocating_init()
  #SPClass.deinit!deallocator: @$s4main7SPClassCfD	// SPClass.__deallocating_deinit
}

看到最后我们确实看到了sil-vtable的存在 下面我们用mach-o来验证我们的猜想:

mach-o分析

将上述代码build然后拖进MachOView看到:

image.png 其中:

  • 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

我们可以验证__swift5_types的字段存的是我们的TargetClassDescriptor的地址

image.png 也就是0x3F94+0xFFFFFF94=0x100003F28 这个地址减去0x100000000(虚拟内存基地址)得到3F28的相对地址 定位3F28的位置

image.png 根据上面结构体的定义我们偏移12个4字节就是size了,如下:

image.png 那么后面的也就是我们func1,func2,和func3了:

image.png 再分析源码

image.png 可以通过image list得到运行随机初始地址 image.png 得到前面的4字节是flags,后面的才是impl(相对地址) 于是我们可以计算出func1在内存运行的实际地址是: 0x0000000100000000(运行随机初始地址)+ 0x3F5C = func1TargetMethodDescriptor 再 + 0x4(flags) + FFFFFAD0(impl的offset) = 0x200003A30 0x200003A30 - 0x0000000100000000(运行随机初始地址)= 0x100003A30 这个就是func1在内存运行的实际地址,我们可以汇编验证下:

image.png 这正好验证了我们的猜想是正确的。

拓展对方法调度的影响:

代码如下:

class SPClass {
  func func1() {
    print("func1")
  }
}

extension SPClass {
  func func2() {
    print("func2")
  }
}

var p = SPClass()
p.func2()

image.png 我们看到是函数地址的直接调用

协议对方法调度的影响:

protocol eat {
  func func2()
}

class SPClass: eat {
  func func1() {
    print("func1")
  }
  
  func func2() {
    print("func2")
  }
}

var p = SPClass()
p.func2()

image.png 走的是函数表的调度

关键字对方法调度的影响:

final关键字

class SPClass {
  final func func1() {
    print("func1")
  }
  
  func func2() {
    print("func2")
  }
}

var p = SPClass()
p.func1()

image.png 我们看到加了final关键字的func1变成了直接调度,func2还是函数表调度 我们可以对比看下sil文件:

sil_vtable SPClass {
  #SPClass.func2: (SPClass) -> () -> () : @$s4main7SPClassC5func2yyF	// SPClass.func2()
  #SPClass.init!allocator: (SPClass.Type) -> () -> SPClass : @$s4main7SPClassCACycfC	// SPClass.__allocating_init()
  #SPClass.deinit!deallocator: @$s4main7SPClassCfD	// SPClass.__deallocating_deinit
}

sil_vtable里面确实也少了func2

dynamic关键字关键字

class SPClass {
  dynamic func func1() {
    print("func1")
  }
  
  func func2() {
    print("func2")
  }
}

var p = SPClass()
p.func1()
p.func2()

image.png dynamic没有影响,还是函数表的调用

@objc关键字

没有影响,还是函数表的调用,只是OC可以调用swift方法

@objc dynamic关键字

class SPClass {
  @objc dynamic func func1() {
    print("func1")
  }
  
  func func2() {
    print("func2")
  }
}

var p = SPClass()
p.func1()
p.func2()

image.png 走的objc_msgSend消息派发的方式

值类型的方法调用

上面说的都是类这种引用类型,那么值类型比如结构体的方法是怎么调度的呢?我们看一下

struct SPStruct {
   dynamic func func1() {
    print("func1")
  }
}

var p = SPStruct()
p.func1()

image.png 我们看到是直接调度。 这里我们注意到struct的方法可以用dynamic关键字修饰,那么dynamic关键字到底有什么用呢,我们看个例子就明白了

struct SPStruct {
   dynamic func func1() {
    print("func1")
   }
}

extension SPStruct {
  @_dynamicReplacement(for:func1)
  func func2() {
    print("func2")
  }
}

var p = SPStruct()
p.func1()

image.png 我们看到我们调用func1 通过dynamic关键字可以动态调用func2

总结

  • 异变方法的本质是:对于异变方法,传入的self会被标记为inout参数。无论mutating内部发生什么,都会影响外部依赖类型的一切
  • 我们通过汇编,sil以及源码分析得到了swift方法调度是函数表的调度
  • 通过源码我们得到了MetadataTargetClassDescriptor的数据结构
  • 我们通过MachO文件验证了确实存在函数表,也精确的计算出了具体调度函数的函数地址,再次通过汇编得到验证
  • 值类型的方法调度是直接调度
  • 关键字对方法调度的影响:
    • final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见
    • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
    • @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
    • @objc + dynamic: 消息派发的方式