Swift方法调用

参考链接

阅读本文前,可以先参考Swift内存布局

本文基于Swift4.2, Xcode10.0

必看:本例的函数指针的替换千万别用在正式项目中,只是用来验证方法的调用方式,具体函数指针在数据结构内存(Vtable和witness_table)的偏移值会在不同的编译器或者语言版本而有不同

编译型程序的方法调用有三种方式,第一种是直接派发(静态调度),运行时寄存器通过call指令直接跳转至代码的具体实现地址。第二种通过虚函数表的方式,通过计算出函数指针在虚函数表中偏移量,得到函数指针,在通过call指令跳转到函数指针指向的内存地址,执行函数。第三种是动态调度,在Objective-C或者Swift(部分方法调用)中通过call objc_msgSend实现消息机制,在执行objc _ msgSend方法时,找到参数类对象(objc_ class)中method__list真正函数实现的指针(IMP),在跳转至函数指针指向的内存执行具体的函数,当然没找到函数指针,会去父类中找,最后在NSObject类对象中没有找到函数指针,还会进行消息转发。(其实第二种和第三种可以合并为动态派发)

我们都知道在Objective-C语言中,继承于NSObject类的实例方法或者类方法调用编译后都会通过objc_msgSend去找到具体的函数然后实现。而对于Swift,三种方法调用都存在。由于不是所有的数据结构都是Class(即编译后不会生成对应的objc _ class),顾方法调用不都是消息机制。即使对于Class,由于Swift中虚函数表的存在以及编译的优化(方法加了@objc也不一定走消息机制),方法调用方式也复杂不少

Class

实例方法的调用

编译后函数存在的方式:
对于没加@objc的实例方法,编译后在类对象中的vtable以指针的方式指向该函数的具体实现。
而对于加了@objc的实例方法,编译后该方法函数指针会在vtable中存在,而且还会以指针的形式在类对象中的method_list中存在。

调用:

  • 对于没加@objc的方法,只能通过虚函数表方式实现具体方法。
  • 而对于加了@objc的方法,由于Swift的编译优化,没有通过Selector调用的方法,编译后还是会走虚函数表的机制。而通过Selector调用的方法,编译后会走消息机制。当然还有一个编译参数dynamic,对于dynamic修饰的方法,编译后不管是否通过Selector调用都会走消息机制。
    (对于final修饰的方法或者类,方法的调用是静态调度)

接下来举个demo,通过替换虚函数表中的函数指针以及运行时的方法交换,来验证函数的调用方式。以及通过反编译二进制文件,看函数调用的汇编指令,来查看调用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import Foundation

// 继承NSObject, 只是为了能调用perform方法,不影响方法调用机制
class Foo: NSObject {
var name: String = "Foo"

@objc func foo() {
print("foo")
}

func foo2() {
print("foo2")
}
}

class Poo {
var name: String = "Poo"

func poo() {
print("poo")
}

func poo2() {
print("poo2")
}
}

// 获取object的指针
func getPointer<T: AnyObject>(objc: T) -> UnsafeMutableRawPointer {
return Unmanaged.passUnretained(objc).toOpaque()
}

// foo类对象指针
let fooClass = objc_getClass("SwiftRuntime.Foo")! as AnyObject
var fooClassPointer = getPointer(objc: fooClass)

// poo类对象指针
let pooClass = objc_getClass("SwiftRuntime.Poo")! as AnyObject
let pooClassPointer = getPointer(objc: pooClass)

// 104(第一个函数指针) 方法替换
let fooMethonPointer = fooClassPointer.advanced(by: 104).assumingMemoryBound(to: UnsafeMutableRawPointer.self)
// 将104改为112,实例foo.foo()就会执行Poo的poo2方法(一个指针占8个字节)
let pooMethonPointer = pooClassPointer.advanced(by: 104).assumingMemoryBound(to: UnsafeMutableRawPointer.self)
fooMethonPointer.initialize(to: pooMethonPointer.pointee)

let foo = Foo()
foo.perform(#selector(Foo.foo))
foo.foo()
print("===end===")

运行结果图

可以看到,在该demo中我只是将Foo虚函数表的第一个函数指针替换成Poo虚函数表的第一个函数指针, 通过直接调用实例foo方法,最后打印了poo,即调用了Poo的poo方法。但是通过Select调用该方法,走的是消息机制,由于没有将Foo类对象的method_list中的foo方法替换(通过method _ exchange API替换,或者自己找到对应函数指针去替换,额,难度有点大…),还是打印的foo。

接着在Foo的方法foo前加上dynamic修饰,其他代码不变。可以发现,打印的都是foo。即加上dynamic修饰的方法无论是否通过Select方式调用该方法,都是走的消息机制。

接下来看看反汇编后,方法调用的指令(没加dynamic修饰)

可以看到编译后两者方法调用方式完全不同(同时也解释了上面demo中104的由来,当然一开始我是从16个字节开始8位8位的往上加试着来…有点蠢(当然我当时通过Xcode的view Memory数出来Foo的objc _ class占用了170多个字节),而且通过objc_class源码知道vtable是一个结构体,vtable中的函数指针应该就直接存在类对象的内存结构中,不会在以一个指针指向vtable了)

类方法的调用

对于没有加@objc的类方法,在编译后,类方法不会以指针的形式被编译到vtable结构体中。而是直接储存在二进制的代码段中。在调用的地方,直接跳转至函数实现的地方。
对于加了@objc的类方法,编译后,会以函数指针的方式会添加到该类对应的元类的method_list中。通过Select调用,跳转至函数指针指向的地方。

  • 对于没有加@objc的类方法,是走的直接派发的方式。
  • 对于加了@objc的类方法,没有通过Selector方式调用,是直接派发的方式调用函数。而通过Selector方式调用函数,走的是消息机制(去元类中的method_list中找方法)。对于dynamic修饰的类方法,无论是否通过Selector方式调用,都走的消息机制。

下面举个demo看下结论是否正确,由于类方法不储存在虚函数表中,因此不能像上面通过替换虚函数表中的函数指针的方式替换类方法(当然替换该方法符号指针,也能替换方法,可以参考MSHookFunction实现原理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Foo: NSObject {
var name: String = "Foo"

@objc class func foo() {
print("foo")
}
}

class Poo {
var name: String = "Poo"

@objc class func poo() {
print("poo")
}
}

// 交换类方法
let fooMethod = class_getClassMethod(Foo.self, #selector(Foo.foo))!
let pooMethod = class_getClassMethod(Poo.self, #selector(Poo.poo))!
method_exchangeImplementations(fooMethod, pooMethod)

Foo.foo()
_ = Foo.perform(#selector(Foo.foo))
print("===end===")

打印结果如下

可以看到,我交换了类方法,通过Selector方式调用方法,达到了方法交换的目的。没有通过Selector方式调用方法,还是打印的foo。

接着可以在方法的@objc后面加上dynamic修饰,其他代码不变,可以发现,打印的都是poo。即都是通过消息机制调用方法。

也可以看看反汇编二进制的函数调用指令,也能知道方法的调用方式

可以看到_$S12SwiftRuntime3FooC3fooyyFZ就是编译后Foo类方法foo的符号,而通过perform调用则通过objc _ msgSend调用方法

类扩展中的方法调用(包括类方法和实例方法)

对于没有加@objc的类扩展的方法,在编译后,方法不会以指针的形式被编译到vtable结构体中(这也能解释为啥扩展中的方法不能override)。在调用的地方,会直接跳转至函数实现的地方。

对于加了@objc的类扩展的方法,编译后,该函数指针除了以符号的形式储存在二进制文件中,还会被添加到objc_category结构体中(顾子类要override父类扩展中的方法得加@objc修饰)。

  • 对于没有加@objc的类扩展中的方法,走的直接派发的方式。
  • 对于加了@objc的类扩展中的方法,无论是否通过Selector方式调用,都走的消息机制。

demo图片,可以看到交换方法后,无论是否通过Selector方式调用方法,都走的消息机制

Struct

由于结构体编译后,并不会编译成objc_class,因此没有vtable(值类型不能继承),也没有method _ list(即消息机制)。都是通过直接派发的方式调用函数。

Protocol

Pure Protocol

最后我们来看看协议类型的方法调用, 在Swift内存布局中我有说到协议类型的内存布局中一个witness_table的属性,是一个指针,指向的是协议中定义的方法表

协议类型,通过类似虚函数表的方式调用。因为witness_table类似于c++中的vtable.

接下来看demo吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
protocol FooProtocol {
func foo()
}

protocol PooProtocol {
func poo()
}

struct Foo: FooProtocol {
let name = "Foo"

func foo() {
print("foo")
}
}

struct Poo: PooProtocol {
let name = "Poo"

func poo() {
print("poo")
}
}

print(MemoryLayout<FooProtocol>.size)
print(MemoryLayout<PooProtocol>.size)

func getPointer<T: Any>(value: inout T) -> UnsafeMutableRawPointer {
let pointer = withUnsafeMutablePointer(to: &value) { (pointer) -> UnsafeMutablePointer<T> in
return pointer
}
return UnsafeMutableRawPointer(pointer)
}

var foo: FooProtocol = Foo()
var poo: PooProtocol = Poo()

// 40-8: 参考Swift内存布局中的Protocol部分
let fooPointer = getPointer(value: &foo)
let foo_witness_table_pointer = fooPointer.advanced(by: 40-8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

let pooPointer = getPointer(value: &poo)
let poo_witness_table_pointer = pooPointer.advanced(by: 40-8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

// 替换Foo的foo方法(替换了foo的witness_table)
foo_witness_table_pointer.initialize(to: poo_witness_table_pointer.pointee)

// 8: 从0开始8个字节8个字节依次试出来的(编译器或语言版本不同,该值会有出入)
let method_foo_pointer = foo_witness_table_pointer.pointee.advanced(by: 8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)
let method_poo_pointer = poo_witness_table_pointer.pointee.advanced(by: 8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

// 替换witness_table中的函数指针
//method_foo_pointer.initialize(to: method_poo_pointer.pointee)

print("=======从这开始看=======")
foo.foo()
let foo2: FooProtocol = Foo()
foo2.foo()
print("===end===")

替换witness_table指针结果图如下

替换witness_table中的函数指针结果图如下

开始分析:
第一个demo中,由于只替换了foo实例的witness_table指针,所以不会影响新的实例foo2的方法调用,而第二个demo,替换了witness _table的某个具体函数的指针,所以新实例foo2调用的foo方法也就替换了。由此可以看出,Swift中这种协议的调用方式。

当然你也可以在替换witness_table中的函数指针后加上下面代码

1
2
let foo3 = Foo()
foo3.foo()


打印的是foo, 通过Swift内存布局中得知即foo3是结构体Foo类型,顾方法派发方式是直接派发。不是协议类型的方法派发方式,所以替换了witness_table的具体函数指针也不起作用。

Class Protocol

接着我们来看看继承于Class的协议的方法调用

也是直接上demo吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
protocol FooProtocol: class {
func foo()
}

protocol PooProtocol {
func poo()
}

class Foo: FooProtocol {
let name = "Foo"

func foo() {
print("foo")
}
}

struct Poo: PooProtocol {
let name = "Poo"

func poo() {
print("poo")
}
}

func getPointer<T: Any>(value: inout T) -> UnsafeMutableRawPointer {
let pointer = withUnsafeMutablePointer(to: &value) { (pointer) -> UnsafeMutablePointer<T> in
return pointer
}
return UnsafeMutableRawPointer(pointer)
}

var foo: FooProtocol = Foo()
let fooPointer = getPointer(value: &foo)
// class protocol witness_table's pointer
let foo_witness_table_pointer = fooPointer.advanced(by: 8).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

var poo: PooProtocol = Poo()
let pooPointer = getPointer(value: &poo)
// pure protocol witness_table's pointer
let poo_witness_table_pointer = pooPointer.advanced(by: 32).assumingMemoryBound(to: UnsafeMutableRawPointer.self)

// relpace witness_table' pointer
foo_witness_table_pointer.initialize(to: poo_witness_table_pointer.pointee)

foo.foo()
let foo2 = Foo()
foo2.foo()
print("===end===")


最终foo打印的是poo,不是foo,即方法交换成功。即foo实例(协议类型)也是通过witness_table方式进行方法调用,类实例foo2则满足上面的Class的方法调用(vtable)

bug

我在研究时Swift方法调用时碰到的问题

希望有缘人能帮我解答下心中的疑惑~