异变办法

上面的文章咱们讲到类和结构体都能界说办法,但是需求留意的是值类型的特点不能被本身办法修正。比如下面的比如:

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
  }
}

swift类,结构体方法的调度方式以及影响因素
关于结构体的值类型会报错’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 
 }

swift类,结构体方法的调度方式以及影响因素
其间有addVtable比较显眼,点击进去

swift类,结构体方法的调度方式以及影响因素
咱们斗胆猜测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看到:

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

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

咱们能够验证__swift5_types的字段存的是咱们的TargetClassDescriptor的地址

swift类,结构体方法的调度方式以及影响因素
也便是0x3F94+0xFFFFFF94=0x100003F28 这个地址减去0x100000000(虚拟内存基地址)得到3F28的相对地址 定位3F28的方位

swift类,结构体方法的调度方式以及影响因素
依据上面结构体的界说咱们偏移12个4字节便是size了,如下:

swift类,结构体方法的调度方式以及影响因素
那么后边的也便是咱们func1,func2,和func3了:

swift类,结构体方法的调度方式以及影响因素
再剖析源码

swift类,结构体方法的调度方式以及影响因素
能够经过image list得到运转随机初始地址
swift类,结构体方法的调度方式以及影响因素
得到前面的4字节是flags,后边的才是impl(相对地址) 所以咱们能够计算出func1在内存运转的实际地址是: 0x0000000100000000(运转随机初始地址)+ 0x3F5C = func1TargetMethodDescriptor 再 + 0x4(flags) + FFFFFAD0(impl的offset) = 0x200003A30 0x200003A300x0000000100000000(运转随机初始地址)= 0x100003A30 这个便是func1在内存运转的实际地址,咱们能够汇编验证下:

swift类,结构体方法的调度方式以及影响因素
这正好验证了咱们的猜想是正确的。

拓宽对办法调度的影响:

代码如下:

class SPClass {
  func func1() {
    print("func1")
  }
}
extension SPClass {
  func func2() {
    print("func2")
  }
}
var p = SPClass()
p.func2()

swift类,结构体方法的调度方式以及影响因素
咱们看到是函数地址的直接调用

协议对办法调度的影响:

protocol eat {
  func func2()
}
class SPClass: eat {
  func func1() {
    print("func1")
  }
  func func2() {
    print("func2")
  }
}
var p = SPClass()
p.func2()

swift类,结构体方法的调度方式以及影响因素
走的是函数表的调度

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

final关键字

class SPClass {
  final func func1() {
    print("func1")
  }
  func func2() {
    print("func2")
  }
}
var p = SPClass()
p.func1()

swift类,结构体方法的调度方式以及影响因素
咱们看到加了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()

swift类,结构体方法的调度方式以及影响因素
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()

swift类,结构体方法的调度方式以及影响因素
走的objc_msgSend消息派发的方法

值类型的办法调用

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

struct SPStruct {
   dynamic func func1() {
    print("func1")
  }
}
var p = SPStruct()
p.func1()

swift类,结构体方法的调度方式以及影响因素
咱们看到是直接调度。 这里咱们留意到struct的办法能够用dynamic关键字修饰,那么dynamic关键字到底有什么用呢,咱们看个比如就理解了

struct SPStruct {
   dynamic func func1() {
    print("func1")
   }
}
extension SPStruct {
  @_dynamicReplacement(for:func1)
  func func2() {
    print("func2")
  }
}
var p = SPStruct()
p.func1()

swift类,结构体方法的调度方式以及影响因素
咱们看到咱们调用func1 经过dynamic关键字能够动态调用func2

总结

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