异变方法
上面的文章我们讲到类和结构体都能定义方法,但是需要注意的是值类型的属性不能被自身方法修改。比如下面的例子:
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
}
}
对于结构体的值类型会报错’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
}
其中有addVtable比较显眼,点击进去
我们大胆猜测
fieldOffsetVectorOffset后面就是offset和size的字段,然后就是我们的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看到:
其中:
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
- Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
我们可以验证__swift5_types的字段存的是我们的TargetClassDescriptor的地址
也就是
0x3F94+0xFFFFFF94=0x100003F28 这个地址减去0x100000000(虚拟内存基地址)得到3F28的相对地址
定位3F28的位置
根据上面结构体的定义我们偏移12个4字节就是
size了,如下:
那么后面的也就是我们
func1,func2,和func3了:
再分析源码
可以通过image list得到运行随机初始地址
得到前面的4字节是
flags,后面的才是impl(相对地址)
于是我们可以计算出func1在内存运行的实际地址是:
0x0000000100000000(运行随机初始地址)+ 0x3F5C = func1的TargetMethodDescriptor
再 + 0x4(flags) + FFFFFAD0(impl的offset) = 0x200003A30
0x200003A30 - 0x0000000100000000(运行随机初始地址)= 0x100003A30
这个就是func1在内存运行的实际地址,我们可以汇编验证下:
这正好验证了我们的猜想是正确的。
拓展对方法调度的影响:
代码如下:
class SPClass {
func func1() {
print("func1")
}
}
extension SPClass {
func func2() {
print("func2")
}
}
var p = SPClass()
p.func2()
我们看到是函数地址的直接调用
协议对方法调度的影响:
protocol eat {
func func2()
}
class SPClass: eat {
func func1() {
print("func1")
}
func func2() {
print("func2")
}
}
var p = SPClass()
p.func2()
走的是函数表的调度
关键字对方法调度的影响:
final关键字
class SPClass {
final func func1() {
print("func1")
}
func func2() {
print("func2")
}
}
var p = SPClass()
p.func1()
我们看到加了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()
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()
走的objc_msgSend消息派发的方式
值类型的方法调用
上面说的都是类这种引用类型,那么值类型比如结构体的方法是怎么调度的呢?我们看一下
struct SPStruct {
dynamic func func1() {
print("func1")
}
}
var p = SPStruct()
p.func1()
我们看到是直接调度。
这里我们注意到struct的方法可以用
dynamic关键字修饰,那么dynamic关键字到底有什么用呢,我们看个例子就明白了
struct SPStruct {
dynamic func func1() {
print("func1")
}
}
extension SPStruct {
@_dynamicReplacement(for:func1)
func func2() {
print("func2")
}
}
var p = SPStruct()
p.func1()
我们看到我们调用
func1 通过dynamic关键字可以动态调用func2
总结
- 异变方法的本质是:对于异变方法,传入的
self会被标记为inout参数。无论mutating内部发生什么,都会影响外部依赖类型的一切 - 我们通过汇编,sil以及源码分析得到了swift方法调度是函数表的调度
- 通过源码我们得到了
Metadata和TargetClassDescriptor的数据结构 - 我们通过MachO文件验证了确实存在函数表,也精确的计算出了具体调度函数的函数地址,再次通过汇编得到验证
- 值类型的方法调度是直接调度
- 关键字对方法调度的影响:
- final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见
- dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
- @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
- @objc + dynamic: 消息派发的方式