异变办法
上面的文章咱们讲到类和结构体都能界说办法,但是需求留意的是值类型的特点不能被本身办法修正。比如下面的比如:
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: 消息派发的方法