Swift 方法(函數)調度

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

運行結果如下:

image

我們可以看到,在這里直接是call這個地址,其實這就足以說明是直接地址調用。

2.2 通過MachOView進一步探索

2.2.1 匯編指令的查找

下面我們將可執行文件拖拽到MachOView中。

image

在MachOView`中的代碼段(__text),由于我們的代碼很少,很容易就找了這段匯編指令,如下圖所示。

image

2.2.2 符號的查找

2.1中的匯編代碼中,我們可以看到后面的注釋直接標示處理函數的符號,那么這個符號存儲在Mach-O中的什么位置呢?

首先我們來到符號表Symbol Table中查找一下:

PS: 其實還有個Dynamic Symbol Table,這是存儲動態庫中一些符號的偏移信息的,這里并不是我們要查找的目標。

image

我們知道字符串是存儲在字符串表的,其中包含變量的名稱,函數的名稱等,那么我們就可以根據符號表中的偏移值到字符串表中查找相應的字符。

image

由于代碼不多,在字符串邊也很容易就找到了我們的字符串,偏移值是2,所以排除第一空格,第二個.往后面就是我們要查找到的符號的首個字符了。

其實我們還可以通過nm命令去查找符號,首先cd 到Mach-O文件的路徑使用nm xxxx來查看當前Mach-O中的符號,比如我們這里的符號就是:

image

當然這些符號是通過命名重整的,如果想要得到和2.1中那樣的符號可以通過xcrun swift-demangle 符號這個命令進行還原符號。比如上圖中的第一個符號:(注意:要去掉前面的_$)

image

關于符號的查找,其實在release環境中并不會像上面一樣查找得到。我們更換為release環境,編譯。

我們打開可執行文件的目錄:

image

在這里多了一個dSYM文件。

我們再次將Mach-O文件拖拽到MachoView中打開,首先我們會看到如下的結果:

image

這里多了個Fat Header,并且有了x86_64ARM64兩種架構的可執行文件,這是為什么呢?其實就是release環境會包含所有支持的架構的可執行文件,詳細的介紹可以看我這篇文章Mach-O探索,下面我們打開x86_64這個可執行文件進行查找,因為我的電腦還是這個架構的,不是M1的。

我們在符號表Symbol Table繼續搜索teach,結果如下:

image

根據上圖我們可以看到并沒有搜索到任何結果,我們去字符串表(String Table)進行查看也同樣沒有teach相關的結果。其實主要原因是靜態鏈接的函數在release環境是不需要使用符號的,在編譯完成后,其地址就已經確定了,此時也不需要調試了,留著符號就會過多的占用存儲空間,增加可執行文件的體積,那么還留在符號表干嘛呢?其實對于靜態函數和一些確定的可以不需要存儲符號,但是那些不確定的符號,比如說一些動態加載的庫和函數,就需要用到符號了,所以保留符號表還是非常有用的。

對于不能確定的符號,比如說print,如果我們不調用它,就不需要加載它,在運行的時候,我們通過print打印一些數據,就會將它加載到內存,相當于懶加載,此時會通過dyldbind操作將其進行綁定,(在Xcode 12.2中我并沒有測試出來,???♂?),但是相關原理還是沒問題的,關于dyld的加載,也可以參考我的另一篇文章iOS 應用加載dyld

其實關于符號在不同語言中是不一樣的,在CObjective-C中我們是不能使用同名方法的,原因就是他們對于命名重整的規則比較簡單。

C

#include <stdio.h>

void test(){
    printf("test");
}
image

我們可以看到在C語言中命名重整就是在函數名稱前面加個_,所以對于C語言來說,如下的寫法是會報編譯錯誤的:

image

Objective-C

@interface Test: NSObject

@end

@implementation Test

-(void)oc_test{
    NSLog(@"oc_test");
}

@end
image

我們可以看到在OC中命名重整就是在使用-[類名 方法名],如果是類方法就是+[類名 方法名],所以對于OC來說,同名的類方法和對象方法是可以同時存在的,但是如下的寫法是會報編譯錯誤的:

image

對于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中查看,結果如下圖:

image

可以明顯的看出兩個的地址是不一樣的。那么這是為什么呢?其實就是iOS系統為了安全性,使用了ASLR,即地址空間配置隨機化,英文全稱是(Address space layout randomization)。

簡單來說就是利用隨機的方式配置一個數據地址空間,這樣攻擊者就不會輕易的找到特定的內存地址來進行攻擊。其實我們常見的操作系統級別都已經實現了ASLR,詳情請自行百度。下面我們就通過計算得出內存地址。

首先在iOS這邊查看ASLR需要通過image list命令。

image

這里我們得到的隨機加載首地址是:0x0000000102da8000,我們讓Mach-O中的函數地址加上ASLR,即:

0000000100007844 + 0x0000000102da8000 = 0x202DAF844

這與我們的實際調用地址還是不一樣啊?其實在Mach-O中還有個虛擬地址,在Mach-OLoad Commands段,存放了各段的虛擬內存地址和偏移量等信息。

我們打開Mach-OLoad Commands中的LC_SEGMENT_64(__PAGEZERO):

image

我們可以看到VM Address是0,VM Size0000000100000000File OffsetFile Size也是0,說明我們在符號表看到的地址是相對Mach-O中首地址做了加虛擬地址和大小的偏移的,這也是Mach-O中做的類似安全性的一種方案,所以去除掉這段虛擬的偏移就是我們想要的真實的內存地址了。

0000000100007844 + 0x0000000102da8000 - 0000000100000000 = 0x102DAF844

其實我們還可以通過image list -o -f命令直接拿到去除掉這段偏移的ASLR

image

0000000100007844 + 0x0000000002da8000 = 0x102DAF844

2.4 小結

經過上面一番的探索,總結如下:

  1. 無論是在Mac OS中的應用程序(call 0x100003e50)還是iOS真機(bl 0x102DAF844),在匯編代碼上都是直接調用一個地址
  2. 在此我們也通過Mach-O文件查找到了函數地址的存儲
  3. 還在iOS真機環境通過ASLR計算得到了真實的函數地址
  4. 所以地址調用就是直接調用,也就是靜態調用
  5. 至此,在Swift結構體struct(值類型)中的函數是靜態調用基本證明完畢

3. 動態派發

既然有靜態,那么當然就會有動態,在Swift中的引用類型數據代表類中的函數調用就是動態派發。下面我們來一起看看。

3.1 vtable 簡介

3.1.1 簡介

首先介紹一下Swift中的vtableSIL文件中的格式:

//聲明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個方法,還有initdeinit兩個函數。

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相關的指令。

image

我們可以看到在截圖中:

  • 每次跳轉的時候都是跳轉到x9寄存器
  • x9寄存器的值每次都是通過將x8寄存器中的值讀取到x9在偏移得到的
  • 每次偏移都是8字節
  • x8寄存器中存儲的值是由sp向下偏移來的,其實就是t對象的地址,這里就不通過匯編分析了,直接通過register read x8里面的值,此時就可以發現確實時t對象的地址,在左側可以看到。

我們在blr x9的地方按住control點擊step into跳轉進行。此時我們發現這里就是我們調用的方法,示例如下:

image

按照幾次跳轉偏移的值,我們可以看到這些方法在內存中的排列是連續的,并且地址都是基于對象的地址進行偏移的。很像一個數組的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

  1. 當前類的VTable,通過一個for循環,在VTableSize內,取出方法存儲到連續的內存中。
  2. 對于繼承的方法,原理類似,只不過增加了繼承自哪個類的一些處理

我們編寫簡單代碼,并添加斷點,進行測試,可以發現確實會調用initClassVTable函數進行存儲方法。

image

4. 類中函數調用的詳細探索

雖然在上面我們驗證了Swift中類的方法調用是函數表調用,但是類中的發放有很多的組合,其中不乏有:

  1. extension 中的方法
  2. 繼承的方法
  3. final 修飾的方法
  4. classstatic修飾的類方法
  5. @objc 修飾的方法時與OC的交互
  6. dynamic 修飾的方法
  7. @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中的方法是如何調用的呢?我們嘗試調用一下這個方法,看看匯編代碼中是什么樣子的。

image

我們可以看到,對于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
}

我們看到在Studentvtable中居然有teach方法,這就說明在Swift中:

  1. 繼承的時候會將父類VTable中的方法繼承到子類的VTable
  2. 這是一種以空間換時間的方式,減少了查找的時間

那么會不會繼承extension中的方法呢?其實是不會的,下面我們來驗證一下:

在剛才的代碼中添加如下代碼:

extension Teacher {
    func teach2() {
    }
}

我們直接看studentVTable

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
}

此時我們并沒有看到studentVTable中有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修飾后就成了靜態調用了。

我們調用一下,根據匯編代碼的結果,成功驗證了其靜態調用的特性。

image

4.4 class和static修飾的類方法

Swift中我們會使用classstatic修飾方法,使其能夠直接被類調用。也就是我們常用的類方法。那么這兩種方法是如何存儲的呢?下面我們就來看看。

測試代碼:

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(),所以說使用classstatic修飾的類方法本質上都是static,但是唯一的區別是,class修飾的類方法存儲在VTable中,static修飾的類方法是以靜態方法的形式存儲的。下面我們在通過匯編代碼看一看。

image

這里我們可以看到,對eatdrink的調用都是直接地址調用。那么eat方法也是靜態存儲的嗎?這個目前我沒能進一步驗證。后續好好思考再看看,如有高見,感謝告知!謝謝!。

4.5 @objc 修飾的方法

使用@objc修飾是將方法暴露給oc

image

添加@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中的方法來實現的。

匯編代碼分析:

image

通過匯編代碼我們也可以看到使用@objc修飾的方法也是函數表調用。

那么通過OC調用是什么樣呢?我們新建一個OC的命令行項目。在新建一個Test.swift文件,此時會提示創建橋接文件,我們直接創建橋接文件,然后在.swift文件中添加如下代碼:

class Teacher {
    @objc func teach() {}
}

我們來到main.m文件中引入混編的頭文件,那么這個頭文件是什么呢?其實是:projectName-Swift.h,我們點擊一個swift文件,此時如下圖:

image

那么此時我們就可以調用聲明的Swift類了嗎?答案是不可以的,如下圖所示:

image

要想在OC中使用Swift中的類,其實還需要Swift代碼繼承自NSObject。繼承后就不會報錯了:

image

其實我們在橋接文件(按照上面截圖中的方法找到)中也可以看到:

image

如果不繼承自NSObject則不會有上圖所示的代碼。

綜上所述:

  1. 使用@objc修飾的方法是為了暴露給OC使用
  2. 使用@objc修飾的方法依舊存儲在VTable
  3. 底層實現是生成兩個方法,一個普通方法,一個使用@objc修飾的方法,在@objc方法中調用這個普通方法
    1. 要想在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中的。

匯編代碼:

image

通過匯編代碼我們也可以看到時函數表調用。

目前看來跟我們直接寫的方法沒什么區別,那么使用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時是一致的。

匯編代碼:

image

此時我們可以看到,這時候的方法調用已經是objc_msgSend了,這就是Objective-C中的消息發送流程了,關于消息發送的詳細解釋請看我的另一篇文章iOS Objective-C 消息的查找

關于動態性,是不是可以通過runtimemethodSwizzling進行方法交換呢?下面我們來驗證一下。繼續使用上面創建的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;
}

打印結果:

image

根據打印結果我們可以看到方法交換成功。其實去掉dynamic修飾,只保留@objc修飾也可以交換成功。所以說在Swift中使用dynamic修飾方法更多的作用還是為Swift提供了方法交換的特性。

5. 總結

至此我們對Swift中的方法的分析就到此為止了,下面總結一下:

  1. Swift中的方法調用分為靜態和動態兩種
  2. 值類型(例如struct)中的方法就是靜態調度
  3. 引用類型(例如class)中的方法調度主要是通過函數表VTable來進行調度,也可以說是動態調度
  4. class中,extension中的方法是靜態調度
  5. class中,使用final修飾的方法是靜態調度
  6. class中,使用classstatic修飾的方法也是靜態調度
  7. class中,雖然使用class修飾的方法是靜態調度,但是會存儲在VTable中,這點還沒仔細分析。
  8. class中,使用@objc修飾的方法也是函數表調度,如果需要被OC調用還需繼承自NSObject
  9. class中,使用@objc修飾的方法可以在OC中使用methodSwizzling進行方法交換
  10. class中,使用dynamic修飾的方法也是函數表調度,可以在Swift中進行方法替換
  11. class中,同時使用dynamic@objc修飾的方法的調度方式是objc_msgSend
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容