Swift函數調用方式淺析

函數的調用機制
 
函數的調用機制是在函數調用時通過那種路徑走到最終調用函數地址的機制。
在編程語言中,函數的調用機制有三種
1.靜態調用:編譯期就確定了函數內存地址,執行效率最高,還可以使用編譯器優化如:inline函數內聯提升執行效率。缺點:因爲函數調用的內存地址在編譯期已經確定,則無法支持繼承等動態修改調用的方式。
2.函數表調用:每個類都有一份自己的v-table虛函數表,裏面是以函數名爲key, 函數地址爲value; 如果子類override了父類的方法,那麼這個方法名key對應的value就是那個子類重寫的新的函數地址。
3.消息發送調用:所有的函數調用最後都會轉換成一系列參數,通過消息發送的方式進行調用。這種方式最靈活,可以override重寫父類方法,可以swizzle交互類中方法的實現,可以swizzle_isa修改類的父類即:修改繼承鏈。
 
常見語言的調用機制
C++:默認使用靜態調用機制,可以通過virtural修飾成使用函數表機制調用。
Java:默認使用函數表調用機制,可以通過final修飾成直接調用機制。
Object-C:只能使用消息發送的方式進行方法調用,可以使用C代碼寫直接調用機制的代碼。
Swift:可以根據不同的修飾符,根據情況使用上面三種方式的任一方式。

Swift中的函數調用方式
Swift的調用方法非常靈活,它三種類型都支持。
首先在大的分類上分2種:Static Dispatch 和 Dynamic Dispatch。
Static Dispatch是靜態調用,調用方法的函數地址是在編譯時確定的。
Dynamic Dispatch是動態調用,調用的函數地址要在運行時才能確定。
Dynamic Dispatch動態調用又可以分爲3個子類:V-Table Dispatch, witness table dispatch, objc_msgSend。

Swift的類型分2種,值類型和引用類型。值類型包含結構體和枚舉,對於值類型中的方法調用基本都是靜態調用,執行效率非常高。
引用類型就是對象,Swift中的類型分爲是否繼承自NSObject, 原因是如果繼承NSObject,那麼對象的結構體中(類的底層實現也是一個結構體)就有了OC運行時中那一套的所有機制,isa指針,方法列表,屬性列表,協議列表等。從而函數調用就支持消息發送方式了。這也是Swift類中方法走消息發送的前提條件。

值類型-Struct
1.因爲結構體不能繼承,所以它的struct下定義的方法的調用都是靜態的。
2.它的extense下擴展的方法不能被override,走的也是靜態的。
3.調用遵守協議的方法實現就是自己定義的方法一樣。

引用類型-純Swift類
1.在Swift類中定義的方法,影響它調用方式的只有final關鍵字,正常定義的方法Swift通過一種Virtual Table的機制在運行時尋找方法的內存地址並調用。被final修飾的方法不能被override,走靜態調用。
2.它的extense下擴展的方法不能被override,走靜態調用。
3.調用遵守協議的方法實現就是自己定義的方法一樣,走V-Table虛表調用。

引用類型-Swift類繼承自NSObject
這種方式創建的Swift類,調用方式受關鍵字影響比較大。
1.被final修飾的,走靜態調用,因爲不能被override。
2.類中定義的普通方法和被@objc修飾的方法,都是走的V-Table方式調用。NSObject的子類+@objc修飾符的方法,只能表示可以讓OC類進行調用,但真正的執行機制還是走的V-Table虛表調用。
3.@objc+dynamic修飾的方法走OC的runtime消息發送。
4.Extense下的方法默認走靜態調用,因爲不能被override。如果此時被@objc和dynamic修飾,就無法走靜態,走的OC的runtime消息發送。
5.調用遵守協議的方法實現就是自己定義的方法一樣。
上面是在沒有做編譯器優化的情況下,如果做了編譯器優化,則編譯器會盡可能走靜態調用的方式,提高運行效率。
Protocal
1.當變量以當前對象的方式調用時,走的是當前對象定義方法的方式。
2.當變量以Protocal協議對象的方式調用時,走的是Witness Table
3.協議中被@objc修飾的方法,走的是runtime的消息發送

 

判斷函數是以哪種方式調用的方法
靜態調用
因爲靜態調用時,call函數的地址是固定的,根據machO文件加載到內存的內存分佈

 

所以,靜態調用時,函數地址是一個固定的,比棧變量內存地址小的內存地址。

消息發送
消息發送時,函數調用通常都會走到同一段函數內存地址中,因爲所有的函數調用都是使用同一個消息發送方法進行的。
函數表調用
函數調用的實際內存地址通常需要根據偏移量動態計算而來, 不像靜態調用和消息發送在彙編代碼裏有明顯的特徵。
可以通過彙編調試進行佐證上面的函數調用方式
查看寄存器中的值
register read/格式
register read/x

register write 寄存器名稱 數值
register write rax 0
查看具體內存地址中的值
x/數值-格式-字節大小 內存地址
x/3xw 0x000010

memory write 內存地址 數值
memory write 0x000010 10
Swift和OC在模擬器調試時,採用的彙編類型是AT&T
常用寄存器說明:
rax寄存器常用於函數傳參和返回值
rbp,rsp寄存器常用於棧數據的讀取
rip常用於指令寄存器

常用指令說明:
movq $30 %rax  //將30數據放置在rax寄存器中
leaq -0x86(%rbp) %rax //將%rbp-0x86中的內存地址放置在rax寄存器中
call -0x86(%rbp) //函數調用,下面通常有ret命令對應
jump -0x86(%rbp)  //if跳轉
常見表示說明:
0x4bdc(%rip),一般是全局變量,全局區(數據段)
-0x78(%rbp), 一般是局部變量,棧空間
0x10(%rax), 一般是堆空間

 

 

參考文章:
https://zhuanlan.zhihu.com/p/35696161
https://www.cnblogs.com/zhou--fei/p/17245908.html
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章