Swift方法調用

OC作為動態語言,方法調用,是通過消息發送機制void objc_msgSend(id self, SEL cmd,…);,第一個參數是接受消息的對象;第二個是消息本身,方法的名字后面的就是消息中的那些參數。
Swift靜態語言,和OC不同,方法的調用有2種方式:靜態調用 & 動態調用。

一、靜態調用

靜態調用,即直接地址調用,調用函數指針,這個函數指針在編譯鏈接完成后就已經確定了,存放在代碼段。結構體是值類型,內部并不存放方法,因此直接通過地址直接調用。

struct Teacher{
    func teach() { }
}
var t = Teacher()
t.teach()

image.png

斷點調試,可以看到是直接地址調用。打開Mach-O,__text段,就是所謂的代碼段,需要執行的匯編指令都在這里:
image.png

那地址后面的符號(重整后方法名)地址:String Table首地址 + 偏移量
image.png

命名重整:工程名+類名+函數名,但符號表中并不存儲字符串,字符串存儲在String Table(字符串表,存放了所有的變量名函數名,以字符串形式存儲),然后根據符號表中的偏移值到字符串中查找對應的字符。

當然我們還可以通過終端查看項目符號表:

image.png

nm Mach-O:查看符號表
grep:管道輸出
xcrun swift-demangle 符號:還原符號名稱

函數重載

c語言-重整,所以無法區分同名函數,即無法方法重載。

image.png

c語言,函數名字一樣,會直接報錯。
image.png

OC中同一作用域內不允許相同函數名,嚴格意義上也不能實現函數重載,如下:

oc代碼:
- (void)eat{}
- (void)eat:(NSString *)name{}
//- (void)eat:(int)age{} //報錯:Duplicate declaration of method 'eat:'
+ (void)eat{}

Mach-O符號:
- [Animal eat]
- [Animal eat:]
//- [Animal eat:]
+ [Animal eat]

Swift通過命名重整技術,使方法名符號復雜,保證方法符號的唯一,因此允許方法重載

二、動態調用—類的方法

代碼:

class Teacher{
    func teach1() { }
    func teach2() { }
    func teach3() { }
    func teach4() { }
    func teach5() { }
}

SIL代碼:

sil_vtable Teacher {
  #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.teach5: (Teacher) -> () -> () : @main.Teacher.teach5() -> () // Teacher.teach5()
  #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類的函數表。函數表 可以理解為數組,聲明在class內部的方法在不加任何關鍵字修飾的過程中,是連續存放在我們當前的地址空間中的。
V-Table源碼:

static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  // 可以看成是Metadata地址
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    //  獲取vtable的相關信息
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    // 獲取方法描述集合
    auto descriptors = description->getMethodDescriptors();
    // &classWords[vtableOffset]可以看成是V-Table的首地址
    // 將方法描述中的方法指針按順序存儲在V-Table中
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init_code_or_data(
          &classWords[vtableOffset + i], methodDescription.getImpl(),
          methodDescription.Flags.getExtraDiscriminator(),
          !methodDescription.Flags.isAsync());
    }
  }

  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());
      //  指向原來(基類)的MethodDescriptor地址
      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.
      // 基類的MethodDescriptors
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      // 如果baseMethod不符合在基類的MethodDescriptors中間,報錯
      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();
       // 基類的vTable地址 + baseMethod在baseClassMethods的index???
      auto offset = (baseVTable- >getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));
      swift_ptrauth_init_code_or_data(&classWords[offset],
                                      descriptor.getImpl(),
                                      baseMethod->Flags.getExtraDiscriminator(),
                                      !baseMethod->Flags.isAsync());
    }
  }
}

創建方法主要分成兩部分:

  1. 獲取vtable信息,獲取方法descriptions,將方法Description的指針Imp(未重寫的)存儲在V-Table(元數據地址 + vtableOffset)中。
  2. 獲取OverrideTable信息,獲取overrideDescriptors,將description的指針Imp(重寫的)存儲在V-Table(offset )中,此處的offset為基類的vTable地址 +baseMethod在baseClassMethods的index。
    即、一個類的V-Table是由自身方法重寫方法(父類方法)組成,對比OC需要去父類去查找,Swift用空間換時間,提高了查找效率。

關于 extension,繼承方法和屬性,不能寫extension中。而extension中創建的函數,一定是只屬于自己類,但是其子類也有其訪問權限,只是不能繼承和重寫。
擴展的函數并沒有在函數表中。新建子類,通過sil一樣會發現,子類函數表中只會繼承父類函數表中的函數。為什么呢?
因為,子類將父類的函數表全部繼承了,此時子類增加函數,那么就繼續在連續的地址中insert。如果extension函數也是在函數表中,則意味著子類也有父類extension中聲明的函數,但是子類并沒有相關的指針記錄函數是父類方法還是子類方法,所以子類方法不知道該insert到哪里,導致extension中的函數無法安全的放入子類中.
所以,擴展的方法是直接(靜態)調用,并只屬于當前類,子類只能使用,無法繼承和重寫。

驗證

調用方法的地方,打上斷點,打開匯編:

企業微信截圖_929e536b-0e6b-43d8-bbf4-c5b1c3d455bc.png

觀察發現,對象指針每次偏移8字節找到方法,如[x8, #0x58]、[x8, #0x60],然后調用。而不是,直接調用方法地址的,參考原文靜態調用部分。

關于匯編,arm64匯編指令

  • mov:將某一寄存器的值復制另一寄存器(只能用于寄存器與寄存器,或者寄存器與常量之間 傳值,不能用于內存地址),
    例,mov x1, x0寄存器x0的值復制寄存器x1中;
  • ldr:將內存中的值讀取到寄存器中,
    例,ldr x0, [x1, x2]寄存器x1寄存器x2 相加作為地址,取該內存地址的值翻入寄存器x0中;
  • str:將寄存器中的值寫入到內存中,
    例,str x0, [x0, x8]寄存器x0的值 保存內存[x0 + x8]處;
  • blr:帶返回的跳轉指令,跳轉到指令后邊跟隨寄存器中保存的地址;
  • bl跳轉到某地址。

三、final、@objc、dynamic修飾函數

final 修飾的方法是 直接調度
@objc 關鍵字是將swift中的方法暴露給OC,函數表調度;
@objc + NSObject @objc修飾函數,OC還是無法調用swift方法的,因此如果想要OC訪問swift,class需要繼承NSObject;
dynamic:可以動態修改,意味著當類繼承自NSObject時,可以使用method-swizzling,走的是objc_msgSend流程,即 動態消息轉發
@_dynamicReplacement(for: 函數符號)進行方法交換。

關于方法的調度,更詳細可以參考文章 Swift 底層是怎么調度方法的

method dispatch.png

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容