Swift 方法(函数)调度
[TOC]
1. 前言
由于Objective-C
是一门动态语言,方法的调度中主要是消息查找和消息转发。那么对于静态的Swift
中的方法是如何调度的呢?下面我们就来一起探索一下。
2. 静态派发
首先还是需要了解一下Swift
中的值类型和引用类型。
对于值类型对象的函数的调用方式是静态(直接)派发(调用),也可以说是直接地址调用(指针调用),也就是说,在编译,链接完成后当前函数的地址在Mach-O
代码段就已经有了确定的位置。
Swift
中典型的值类型就是结构体,那么我们就通过如下的分析,证明结构体中函数的调用是静态调用。
首先我们编写如下代码:
struct Teacher {
func teach() {}
}
var t = Teacher()
t.teach()
print("end")
2.1 通过汇编看函数的调用
在函数调用处添加断点
开启汇编调试Xcode
->Debug
->Debug Workflow
->Always Show Disassembly
运行结果如下:
我们可以看到,在这里直接是call
这个地址,其实这就足以说明是直接地址调用。
2.2 通过MachOView进一步探索
2.2.1 汇编指令的查找
下面我们将可执行文件拖拽到MachOView
中。
在MachOView`中的代码段(__text),由于我们的代码很少,很容易就找了这段汇编指令,如下图所示。
2.2.2 符号的查找
在2.1
中的汇编代码中,我们可以看到后面的注释直接标示处理函数的符号,那么这个符号存储在Mach-O
中的什么位置呢?
首先我们来到符号表Symbol Table
中查找一下:
PS: 其实还有个Dynamic Symbol Table
,这是存储动态库中一些符号的偏移信息的,这里并不是我们要查找的目标。
我们知道字符串是存储在字符串表的,其中包含变量的名称,函数的名称等,那么我们就可以根据符号表中的偏移值到字符串表中查找相应的字符。
由于代码不多,在字符串边也很容易就找到了我们的字符串,偏移值是2,所以排除第一空格,第二个.
往后面就是我们要查找到的符号的首个字符了。
其实我们还可以通过nm
命令去查找符号,首先cd 到Mach-O
文件的路径使用nm xxxx
来查看当前Mach-O
中的符号,比如我们这里的符号就是:
当然这些符号是通过命名重整的,如果想要得到和2.1
中那样的符号可以通过xcrun swift-demangle 符号
这个命令进行还原符号。比如上图中的第一个符号:(注意:要去掉前面的_$)
关于符号的查找,其实在release
环境中并不会像上面一样查找得到。我们更换为release
环境,编译。
我们打开可执行文件的目录:
在这里多了一个dSYM
文件。
我们再次将Mach-O
文件拖拽到MachoView
中打开,首先我们会看到如下的结果:
这里多了个Fat Header
,并且有了x86_64
和ARM64
两种架构的可执行文件,这是为什么呢?其实就是release
环境会包含所有支持的架构的可执行文件,详细的介绍可以看我这篇文章Mach-O探索,下面我们打开x86_64
这个可执行文件进行查找,因为我的电脑还是这个架构的,不是M1
的。
我们在符号表Symbol Table
继续搜索teach
,结果如下:
根据上图我们可以看到并没有搜索到任何结果,我们去字符串表(String Table)进行查看也同样没有teach
相关的结果。其实主要原因是静态链接的函数在release
环境是不需要使用符号的,在编译完成后,其地址就已经确定了,此时也不需要调试了,留着符号就会过多的占用存储空间,增加可执行文件的体积,那么还留在符号表干嘛呢?其实对于静态函数和一些确定的可以不需要存储符号,但是那些不确定的符号,比如说一些动态加载的库和函数,就需要用到符号了,所以保留符号表还是非常有用的。
对于不能确定的符号,比如说print
,如果我们不调用它,就不需要加载它,在运行的时候,我们通过print
打印一些数据,就会将它加载到内存,相当于懒加载,此时会通过dyld
的bind
操作将其进行绑定,(在Xcode 12.2
中我并没有测试出来,♂️),但是相关原理还是没问题的,关于dyld
的加载,也可以参考我的另一篇文章iOS 应用加载dyld
篇。
其实关于符号在不同语言中是不一样的,在C
和Objective-C
中我们是不能使用同名方法的,原因就是他们对于命名重整的规则比较简单。
C
#include
void test(){
printf("test");
}
我们可以看到在C语言中命名重整就是在函数名称前面加个_
,所以对于C语言来说,如下的写法是会报编译错误的:
Objective-C
@interface Test: NSObject
@end
@implementation Test
-(void)oc_test{
NSLog(@"oc_test");
}
@end
我们可以看到在OC中命名重整就是在使用-[类名 方法名],如果是类方法就是+[类名 方法名],所以对于OC来说,同名的类方法和对象方法是可以同时存在的,但是如下的写法是会报编译错误的:
对于Swift
来说,使用的是运行函数重载,可以使用同名不同参的函数,因为它的命名重整规则很复杂,可以确保函数符号的唯一性。
2.2.3 函数地址
在上面测试中我们看到汇编代码中call
的地址和Mach-O
文件中的地址是一致的,其实在iOS手机中运行的地址是不一致的,下面我们编写一个iOS APP。
代码如下:
import UIKit
struct Teacher {
func teach() {}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach()
print("end")
}
}
使用真机运行,在汇编代码和Mach-O
中查看,结果如下图:
可以明显的看出两个的地址是不一样的。那么这是为什么呢?其实就是iOS系统为了安全性,使用了ASLR
,即地址空间配置随机化,英文全称是(Address space layout randomization)。
简单来说就是利用随机的方式配置一个数据地址空间,这样攻击者就不会轻易的找到特定的内存地址来进行攻击。其实我们常见的操作系统级别都已经实现了ASLR
,详情请自行百度。下面我们就通过计算得出内存地址。
首先在iOS这边查看ASLR
需要通过image list
命令。
这里我们得到的随机加载首地址是:0x0000000102da8000
,我们让Mach-O
中的函数地址加上ASLR
,即:
0000000100007844 + 0x0000000102da8000 = 0x202DAF844
这与我们的实际调用地址还是不一样啊?其实在Mach-O
中还有个虚拟地址,在Mach-O
的Load Commands
段,存放了各段的虚拟内存地址和偏移量等信息。
我们打开Mach-O
的Load Commands
中的LC_SEGMENT_64(__PAGEZERO)
:
我们可以看到VM Address
是0,VM Size
是0000000100000000
,File Offset
和File Size
也是0,说明我们在符号表看到的地址是相对Mach-O
中首地址做了加虚拟地址和大小的偏移的,这也是Mach-O
中做的类似安全性的一种方案,所以去除掉这段虚拟的偏移就是我们想要的真实的内存地址了。
0000000100007844 + 0x0000000102da8000 - 0000000100000000 = 0x102DAF844
其实我们还可以通过image list -o -f
命令直接拿到去除掉这段偏移的ASLR
0000000100007844 + 0x0000000002da8000 = 0x102DAF844
2.4 小结
经过上面一番的探索,总结如下:
- 无论是在
Mac OS
中的应用程序(call 0x100003e50
)还是iOS
真机(bl 0x102DAF844
),在汇编代码上都是直接调用一个地址 - 在此我们也通过
Mach-O
文件查找到了函数地址的存储 - 还在
iOS
真机环境通过ASLR
计算得到了真实的函数地址 - 所以地址调用就是直接调用,也就是静态调用
- 至此,在
Swift
结构体struct
(值类型)中的函数是静态调用基本证明完毕
3. 动态派发
既然有静态,那么当然就会有动态,在Swift
中的引用类型数据代表类中的函数调用就是动态派发。下面我们来一起看看。
3.1 vtable 简介
3.1.1 简介
首先介绍一下Swift
中的vtable
在SIL
文件中的格式:
//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法 包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na
me
3.1.2 示例
Swift 示例代码:
class Teacher {
func teach() {}
func teach1() {}
func teach2() {}
func teach3() {}
func teach4() {}
}
cd
到文件目录,执行如下命令,生成sil
文件并打开:
rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
sil 代码:
class Teacher {
func teach()
func teach1()
func teach2()
func teach3()
func teach4()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () // Teacher.teach()
#Teacher.teach1: (Teacher) -> () -> () : @main.Teacher.teach1() -> () // Teacher.teach1()
#Teacher.teach2: (Teacher) -> () -> () : @main.Teacher.teach2() -> () // Teacher.teach2()
#Teacher.teach3: (Teacher) -> () -> () : @main.Teacher.teach3() -> () // Teacher.teach3()
#Teacher.teach4: (Teacher) -> () -> () : @main.Teacher.teach4() -> () // Teacher.teach4()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
以上就是Teacher
这个类的函数表。除了我们自定义的5个方法,还有init
和deinit
两个函数。
3.2 引入vtable
上面我们提到了vtable
的概念,其实是开了上帝视角,那么对于一个不知道vtable
的人该如何进行探索呢?那么我们首先向到的就是通过汇编代码,下面我们就编写如下代码:
import UIKit
class Teacher {
func teach() {}
func teach1() {}
func teach2() {}
func teach3() {}
func teach4() {}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var t = Teacher()
t.teach()
t.teach1()
t.teach2()
t.teach3()
}
}
我们使用真机环境运行,Arm64
汇编,对于函数的调用,我们首先就去找bl
相关的指令。
我们可以看到在截图中:
- 每次跳转的时候都是跳转到
x9
寄存器 -
x9
寄存器的值每次都是通过将x8
寄存器中的值读取到x9
在偏移得到的 - 每次偏移都是8字节
-
x8
寄存器中存储的值是由sp
向下偏移来的,其实就是t
对象的地址,这里就不通过汇编分析了,直接通过register read x8
里面的值,此时就可以发现确实时t
对象的地址,在左侧可以看到。
我们在blr x9
的地方按住control
点击step into
跳转进行。此时我们发现这里就是我们调用的方法,示例如下:
按照几次跳转偏移的值,我们可以看到这些方法在内存中的排列是连续的,并且地址都是基于对象的地址进行偏移的。很像一个数组的index
取值一样。其实跟表也很像了。
3.3 源码分析
根据上面的分析,我们可以想到肯定有那么一个方法,初始化了这个函数表,我们在源码中找到了initClassVTable
这个函数。
initClassVTable 源码:
/// Using the information in the class context descriptor, fill in in the
/// immediate vtable entries for the class and install overrides of any
/// superclass vtable entries.
static void initClassVTable(ClassMetadata *self) {
const auto *description = self->getDescription();
auto *classWords = reinterpret_cast(self);
if (description->hasVTable()) {
auto *vtable = description->getVTableDescriptor();
auto vtableOffset = vtable->getVTableOffset(description);
auto descriptors = description->getMethodDescriptors();
for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
auto &methodDescription = descriptors[i];
swift_ptrauth_init(&classWords[vtableOffset + i],
methodDescription.Impl.get(),
methodDescription.Flags.getExtraDiscriminator());
}
}
if (description->hasOverrideTable()) {
auto *overrideTable = description->getOverrideTable();
auto overrideDescriptors = description->getMethodOverrideDescriptors();
for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
auto &descriptor = overrideDescriptors[i];
// Get the base class and method.
auto *baseClass = cast_or_null(descriptor.Class.get());
auto *baseMethod = descriptor.Method.get();
// If the base method is null, it's an unavailable weak-linked
// symbol.
if (baseClass == nullptr || baseMethod == nullptr)
continue;
// Calculate the base method's vtable offset from the
// base method descriptor. The offset will be relative
// to the base class's vtable start offset.
auto baseClassMethods = baseClass->getMethodDescriptors();
// If the method descriptor doesn't land within the bounds of the
// method table, abort.
if (baseMethod < baseClassMethods.begin() ||
baseMethod >= baseClassMethods.end()) {
fatalError(0, "resilient vtable at %p contains out-of-bounds "
"method descriptor %p\n",
overrideTable, baseMethod);
}
// Install the method override in our vtable.
auto baseVTable = baseClass->getVTableDescriptor();
auto offset = (baseVTable->getVTableOffset(baseClass) +
(baseMethod - baseClassMethods.data()));
swift_ptrauth_init(&classWords[offset],
descriptor.Impl.get(),
baseMethod->Flags.getExtraDiscriminator());
}
}
}
在initClassVTable
函数中,主要有两部分,第一部分就是判断当前类的VTable
,第二部分就是判断继承的VTable
- 当前类的
VTable
,通过一个for循环,在VTableSize
内,取出方法存储到连续的内存中。 - 对于继承的方法,原理类似,只不过增加了继承自哪个类的一些处理
我们编写简单代码,并添加断点,进行测试,可以发现确实会调用initClassVTable
函数进行存储方法。
4. 类中函数调用的详细探索
虽然在上面我们验证了Swift
中类的方法调用是函数表调用,但是类中的发放有很多的组合,其中不乏有:
-
extension
中的方法 - 继承的方法
-
final
修饰的方法 -
class
和static
修饰的类方法 -
@objc
修饰的方法时与OC
的交互 -
dynamic
修饰的方法 -
@objc + dynamic
修饰的方法
下面我们一一探索一下。
4.1 extension 中的方法
在类中的方法会存储到VTable
中,那么我们写的extension
中的方法会存储到哪里呢?我们添加如下代码:
extension Teacher {
func teach2() {}
}
编译为sil
class Teacher {
func teach()
@objc deinit
init()
}
extension Teacher {
func teach2()
}
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () // Teacher.teach()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
我们在VTable
中并没有找到teach2
这个函数。那么extension
中的方法是如何调用的呢?我们尝试调用一下这个方法,看看汇编代码中是什么样子的。
我们可以看到,对于extension
中的函数,使用的是直接调用(静态调用)。
那么这是为什么呢?
我猜想是,由于extension
可以编写在任何地方,在编译连接的时候会有先后顺序,不可能保证把extension
中的方法也顺序的加载到VTable
中。而且像String
等系统库中的类,都是一些动态库,更不可能因为我们加了个extension
就重新将方法添加到VTable
中。
关于子类是否会继承父类的extension
中的VTable
在下一节讲述。
4.2 继承的方法
在OC
中对于继承的方法会不断的通过isa
想父类进行查找,那么Swift
中会是怎么样呢?我们编写如下代码:
class Teacher {
func teach() {}
}
class Student: Teacher {
func study() {}
}
编译后的sil
代码:
class Teacher {
func teach()
@objc deinit
init()
}
@_inheritsConvenienceInitializers class Student : Teacher {
func study()
@objc deinit
override init()
}
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () // Teacher.teach()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
sil_vtable Student {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () [inherited] // Teacher.teach()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Student.__allocating_init() -> main.Student [override] // Student.__allocating_init()
#Student.study: (Student) -> () -> () : @main.Student.study() -> () // Student.study()
#Student.deinit!deallocator: @main.Student.__deallocating_deinit // Student.__deallocating_deinit
}
我们看到在Student
的vtable
中居然有teach
方法,这就说明在Swift
中:
- 继承的时候会将父类
VTable
中的方法继承到子类的VTable
中 - 这是一种以空间换时间的方式,减少了查找的时间
那么会不会继承extension
中的方法呢?其实是不会的,下面我们来验证一下:
在刚才的代码中添加如下代码:
extension Teacher {
func teach2() {
}
}
我们直接看student
的VTable
:
sil_vtable Student {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () [inherited] // Teacher.teach()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Student.__allocating_init() -> main.Student [override] // Student.__allocating_init()
#Student.study: (Student) -> () -> () : @main.Student.study() -> () // Student.study()
#Student.deinit!deallocator: @main.Student.__deallocating_deinit // Student.__deallocating_deinit
}
此时我们并没有看到student
的VTable
中有teach2
这个函数,所以子类并不会继承父类extension
中的函数到自己的VTable
,在调用过程中依旧会是静态调用。其实原理还是那样,extension
的添加时机不确定,VTable
中的函数是顺序排列的,对于父类和子类都会存在添加到函数表时无法确定插入位置,另外也有可能存在先继承后extension
的情况。所以继承的时候extension
中的函数也是静态的。
4.3 使用final修饰的方法
测试代码:
class Teacher {
final func teach() {}
}
sil代码:
class Teacher {
final func teach()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
此时我们在VTable
中并没有找到teach
方法,其实这里使用final
修饰后就成了静态调用了。
我们调用一下,根据汇编代码的结果,成功验证了其静态调用的特性。
4.4 class和static修饰的类方法
在Swift
中我们会使用class
和static
修饰方法,使其能够直接被类调用。也就是我们常用的类方法。那么这两种方法是如何存储的呢?下面我们就来看看。
测试代码:
class Teacher {
class func eat() {
print("eat")
}
static func drink() {
print("drink")
}
}
Teacher.eat()
Teacher.drink()
print("end")
sil代码:
class Teacher {
class func eat()
static func drink()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.eat: (Teacher.Type) -> () -> () : @static main.Teacher.eat() -> () // static Teacher.eat()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
此时我们在VTable
中只看见了eat
方法,并且后面的注释是static Teacher.eat()
,所以说使用class
和static
修饰的类方法本质上都是static
,但是唯一的区别是,class
修饰的类方法存储在VTable
中,static
修饰的类方法是以静态方法的形式存储的。下面我们在通过汇编代码看一看。
这里我们可以看到,对eat
和drink
的调用都是直接地址调用。那么eat
方法也是静态存储的吗?这个目前我没能进一步验证。后续好好思考再看看,如有高见,感谢告知!谢谢!。
4.5 @objc 修饰的方法
使用@objc
修饰是将方法暴露给oc
。
添加@objc
后,编译器会提醒你引入Foundation
。
测试代码:
class Teacher {
@objc func teach() { print("teach") }
func teach1() { print("teach1") }
}
print("end")
sil代码:
class Teacher {
@objc func teach()
func teach1()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () // Teacher.teach()
#Teacher.teach1: (Teacher) -> () -> () : @main.Teacher.teach1() -> () // Teacher.teach1()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
通过sil
代码我们可以发现,使用@objc
修饰的方法依旧存在VTable
中。
特别注意:
// @objc Teacher.teach()
sil hidden [thunk] @@objc main.Teacher.teach() -> () : $@convention(objc_method) (Teacher) -> () {
// %0 // users: %4, %3, %1
bb0(%0 : $Teacher):
strong_retain %0 : $Teacher // id: %1
// function_ref Teacher.teach()
%2 = function_ref @main.Teacher.teach() -> () : $@convention(method) (@guaranteed Teacher) -> () // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed Teacher) -> () // user: %5
strong_release %0 : $Teacher // id: %4
return %3 : $() // id: %5
} // end sil function '@objc main.Teacher.teach() -> ()'
在sil
代码中我们可以看到,对于使用@objc
修饰的方法,实际上是生成了两个方法,其中一个就是我们Swift
中原有的方法,另一个就是如上面代码所示的@objc
方法,并在其内部调用了Swift
原有的方法。
所以使用@objc
修饰的方法本质是,通过sil
代码中的@objc
方法调用,Swift
中的方法来实现的。
汇编代码分析:
通过汇编代码我们也可以看到使用@objc
修饰的方法也是函数表调用。
那么通过OC
调用是什么样呢?我们新建一个OC
的命令行项目。在新建一个Test.swift
文件,此时会提示创建桥接文件,我们直接创建桥接文件,然后在.swift
文件中添加如下代码:
class Teacher {
@objc func teach() {}
}
我们来到main.m
文件中引入混编的头文件,那么这个头文件是什么呢?其实是:projectName-Swift.h
,我们点击一个swift
文件,此时如下图:
那么此时我们就可以调用声明的Swift
类了吗?答案是不可以的,如下图所示:
要想在OC
中使用Swift
中的类,其实还需要Swift
代码继承自NSObject
。继承后就不会报错了:
其实我们在桥接文件(按照上面截图中的方法找到)中也可以看到:
如果不继承自NSObject
则不会有上图所示的代码。
综上所述:
- 使用
@objc
修饰的方法是为了暴露给OC
使用 - 使用
@objc
修饰的方法依旧存储在VTable
中 - 底层实现是生成两个方法,一个普通方法,一个使用
@objc
修饰的方法,在@objc
方法中调用这个普通方法
-
- 要想在
OC
中使用Swift
类,还需其继承自NSObject
- 要想在
4.6 dynamic修饰的方法
下面我们来看看使用dynamic
修饰的方法。
测试代码:
class Teacher {
dynamic func teach() {
print("teach")
}
}
sil代码:
class Teacher {
dynamic func teach()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () // Teacher.teach()
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
通过sil
代码我们可以看到,使用dynamic
修饰的方法是存放在VTable
中的。
汇编代码:
通过汇编代码我们也可以看到时函数表调用。
目前看来跟我们直接写的方法没什么区别,那么使用dynamic
修饰的函数有什么用呢?
使用dynamic
修饰的方法也会有动态性,在Swift
中可以使用@_dynamicReplacement(for: xxx)
将使用dynamic
修饰的方法进行替换,比如:
class Teacher {
dynamic func teach() {
print("teach")
}
}
extension Teacher {
@_dynamicReplacement(for: teach)
func teach1() {
print("teach1")
}
}
此时我们调用teach
方法就会打印teach1
。如果需要被替换的方法没有使用dynamic
修饰会报编译错误。这里就不贴图了,自己试试就行。
4.7 @objc + dynamic修饰的方法
通过上面的分析我们知道在Swift
中使用@objc
修饰方法是将Swift
方法暴露给OC
,使用dynamic
修饰,可以替换方法。那么同时使用这两修饰方法呢?下面我们就来一起看看。
测试代码:
class Teacher {
@objc dynamic func teach() {
print("teach")
}
}
sil代码:
class Teacher {
@objc dynamic func teach()
@objc deinit
init()
}
sil_vtable Teacher {
#Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher // Teacher.__allocating_init()
#Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit // Teacher.__deallocating_deinit
}
此时在VTable
中就没有我们的方法了。在sil
代码中也会同时生成teach
方法和@objc teach
方法。并且在@objc teach
方法中会调用teach
方法,这与我们分析@objc
时是一致的。
汇编代码:
此时我们可以看到,这时候的方法调用已经是objc_msgSend
了,这就是Objective-C
中的消息发送流程了,关于消息发送的详细解释请看我的另一篇文章iOS Objective-C 消息的查找
关于动态性,是不是可以通过runtime
的methodSwizzling
进行方法交换呢?下面我们来验证一下。继续使用上面创建的Objective-C
工程,并新建一个OC
类,代码如下:
#import
NS_ASSUME_NONNULL_BEGIN
@interface TestObjectC : NSObject
-(void)study;
@end
NS_ASSUME_NONNULL_END
#import "TestObjectC.h"
#import
#import "TestOC-Swift.h"
@implementation TestObjectC
+ (void)load{
Method oriMethod = class_getInstanceMethod([Teacher class], @selector(teach));
Method swiMethod = class_getInstanceMethod([self class], @selector(study));
method_exchangeImplementations(oriMethod, swiMethod);
}
-(void)study{
NSLog(@"study");
}
@end
Swift代码:
import Foundation
class Teacher: NSObject {
@objc dynamic func teach() {
print("teach")
}
}
main中的代码:
#import
#import "TestOC-Swift.h"
#import "TestObjectC.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Teacher *te = [Teacher new];
[te teach];
TestObjectC *t = [TestObjectC new];
[t study];
}
return 0;
}
打印结果:
根据打印结果我们可以看到方法交换成功。其实去掉dynamic
修饰,只保留@objc
修饰也可以交换成功。所以说在Swift
中使用dynamic
修饰方法更多的作用还是为Swift
提供了方法交换的特性。
5. 总结
至此我们对Swift
中的方法的分析就到此为止了,下面总结一下:
-
Swift
中的方法调用分为静态和动态两种 - 值类型(例如
struct
)中的方法就是静态调度 - 引用类型(例如
class
)中的方法调度主要是通过函数表VTable
来进行调度,也可以说是动态调度 - 在
class
中,extension
中的方法是静态调度 - 在
class
中,使用final
修饰的方法是静态调度 - 在
class
中,使用class
和static
修饰的方法也是静态调度 - 在
class
中,虽然使用class
修饰的方法是静态调度,但是会存储在VTable
中,这点还没仔细分析。 - 在
class
中,使用@objc
修饰的方法也是函数表调度,如果需要被OC
调用还需继承自NSObject
- 在
class
中,使用@objc
修饰的方法可以在OC
中使用methodSwizzling
进行方法交换 - 在
class
中,使用dynamic
修饰的方法也是函数表调度,可以在Swift
中进行方法替换 - 在
class
中,同时使用dynamic
和@objc
修饰的方法的调度方式是objc_msgSend