一、异变方法
Swift
中 class
和 struct
都能定义方法。但是有一点区别的是默认情况下,值类型属性不能被自身的实例方法修改。如下所示:会报错。
mutating关键字
在结构体的方法前加上mutating
关键字就可以了:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
inout关键字
为什么呢?通过生成sil文件,分析源码可以看到(生成方法省略),加入mutating
关键字的方法,在该方法的参数列表中,self参数(self是默认参数,在参数列表的最后面)前面多了一个inout
关键字,而这个关键字的作用如下:
//不加mutating关键字的方法定义
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
//加入mutating关键字的方法定义
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
SIL 文档的解释 An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
异变方法的本质 :对于变异方法, 传入的 self
被标记为 inout
参数。无论在 mutating
方法 内部发生什么,都会影响外部依赖类型的一切。
输入输出参数 :如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个 inout
关键字可以定义一个输入输出形式参数
扩展:
如何通过函数修改外部变量的值?
如下的方法,直接修改是会报错的,因为函数的形参默认是 let
类型的
加入 inout
关键字之后,就不报错了,而且可以修改:
var age = 10
func modifyage(_ age: Int){
age += 1
}
modifyage(&age)
print(age)
二、方法调度
通过汇编调试,可以得出如下结果:
函数的调用过程: 找到Metadata
,确定函数地址(metadata
+ 偏移量),执行函数。
基于函数表(sil_vtable
)的调度
分析 sil
文件
sil_vtable 就是类的函数表
sil_vtable LGTeacher {
#LGTeacher.age!getter.1: (LGTeacher) -> () -> Int : @$s14LGSwiftExplore9LGTeacherC3ageSivg // LGTeacher.age.getter
#LGTeacher.age!setter.1: (LGTeacher) -> (Int) -> () : @$s14LGSwiftExplore9LGTeacherC3ageSivs // LGTeacher.age.setter
#LGTeacher.age!modify.1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC3ageSivM // LGTeacher.age.modify
#LGTeacher.teach!1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC5teachyyF // LGTeacher.teach()
#LGTeacher.teach1!1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC5teachyyF // LGTeacher.teach()
#LGTeacher.init!allocator.1: (LGTeacher.Type) -> () -> LGTeacher : @$s14LGSwiftExplore9LGTeacherCACycfC // LGTeacher.__allocating_init()
#LGTeacher.deinit!deallocator.1: @$s14LGSwiftExplore9LGTeacherCfD // LGTeacher.__deallocating_deinit
}
V-Table的存储位置
提到函数表,那V-Table是存在什么地方呢?
回顾一下当前的数据结构:
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
都有自己的Descriptor
,就是对类的一个详细的描述:
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
}
Mach-O
Mahco: Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常见的 .o,.a .dylib Framework,dyld .dsym。
Mahoc文件格式:
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排
- Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
方法调度方式总结:
类型 | 调度方式 | extension |
---|---|---|
值类型 | 静态派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject子类 | 函数表派发 | 静态派发 |
三、影响函数派发方式
final
: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见。 在实际开发过程中,属性、方法不需要被子类继承的时候就使用final。
dynamic
: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
@objc
: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
@objc + dynamic
: 消息派发的方式,msg_send
四、函数内联
函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很长并且想避免增加代码段大小,请使用@inline(never)(使用inline(never))
扩展
探索方法
关于上述探索过程,可以使用的方法包括:
1、汇编调试跟踪
2、源码阅读
3、MachO文件阅读