OC
作為動態語言
,方法調用,是通過消息發送機制
,void objc_msgSend(id self, SEL cmd,…);
,第一個參數是接受消息的對象;第二個是消息本身,方法的名字后面的就是消息中的那些參數。
Swift
是靜態語言
,和OC不同,方法的調用有2種方式:靜態調用
& 動態調用
。
一、靜態調用
靜態調用,即直接地址調用
,調用函數指針,這個函數指針在編譯
、鏈接完成
后就已經確定了,存放在代碼段
。結構體是值類型,內部并不存放方法,因此直接通過地址直接調用。
struct Teacher{
func teach() { }
}
var t = Teacher()
t.teach()
斷點調試,可以看到是
直接地址調用
。打開Mach-O
,__text段
,就是所謂的代碼段,需要執行的匯編指令
都在這里:那地址后面的符號(重整后
方法名
)地址:String Table首地址 + 偏移量
命名重整:工程名+類名+函數名
,但符號表
中并不存儲字符串,字符串存儲在String Table
(字符串表,存放了所有的變量名
和函數名
,以字符串形式存儲),然后根據符號表中的偏移值到字符串中查找對應的字符。
當然我們還可以通過終端查看項目符號表:
nm Mach-O
:查看符號表grep
:管道輸出xcrun swift-demangle 符號
:還原符號名稱
函數重載
c語言
加-
重整,所以無法區分同名函數,即無法方法重載
。
c語言
,函數名字一樣,會直接報錯。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());
}
}
}
創建方法主要分成兩部分:
- 獲取
vtable
信息,獲取方法descriptions
,將方法Description
的指針Imp(未重寫的)存儲在V-Table(元數據地址 + vtableOffset)
中。- 獲取
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中的函數無法安全的放入子類
中.
所以,擴展的方法是直接(靜態)調用,并只屬于當前類,子類只能使用,無法繼承和重寫。
驗證
調用方法的地方,打上斷點,打開匯編:
觀察發現,
對象指針
每次偏移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