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 <stdio.h>
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<void **>(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<ClassDescriptor>(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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestObjectC : NSObject
-(void)study;
@end
NS_ASSUME_NONNULL_END
#import "TestObjectC.h"
#import <objc/runtime.h>
#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 <Foundation/Foundation.h>
#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