一、异变方法
1.1 mutating关键字
Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况 下,值类型属性不能被自身的实例方法修改。
解决方式:方法用mutating关键字进行修饰就不报错了
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltax: Double,y deltaY:Double) {
x += deltax
y += deltaY
}
}
1.2 sil分析
添加测试代码
import Foundation
struct Point {
var x = 0.0, y = 0.0
func test(){
let z = self.x
}
mutating func moveBy(x deltax: Double,y deltaY:Double) {
x += deltax
y += deltaY
}
}
在命令行中调用swiftc -emit-sil命令来生成sil文件,查看中间代码
swiftc -emit-sil main.swift
过 sil 来对比一下,不添加 mutating 访问和添加 mutating 两者有什么本质的区别
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
...
} // end sil function '$s4main5PointV4testyyF'
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
...
} // end sil function '$s4main5PointV6moveBy1x1yySd_SdtF'
对比两个函数可以发现test隐藏参数是Point,moveBy的隐藏参数是 @inout Point,关于@inout的官方解释:
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
用 @inout 修饰接受的是一个地址就,是可以修改的,否则接受的就是一个值,就不能够被修改。
1.3 输入输出参数 inout
如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。在形式参数定义开始的时候在前边添加一个 inout 关键字可以定义一个输入输出形式参数
如果形参没有被 inout 修饰,修改形参就会报错:
想要修改形参,需要用 inout 进行修饰,并传入地址类型:
var age = 2
func changeAge(_ age: inout Int) {
age += 10
}
print(age)
changeAge(&age)
print(age)
打印结果
2
12
二、方法调度
2.1 class方法调度
2.1.1 示例
class SwiftMethodViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = CHTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
class CHTeacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
汇编调试:
-
上图中#0x50,0x58,0x60是三个连续的偏移地址,分别表示方法teach()、teach1()、teach2()
-
当前断点指在t.teach2(),读取当前寄存器x8,如下
(lldb) register read x8
x8 = 0x0000000102ed8968 SwiftDemo`SwiftDemo.CHTeacher.teach2() -> () at SwiftMethodViewController.swift:33
2.1.2 sil验证
打开命令行,调用下面命令生sil
swiftc -emit-silgen -Onone -target x86_64-apple-ios14.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) 文件地址
sil中,定位到最后面的vtable函数表,其中有类所有的函数
sil_vtable CHTeacher {
#CHTeacher.teach: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC5teachyyF // CHTeacher.teach()
#CHTeacher.teach1: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach1yyF // CHTeacher.teach1()
#CHTeacher.teach2: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach2yyF // CHTeacher.teach2()
#CHTeacher.init!allocator: (CHTeacher.Type) -> () -> CHTeacher : @$s25SwiftMethodViewController9CHTeacherCACycfC // CHTeacher.__allocating_init()
#CHTeacher.deinit!deallocator: @$s25SwiftMethodViewController9CHTeacherCfD // CHTeacher.__deallocating_deinit
}
2.2 class的extension函数调用
extension CHTeacher {
func teach3() {
print("teach3")
}
}
汇编调试:
可以看到,class的extension函数teach3是直接地址调用,静态派发
2.3 struct函数调度
class SwiftMethodViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = CHTeacher()
t.teach()
t.teach1()
t.teach2()
}
}
struct CHTeacher{
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
汇编调试:
通过汇编代码可以看到,struct的函数调用是直接地址调用,静态派发
2.4 struct的extension函数调度
extension CHTeacher {
func teach3() {
print("teach3")
}
}
汇编调试:
可以看到,struct的extension函数teach3是直接地址调用,静态派发
2.5 方法调度总结
| 类型 | 调度方式 | extension |
|---|---|---|
| 值类型 | 静态派发 | 静态派发 |
| 类 | 函数表派发 | 静态派发 |
| NSObject子类 | 函数表派发 | 静态派发 |
三、关键字对派发的影响
3.1 final
添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
class CHTeacher{
final func teach(){
print("teach")
}
...
}
汇编调试:
sil_vtable CHTeacher {
#CHTeacher.teach1: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach1yyF // CHTeacher.teach1()
#CHTeacher.teach2: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach2yyF // CHTeacher.teach2()
#CHTeacher.init!allocator: (CHTeacher.Type) -> () -> CHTeacher : @$s25SwiftMethodViewController9CHTeacherCACycfC // CHTeacher.__allocating_init()
#CHTeacher.deinit!deallocator: @$s25SwiftMethodViewController9CHTeacherCfD // CHTeacher.__deallocating_deinit
}
3.2 dynamic
函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
class CHTeacher{
dynamic func teach(){
print("teach")
}
}
extension CHTeacher {
@_dynamicReplacement(for: teach())
func teach3(){
print("teach3")
}
}
let t = CHTeacher()
t.teach()
打印结果:teach3
3.3 @objc
该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class CHTeacher{
@objc func teach(){
print("teach")
}
}
3.4 @objec+dynamic
消息派发的方式,这时我们可以使用runtime的api
class SwiftMethodViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = CHTeacher()
t.teach()
}
}
class CHTeacher{
@objc dynamic func teach(){
print("teach")
}
}
汇编调试:
四、函数内联
函数内联 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
4.1 OC函数内联
int sum(int a, int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int a = sum(1,2);
NSLog(@"%d",a);
return 0;
}
汇编调试:
汇编看到,没有优化时,将1和2分别存入w0和w1,再调用sun函数
然后开启优化
再进行汇编调试:
开启优化后,直接将结果3存入寄存器,然后调用NSLog函数
4.2 Swift函数内联
- Swift 中的内联函数是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
// 编译器会认为 test 没有太多意义,会省略test的符号调用,直接调用print
func test() {
print("test");
}
-
always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
-
never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
-
如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))
-
如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明中访问)