阅读本文前,可以先参考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 | import Foundation |
运行结果图
可以看到,在该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 | class Foo: NSObject { |
打印结果如下
可以看到,我交换了类方法,通过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 | protocol FooProtocol { |
替换witness_table指针结果图如下
替换witness_table中的函数指针结果图如下
开始分析:
第一个demo中,由于只替换了foo实例的witness_table指针,所以不会影响新的实例foo2的方法调用,而第二个demo,替换了witness _table的某个具体函数的指针,所以新实例foo2调用的foo方法也就替换了。由此可以看出,Swift中这种协议的调用方式。
当然你也可以在替换witness_table中的函数指针后加上下面代码
1 | let foo3 = Foo() |
打印的是foo, 通过Swift内存布局中得知即foo3是结构体Foo类型,顾方法派发方式是直接派发。不是协议类型的方法派发方式,所以替换了witness_table的具体函数指针也不起作用。
Class Protocol
接着我们来看看继承于Class的协议的方法调用
也是直接上demo吧
1 | protocol FooProtocol: class { |
最终foo打印的是poo,不是foo,即方法交换成功。即foo实例(协议类型)也是通过witness_table方式进行方法调用,类实例foo2则满足上面的Class的方法调用(vtable)
bug
希望有缘人能帮我解答下心中的疑惑~