<optgroup id="b2bz8"><code id="b2bz8"><blockquote id="b2bz8"></blockquote></code></optgroup> <form id="b2bz8"><nobr id="b2bz8"><meter id="b2bz8"></meter></nobr></form>

    <address id="b2bz8"></address>
    <nav id="b2bz8"><strong id="b2bz8"></strong></nav>

    <address id="b2bz8"><nobr id="b2bz8"></nobr></address><menu id="b2bz8"><tt id="b2bz8"></tt></menu>
    |
    |
    51CTO旗下網站
    |
    |
    移動端

    Swift 5.0的Runtime機制淺析

    你想知道Swift內部對象是如何創建的嗎?方法以及函數調用又是如何實現的嗎?成員變量的訪問以及對象內存布局又是怎樣的嗎?這些問題都會在這篇文章中得到解答。為了更好的讓大家理解這些內部實現,我會將源代碼翻譯為用C語言表示的偽代碼來實現。

    作者:歐陽大哥2013來源:掘金|2019-07-18 13:23

    導讀:你想知道Swift內部對象是如何創建的嗎?方法以及函數調用又是如何實現的嗎?成員變量的訪問以及對象內存布局又是怎樣的嗎?這些問題都會在這篇文章中得到解答。為了更好的讓大家理解這些內部實現,我會將源代碼翻譯為用C語言表示的偽代碼來實現。

    Objective-C語言是一門以C語言為基礎的面向對象編程語言,其提供的運行時(Runtime)機制使得它也可以被認為是一種動態語言。運行時的特征之一就是對象方法的調用是在程序運行時才被確定和執行的。系統提供的開放接口使得我們可以在程序運行的時候執行方法替換以便實現一些諸如系統監控、對象行為改變、Hook等等的操作處理。然而這種開放性也存在著安全的隱患,我們可以借助Runtime在AOP層面上做一些額外的操作,而這些額外的操作因為無法進行管控, 所以有可能會輸出未知的結果。

    可能是蘋果意識到了這個問題,所以在推出的Swift語言中Runtime的能力得到了限制,甚至可以說是取消了這個能力,這就使得Swift成為了一門靜態語言。Swift語言中對象的方法調用機制和OC語言完全不同,Swift語言的對象方法調用基本上是在編譯鏈接時刻就被確定的,可以看做是一種硬編碼形式的調用實現。

    Swfit中的對象方法調用機制加快了程序的運行速度,同時減少了程序包體積的大小。但是從另外一個層面來看當編譯鏈接優化功能開啟時反而又會出現包體積增大的情況。Swift在編譯鏈接期間采用的是空間換時間的優化策略,是以提高運行速度為主要優化考慮點。具體這些我會在后面詳細談到。

    通過程序運行時匯編代碼分析Swift中的對象方法調用,發現其在Debug模式下和Release模式下的實現差異巨大。其原因是在Release模式下還同時會把編譯鏈接優化選項打開。因此更加確切的說是在編譯鏈接優化選項開啟與否的情況下二者的實現差異巨大。

    在這之前先介紹一下OC和Swift兩種語言對象方法調用的一般實現。

    OC類的對象方法調用

    對于OC語言來說對象方法調用的實現機制有很多文章都進行了深入的介紹。所有OC類中定義的方法函數的實現都隱藏了兩個參數:一個是對象本身,一個是對象方法的名稱。每次對象方法調用都會至少傳遞對象和對象方法名稱作為開始的兩個參數,方法的調用過程都會通過一個被稱為消息發送的C函數objc_msgSend來完成。objc_msgSend函數是OC對象方法調用的總引擎,這個函數內部會根據第一個參數中對象所保存的類結構信息以及第二個參數中的方法名來找到最終要調用的方法函數的地址并執行函數調用。這也是OC語言Runtime的實現機制,同時也是OC語言對多態的支持實現。整個流程就如下表述:

    Swift 5.0的Runtime機制淺析

    Swift類的對象創建和銷毀

    在Swift中可以定義兩種類:一種是從NSObject或者派生類派生的類,一類是從系統Swift基類SwiftObject派生的類。對于后者來說如果在定義類時沒有指定基類則默認會從基類SwiftObject派生。SwiftObject是一個隱藏的基類,不會在源代碼中體現。

    Swift類對象的內存布局和OC類對象的內存布局相似。二者對象的最開始部分都有一個isa成員變量指向類的描述信息。Swift類的描述信息結構繼承自OC類的描述信息,但是并沒有完全使用里面定義的屬性,對于方法的調用則主要是使用其中擴展了一個所謂的虛函數表的區域,關于這部分會在后續中詳細介紹。

    Swift類的對象實例都是在堆內存中創建,這和OC語言的對象實例創建方式相似。系統會為類提供一個默認的init構造函數,如果想自定義構造函數則需要重寫和重載init函數。一個Swift類的對象實例的構建分為兩部分:首先是進行堆內存的分配,然后才是調用init構造函數。在源代碼編寫中不會像OC語言那樣明確的分為alloc和init兩個分離的調用步驟,而是直接采用: 類名(初始化參數) 這種方式來完成對象實例的創建。在編譯時系統會為每個類的初始化方法生成一個: 模塊名.類名.__allocating_init(類名,初始化參數) 的函數,這個函數的偽代碼實現如下:

    1. //假設定義了一個CA類。 
    2. class CA { 
    3.    init(_ a:Int){} 
    4.  
    5. //編譯生成的對象內存分配創建和初始化函數代碼 
    6. CA * XXX.CA.__allocating_init(swift_class  classCA,  int a) 
    7.     CA *obj = swift_allocObject(classCA);  //分配內存。 
    8.     obj->init(a);  //調用初始化函數。 
    9.  
    10. //編譯時還會生成對象的析構和內存銷毀函數代碼 
    11. XXX.CA.__deallocating_deinit(CA *obj) 
    12.    obj->deinit()  //調用析構函數 
    13.    swift_deallocClassInstance(obj);  //銷毀對象分配的內存。 

    其中的swift_class 就是從objc_class派生出來,用于描述類信息的結構體。

    Swift對象的生命周期也和OC對象的生命周期一樣是通過引用計數來進行控制的。當對象初次創建時引用計數被設置為1,每次進行對象賦值操作都會調用swift_retain函數來增加引用計數,而每次對象不再被訪問時都會調用swift_release函數來減少引用計數。當引用計數變為0后就會調用編譯時為每個類生成的析構和銷毀函數:模塊名.類名.__deallocating_deinit(對象)。這個函數的定義實現在前面有說明。

    這就是Swift對象的創建和銷毀以及生命周期的管理過程,這些C函數都是在編譯鏈接時插入到代碼中并形成機器代碼的,整個過程對源代碼透明。下面的例子展示了對象創建和銷毀的過程。

    1. ////////Swift源代碼 
    2.  
    3. let obj1:CA = CA(20); 
    4. let obj2 = obj1 
    5.  
    6. ///////C偽代碼 
    7.  
    8. CA *obj1 = XXX.CA. __allocating_init(classCA, 20); 
    9. CA *obj2 = obj1; 
    10. swift_retain(obj1); 
    11. swift_release(obj1); 
    12. swift_release(obj2); 

    swift_release函數內部會在引用計數為0時調用 模塊名.類名.__deallocating_deinit(對象) 函數進行對象的析構和銷毀。這個函數的指針保存在swift類描述信息結構體中,以便swift_release函數內部能夠訪問得到。

    Swift類的對象方法調用

    Swift語言中對象的方法調用的實現機制和C++語言中對虛函數調用的機制是非常相似的。(需要注意的是我這里所說的調用實現只是在編譯鏈接優化選項開關在關閉的時候是這樣的,在優化開關打開時這個結論并不正確)。

    Swift語言中類定義的方法可以分為三種:OC類的派生類并且重寫了基類的方法、extension中定義的方法、類中定義的常規方法。針對這三種方法定義和實現,系統采用的處理和調用機制是完全不一樣的。

    OC類的派生類并且重寫了基類的方法

    如果在Swift中的使用了OC類,比如還在使用的UIViewController、UIView等等。并且還重寫了基類的方法,比如一定會重寫UIViewController的viewDidLoad方法。對于這些類的重寫的方法定義信息還是會保存在類的Class結構體中,而在調用上還是采用OC語言的Runtime機制來實現,即通過objc_msgSend來調用。而如果在OC派生類中定義了一個新的方法的話則實現和調用機制就不會再采用OC的Runtime機制來完成了,比如說在UIView的派生類中定義了一個新方法foo,那么這個新方法的調用和實現將與OC的Runtime機制沒有任何關系了! 它的處理和實現機制會變成我下面要說到的第三種方式。下面的Swift源代碼以及C偽代碼實現說明了這個情況:

    1. ////////Swift源代碼 
    2.  
    3. //類定義 
    4. class MyUIView:UIView { 
    5.     open func foo(){}   //常規方法 
    6.     override func layoutSubviews() {}  //重寫OC方法 
    7.  
    8. func main(){ 
    9.   let obj = MyUIView() 
    10.   obj.layoutSubviews()   //調用OC類重寫的方法 
    11.   obj.foo()   //調用常規的方法。 
    12.  
    13. ////////C偽代碼 
    14.  
    15. //...........................................運行時定義部分 
    16.  
    17. //OC類的方法結構體 
    18. struct method_t { 
    19.     SEL name
    20.     IMP imp; 
    21. }; 
    22.  
    23. //Swift類描述 
    24. struct swift_class { 
    25.     ...   //其他的屬性,因為這里不關心就不列出了。 
    26.     struct method_t  methods[1]; 
    27.     ...   //其他的屬性,因為這里不關心就不列出了。 
    28.     //虛函數表剛好在結構體的第0x50的偏移位置。 
    29.     IMP vtable[1]; 
    30. }; 
    31.  
    32.  
    33. //...........................................源代碼中類的定義和方法的定義和實現部分 
    34.  
    35. //類定義 
    36. struct MyUIView { 
    37.       struct swift_class *isa; 
    38.  
    39. //類的方法函數的實現 
    40. void layoutSubviews(id self, SEL _cmd){} 
    41. void foo(){}  //Swift類的常規方法中和源代碼的參數保持一致。 
    42.  
    43. //類的描述信息構建,這些都是在編譯代碼時就明確了并且保存在數據段中。 
    44. struct swift_class classMyUIView; 
    45. classMyUIView.methods[0] = {"layoutSubviews", &layoutSubviews}; 
    46. classMyUIView.vtable[0] = {&foo}; 
    47.  
    48.  
    49. //...........................................源代碼中程序運行的部分 
    50.  
    51. void main(){ 
    52.   MyUIView *obj = MyUIView.__allocating_init(classMyUIView); 
    53.   obj->isa = &classMyUIView; 
    54.   //OC類重寫的方法layoutSubviews調用還是用objc_msgSend來實現 
    55.   objc_msgSend(obj, @selector(layoutSubviews); 
    56.   //Swift方法調用時對象參數被放到x20寄存器中 
    57.   asm("mov x20, obj"); 
    58.   //Swift的方法foo調用采用間接調用實現 
    59.   obj->isa->vtable[0](); 

    extension中定義的方法

    如果是在Swift類的extension中定義的方法(重寫OC基類的方法除外)。那么針對這個方法的調用總是會在編譯時就決定,也就是說在調用這類對象方法時,方法調用指令中的函數地址將會以硬編碼的形式存在。在extension中定義的方法無法在運行時做任何的替換和改變!而且方法函數的符號信息都不會保存到類的描述信息中去。這也就解釋了在Swift中派生類無法重寫一個基類中extension定義的方法的原因了。因為extension中的方法調用是硬編碼完成,無法支持多態!下面的Swift源代碼以及C偽代碼實現說明了這個情況:

    1. ////////Swift源代碼 
    2.  
    3. //類定義 
    4. class CA { 
    5.     open func foo(){} 
    6.  
    7. //類的extension定義 
    8. extension CA { 
    9.    open func extfoo(){} 
    10.  
    11. func main() { 
    12.   let obj = CA() 
    13.   obj.foo() 
    14.   obj.extfoo() 
    15.  
    16. ////////C偽代碼 
    17.  
    18. //...........................................運行時定義部分 
    19.  
    20.  
    21. //Swift類描述。 
    22. struct  swift_class { 
    23.     ...   //其他的屬性,因為這里不關心就不列出了。 
    24.    //虛函數表剛好在結構體的第0x50的偏移位置。 
    25.     IMP vtable[1]; 
    26. }; 
    27.  
    28.  
    29. //...........................................源代碼中類的定義和方法的定義和實現部分 
    30.  
    31.  
    32. //類定義 
    33. struct CA { 
    34.       struct  swift_class *isa; 
    35.  
    36. //類的方法函數的實現定義 
    37. void foo(){} 
    38. //類的extension的方法函數實現定義 
    39. void extfoo(){} 
    40.  
    41. //類的描述信息構建,這些都是在編譯代碼時就明確了并且保存在數據段中。 
    42. //extension中定義的函數不會保存到虛函數表中。 
    43. struct swift_class classCA; 
    44. classCA.vtable[0] = {&foo}; 
    45.  
    46.  
    47. //...........................................源代碼中程序運行的部分 
    48.  
    49. void main(){ 
    50.   CA *obj =  CA.__allocating_init(classCA) 
    51.   obj->isa = &classCA; 
    52.   asm("mov x20, obj"); 
    53.   //Swift中常規方法foo調用采用間接調用實現 
    54.   obj->isa->vtable[0](); 
    55.   //Swift中extension方法extfoo調用直接硬編碼調用,而不是間接調用實現 
    56.   extfoo(); 

    需要注意的是extension中是可以重寫OC基類的方法,但是不能重寫Swift類中的定義的方法。具體原因根據上面的解釋就非常清楚了。

    類中定義的常規方法

    如果是在Swift中定義的常規方法,方法的調用機制和C++中的虛函數的調用機制是非常相似的。Swift為每個類都建立了一個被稱之為虛表的數組結構,這個數組會保存著類中所有定義的常規成員方法函數的地址。每個Swift類對象實例的內存布局中的第一個數據成員和OC對象相似,保存有一個類似isa的數據成員。isa中保存著Swift類的描述信息。對于Swift類的類描述結構蘋果并未公開(也許有我并不知道),類的虛函數表保存在類描述結構的第0x50個字節的偏移處,每個虛表條目中保存著一個常規方法的函數地址指針。每一個對象方法調用的源代碼在編譯時就會轉化為從虛表中取對應偏移位置的函數地址來實現間接的函數調用。下面是對于常規方法的調用Swift語言源代碼和C語言偽代碼實現:

    1. ////////Swift源代碼 
    2.  
    3. //基類定義 
    4. class CA { 
    5.   open func foo1(_ a:Int){} 
    6.   open func foo1(_ a:Int, _ b:Int){} 
    7.   open func foo2(){} 
    8.  
    9. //擴展 
    10. extension CA{ 
    11.   open func extfoo(){}  
    12.  
    13. //派生類定義 
    14. class CB:CA{ 
    15.   open func foo3(){} 
    16.   override open func foo1(_ a:Int){} 
    17.  
    18. func testfunc(_ obj:CA){ 
    19.   obj.foo1(10) 
    20.  
    21. func main() { 
    22.   let objA = A() 
    23.   objA.foo1(10) 
    24.   objA.foo1(10,20) 
    25.   objA.foo2() 
    26.   objA.extfoo() 
    27.  
    28.   let objB = B() 
    29.   objB.foo1(10) 
    30.   objB.foo1(10,20) 
    31.   objB.foo2() 
    32.   objB.foo3() 
    33.   objB.extfoo() 
    34.  
    35.   testfunc(objA) 
    36.   testfunc(objB) 
    37.  
    38. ////////C偽代碼 
    39.  
    40. //...........................................運行時定義部分 
    41.  
    42. //Swift類描述。 
    43. struct swift_class { 
    44.     ...   //其他的屬性,因為這里不關心就不列出了 
    45.     //虛函數表剛好在結構體的第0x50的偏移位置。 
    46.     IMP vtable[0]; 
    47. }; 
    48.  
    49.  
    50. //...........................................源代碼中類的定義和方法的定義和實現部分 
    51.  
    52.  
    53. //基類定義 
    54. struct CA { 
    55.       struct swift_class *isa; 
    56. }; 
    57.  
    58. //派生類定義 
    59. struct CB { 
    60.    struct swift_class *isa; 
    61. }; 
    62.  
    63. //基類CA的方法函數的實現,這里對所有方法名都進行修飾命名 
    64. void _$s3XXX2CAC4foo1yySiF(int a){}   //CA類中的foo1 
    65. void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA類中的兩個參數的foo1 
    66. void _$s3XXX2CAC4foo2yyF(){}   //CA類中的foo2 
    67. void _$s3XXX2CAC6extfooyyF(){} //CA類中的extfoo函數   
    68.  
    69. //派生類CB的方法函數的實現。 
    70. void _$s3XXX2CBC4foo1yySiF(int a){}   //CB類中的foo1,重寫了基類的方法,但是名字不一樣了。 
    71. void _$s3XXX2CBC4foo3yyF(){}             //CB類中的foo3 
    72.  
    73.  //構造基類的描述信息以及虛函數表 
    74. struct swift_class classCA; 
    75. classCA.vtable[3] = {&_$s3XXX2CAC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF}; 
    76.  
    77. //構造派生類的描述信息以及虛函數表,注意這里虛函數表會將基類的函數也添加進來而且排列在前面。 
    78. struct swift_class classCB; 
    79. classCB.vtable[4] = {&_$s3XXX2CBC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF, &_$s3XXX2CBC4foo3yyF}; 
    80.  
    81. void testfunc(A *obj){ 
    82.    obj->isa->vtable[0](10);   //間接調用實現多態的能力。 
    83.  
    84.  
    85. //...........................................源代碼中程序運行的部分 
    86.  
    87. void main(){ 
    88.    CA *objA = CA.__allocating_init(classCA); 
    89.    objA->isa = &classCA; 
    90.    asm("mov x20, objA"
    91.    objA->isa->vtable[0](10); 
    92.    objA->isa->vtable[1](10,20); 
    93.    objA->isa->vtable[2](); 
    94.    _$s3XXX2CAC6extfooyyF() 
    95.  
    96.   CB *objB = CB.__allocating_init(classCB); 
    97.   objB->isa = &classCB; 
    98.   asm("mov x20, objB"); 
    99.   objB->isa->vtable[0](10); 
    100.   objB->isa->vtable[1](10,20); 
    101.   objB->isa->vtable[2](); 
    102.   objB->isa->vtable[3](); 
    103.    _$s3XXX2CAC6extfooyyF(); 
    104.  
    105.   testfunc(objA); 
    106.   testfunc(objB); 
    107.  

    從上面的代碼中可以看出一些特點:

    Swift類的常規方法中不會再有兩個隱藏的參數了,而是和字面定義保持一致。那么問題就來了,方法調用時對象如何被引用和傳遞呢?在其他語言中一般情況下對象總是會作為方法的第一個參數,在編譯階段生成的機器碼中,將對象存放在x0這個寄存器中(本文以arm64體系結構為例)。而Swift則不同,對象不再作為第一個參數來進行傳遞了,而是在編譯階段生成的機器碼中,將對象存放在x20這個寄存器中(本文以arm64體系結構為例)。這樣設計的一個目的使得代碼更加安全。

    每一個方法調用都是通過讀取方法在虛表中的索引獲取到了方法函數的真實地址,然后再執行間接調用。在這個過程虛表索引的值是在編譯時就確定了,因此不再需要通過方法名來在運行時動態的去查找真實的地址來實現函數調用了。雖然索引的位置在編譯時確定的,但是基類和派生類虛表中相同索引處的函數的地址確可以不一致,當派生類重寫了父類的某個方法時,因為會分別生成兩個類的虛表,在相同索引位置保存不同的函數地址來實現多態的能力。

    每個方法函數名字都和源代碼中不一樣了,原因在于在編譯鏈接是系統對所有的方法名稱進行了重命名處理,這個處理稱為命名修飾。之所以這樣做是為了解決方法重載和運算符重載的問題。因為源代碼中重載的方法函數名稱都一樣只是參數和返回類型不一樣,因此無法簡單的通過名字進行區分,而只能對名字進行修飾重命名。另外一個原因是Swift還提供了命名空間的概念,也就是使得可以支持不同模塊之間是可以存在相同名稱的方法或者函數。因為整個重命名中是會帶上模塊名稱的。下面就是Swift中對類的對象方法的重命名修飾規則: _$s<模塊名長度><模塊名><類名長度><類名>C<方法名長度><方法名>yy<參數類型1>_<參數類型2>_<參數類型n>F

    就比如上面的CA類中的foo1兩個同名函數在編譯鏈接時刻就會被分別重命名為:

    1. //這里面的XXX就是你工程模塊的名稱。 
    2. void _$s3XXX2CAC4foo1yySiF(int a){}   //CA類中的foo1 
    3. void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA類中的兩個參數的foo1 

    下面這張圖就清晰的描述了Swift類的對象方法調用以及類描述信息。

    Swift 5.0的Runtime機制淺析

    Swift類中成員變量的訪問

    雖然說OC類和Swift類的對象內存布局非常相似,每個對象實例的開始部分都是一個isa數據成員指向類的描述信息,而類中定義的屬性或者變量則一般會根據定義的順序依次排列在isa的后面。OC類還會為所有成員變量,生成一張變量表信息,變量表的每個條目記錄著每個成員變量在對象內存中的偏移量。這樣在訪問對象的屬性時會通過偏移表中的偏移量來讀取偏移信息,然后再根據偏移量來讀取或設置對象的成員變量數據。在每個OC類的get和set兩個屬性方法的實現中,對于屬性在類中的偏移量值的獲取都是通過硬編碼來完成,也就是說是在編譯鏈接時刻決定的。

    對于Swift來說,對成員變量的訪問得到更加的簡化。系統會對每個成員變量生成get/set兩個函數來實現成員變量的訪問。系統不會再為類的成員變量生成變量偏移信息表,因此對于成員變量的訪問就是直接在編譯鏈接時確定成員變量在對象的偏移位置,這個偏移位置是硬編碼來確定的。下面展示Swift源代碼和C偽代碼對數據成員訪問的實現:

    1. ////////Swift源代碼 
    2.  
    3. class CA 
    4.    var a:Int = 10 
    5.    var b:Int = 20 
    6.  
    7. void main() 
    8.     let obj = CA() 
    9.     obj.b = obj.a 
    10.  
    11. ////////C偽代碼 
    12.  
    13. //...........................................運行時定義部分 
    14.  
    15. //Swift類描述。 
    16. struct swift_class { 
    17.     ...   //其他的屬性,因為這里不關心就不列出了 
    18.     //虛函數表剛好在結構體的第0x50的偏移位置。 
    19.     IMP vtable[4]; 
    20. }; 
    21.  
    22.  
    23. //...........................................源代碼中類的定義和方法的定義和實現部分 
    24.  
    25. //CA類的結構體定義也是CA類對象在內存中的布局。 
    26. struct CA 
    27.    struct swift_class *isa; 
    28.    long  reserve;   //這里的值目前總是2 
    29.    int a; 
    30.    int b; 
    31. }; 
    32.  
    33. //類CA的方法函數的實現。 
    34. int getA(){ 
    35.     struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。 
    36.     return obj->a; 
    37. void setA(int a){ 
    38.  struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。 
    39.  obj->a = a; 
    40. int getB(){ 
    41.     struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。 
    42.     return obj->b; 
    43. void setB(int b){ 
    44.  struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。 
    45.  obj->b = b; 
    46.  
    47. struct swift_class classCA; 
    48. classCA.vtable[4] = {&getA,&setA,&getB, &setB}; 
    49.  
    50.  
    51. //...........................................源代碼中程序運行的部分 
    52.  
    53. void main(){ 
    54.    CA *obj =  CA.__allocating_init(classCA); 
    55.    obj->isa = &classCA; 
    56.    obj->reserve = 2; 
    57.    obj->a = 10; 
    58.    obj->b = 20; 
    59.    asm("mov x20, obj"); 
    60.    obj->isa->vtable[2](obj->isa->vtable[0]());  // obj.b = obj.a的實現 

    從上面的代碼可以看出,Swift類會為每個定義的成員變量都生成一對get/set方法并保存到虛函數表中。所有對對象成員變量的方法的代碼都會轉化為通過虛函數表來執行get/set相對應的方法。 下面是Swift類中成員變量的實現和內存結構布局圖:

    Swift 5.0的Runtime機制淺析

    結構體中的方法

    在Swift結構體中也可以定義方法,因為結構體的內存結構中并沒有地方保存結構體的信息(不存在isa數據成員),因此結構體中的方法是不支持多態的,同時結構體中的所有方法調用都是在編譯時硬編碼來實現的。這也解釋了為什么結構體不支持派生,以及結構體中的方法不支持override關鍵字的原因。

    類的方法以及全局函數

    Swift類中定義的類方法和全局函數一樣,因為不存在對象作為參數,因此在調用此類函數時也不會存在將對象保存到x20寄存器中這么一說。同時源代碼中定義的函數的參數在編譯時也不會插入附加的參數。Swift語言會對所有符號進行重命名修飾,類方法和全局函數也不例外。這也就使得全局函數和類方法也支持名稱相同但是參數不同的函數定義。簡單的說就是類方法和全局函數就像C語言的普通函數一樣被實現和定義,所有對類方法和全局函數的調用都是在編譯鏈接時刻硬編碼為函數地址調用來處理的。

    OC調用Swift類中的方法

    如果應用程序是通過OC和Swift兩種語言混合開發完成的。那就一定會存在著OC語言代碼調用Swift語言代碼以及相反調用的情況。對于Swift語言調用OC的代碼的處理方法是系統會為工程建立一個橋聲明頭文件:項目工程名-Bridging-Header.h,所有Swift需要調用的OC語言方法都需要在這個頭文件中聲明。而對于OC語言調用Swift語言來說,則有一定的限制。因為Swift和OC的函數調用ABI規則不相同,OC語言只能創建Swift中從NSObject類中派生類對象,而方法調用則只能調用原NSObject類以及派生類中的所有方法以及被聲明為@objc關鍵字的Swift對象方法。如果需要在OC語言中調用Swift語言定義的類和方法,則需要在OC語言文件中添加:#import "項目名-Swift.h"。當某個Swift方法被聲明為@objc關鍵字時,在編譯時刻會生成兩個函數,一個是本體函數供Swift內部調用,另外一個是跳板函數(trampoline)是供OC語言進行調用的。這個跳板函數信息會記錄在OC類的運行時類結構中,跳板函數的實現會對參數的傳遞規則進行轉換:把x0寄存器的值賦值給x20寄存器,然后把其他參數依次轉化為Swift的函數參數傳遞規則要求,最后再執行本地函數調用。整個過程的實現如下:

    1. ////////Swift源代碼 
    2.  
    3. //Swift類定義 
    4. class MyUIView:UIView { 
    5.   @objc     
    6.   open func foo(){} 
    7.  
    8. func main() { 
    9.   let obj = MyUIView() 
    10.   obj.foo() 
    11.  
    12. //////// OC源代碼 
    13. #import "工程-Swift.h" 
    14.  
    15. void main() { 
    16.   MyUIView *obj = [MyUIView new]; 
    17.   [obj foo]; 
    18.  
    19. ////////C偽代碼 
    20.  
    21. //...........................................運行時定義部分 
    22.  
    23. //OC類的方法結構體 
    24. struct method_t { 
    25.     SEL name
    26.     IMP imp; 
    27. }; 
    28.  
    29. //Swift類描述 
    30. struct swift_class { 
    31.     ...   //其他的屬性,因為這里不關心就不列出了。 
    32.     struct method_t  methods[1]; 
    33.     ...   //其他的屬性,因為這里不關心就不列出了。 
    34.     //虛函數表剛好在結構體的第0x50的偏移位置。 
    35.     IMP vtable[1]; 
    36. }; 
    37.  
    38. //...........................................源代碼中類的定義和方法的定義和實現部分 
    39.  
    40. //類定義 
    41. struct MyUIView { 
    42.       struct swift_class *isa; 
    43.  
    44. //類的方法函數的實現 
    45.  
    46. //本體函數foo的實現 
    47. void foo(){} 
    48. //跳板函數的實現 
    49. void trampoline_foo(id self, SEL _cmd){ 
    50.      asm("mov x20, x0"); 
    51.      self->isa->vtable[0](); //這里調用本體函數foo 
    52.  
    53. //類的描述信息構建,這些都是在編譯代碼時就明確了并且保存在數據段中。 
    54. struct swift_class classMyUIView; 
    55. classMyUIView.methods[0] = {"foo", &trampoline_foo}; 
    56. classMyUIView.vtable[0] = {&foo}; 
    57.  
    58.  
    59. //...........................................源代碼中程序運行的部分 
    60.  
    61. //Swift代碼部分 
    62. void main() 
    63.   MyUIView *obj = MyUIView.__allocating_init(classMyUIView); 
    64.   obj->isa = &classMyUIView; 
    65.    asm("mov x20, obj"); 
    66.    //Swift方法foo的調用采用間接調用實現。 
    67.    obj->isa->vtable[0](); 
    68.  
    69. //OC代碼部分 
    70. void main() 
    71.   MyUIView *obj = objc_msgSend(objc_msgSend(classMyUIView, "alloc"), "init"); 
    72.   obj->isa = &classMyUIView; 
    73.   //OC語言對foo的調用還是用objc_msgSend來執行調用。 
    74.   //因為objc_msgSend最終會找到methods中的方法結構并調用trampoline_foo  
    75.   //而trampoline_foo內部則直接調用foo來實現真實的調用。 
    76.   objc_msgSend(obj, @selector(foo)); 

    下面的圖形展示了Swift中帶@objc關鍵字的方法實現,以及OC語言調用Swift對象方法的實現:

    Swift 5.0的Runtime機制淺析

    Swift類方法的運行時替換實現的可行性

    從上面的介紹中我們已經了解到了Swift類的常規方法定義和調用實現的機制,同樣了解到Swift對象實例的開頭部分也有和OC類似的isa數據,用來指向類的信息結構。一個令人高興的事情就是Swift類的結構定義部分是存放在可讀寫的數據段中,這似乎給了我們一個提示是說可以在運行時通過修改一個Swift類的虛函數表的內容來達到運行時對象行為改變的能力。要實現這種機制有三個難點需要解決:

    一個是Swift對內存和指針的操作進行了極大的封裝,同時Swift中也不再支持簡單直接的對內存進行操作的機制了。這樣就使得我們很難像OC那樣直接修改類結構的內存信息來進行運行時的更新處理,因為Swift不再公開運行時的相關接口了。雖然可以將方法函數名稱賦值給某個變量,但是這個變量的值并非是類方法函數的真實地址,而是一個包裝函數的地址。

    第二個就是Swift中的類方法調用和參數傳遞的ABI規則和其他語言不一致。在OC類的對象方法中,對象是作為方法函數的第一個參數傳遞的。在機器指令層面以arm64體系結構為例,對象是保存在x0寄存器作為參數進行傳遞。而在Swift的對象方法中這個規則變為對象不再作為第一個參數傳遞了,而是統一改為通過寄存器x20來進行傳遞。需要明確的是這個規則不會針對普通的Swift函數。因此當我們想將一個普通的函數來替換類定義的對象方法實現時就幾乎變得不太可能了,除非借助一些OC到Swift的橋的技術和跳板技術來實現這個功能也許能夠成功。

    當然我們也可以通過為類定義一個extension方法,然后將這個extension方法函數的指針來替換掉虛函數表中類的某個原始方法的函數指針地址,這樣能夠解決對象作為參數傳遞的寄存器的問題。但是這里仍然需要面臨兩個問題:一是如何獲取得到extension中的方法函數的地址,二是在替換完成后如何能在合適的時機調用原始的方法。

    第三是Swift語言將不再支持內嵌匯編代碼了,所以我們很難在Swift中通過匯編來寫一些跳板程序了。

    因為Swift具有比較強的靜態語言的特性,外加上函數調用的規則特點使得我們很難在運行時進行對象方法行為的改變。還有一個非常大的因素是當編譯鏈接優化開關打開時,上述的對象方法調用規則還將進一步被打破,這樣就導致我們在運行時進行對象方法行為的替換變得幾乎不可能或者不可行。

    編譯鏈接優化開啟后的Swift方法定義和調用

    一個不幸的事實是,當我們開啟了編譯鏈接的優化選項后,Swift的對象方法的調用機制做了非常大的改進。最主要的就是進一步弱化了通過虛函數表來進行間接方法調用的實現,而是大量的改用了一些內聯的方式來處理方法函數調用。同時對多態的支持也采用了一些別的策略。具體用了如下一些策略:

    大量的將函數實現換成了內聯函數模式,也就是對于大部分類中定義的源代碼比較少的方法函數都統一換成內聯。這樣對象方法的調用將不再通過虛函數表來間接調用,而是簡單粗暴的將函數的調用改為直接將內聯函數生成的機器碼進行拷貝處理。這樣的一個好處就是由于沒有函數調用的跳轉指令,而是直接執行方法中定義的指令,從而極大的加速了程序的運行速度。另外一個就是使得整個程序更加安全,因為此時函數的實現邏輯已經散布到各處了,除非惡意修改者改動了所有的指令,否則都只會影響局部程序的運行。內聯的一個的缺點就是使得整個程序的體積會增大很多。比如下面的類代碼在優化模式下的Swift語言源代碼和C語言偽代碼實現:

    1. ////////Swift源代碼 
    2.  
    3. //類定義 
    4. class CA { 
    5.   open func foo(_ a:Int, _ b:Int) ->Int { 
    6.     return a + b 
    7.   } 
    8.  
    9. func main() { 
    10.   let obj = CA() 
    11.   let a = obj.foo(10,20) 
    12.   let b = obj.foo(a, 40) 
    13.  
    14. ////////C偽代碼 
    15.  
    16.  
    17. //...........................................運行時定義部分 
    18.  
    19.  
    20. //Swift類描述。 
    21. struct swift_class { 
    22.     ...   //其他的屬性,因為這里不關心就不列出了 
    23.     //這里也沒有虛表的信息。 
    24. }; 
    25.  
    26. //...........................................源代碼中類的定義和方法的定義和實現部分 
    27.  
    28.  
    29. //類定義 
    30. struct CA { 
    31.       struct swift_class *isa; 
    32. }; 
    33.  
    34. //這里沒有方法實現,因為短方法被內聯了。 
    35.  
    36. struct swift_class classCA; 
    37.  
    38.  
    39. //...........................................源代碼中程序運行的部分 
    40.  
    41.  
    42. void main() { 
    43.   CA *obj =  CA.__allocating_init(classCA); 
    44.   obj->isa = &classCA; 
    45.   int a = 10 + 20;  //代碼被內聯優化 
    46.   int b = a + 40;   //代碼被內聯優化 

    2. 就是對多態的支持,也可能不是通過虛函數來處理了,而是通過類型判斷采用條件語句來實現方法的調用。就比如下面Swift語言源代碼和C語言偽代碼:

    1. ////////Swift源代碼 
    2.  
    3. //基類 
    4. class CA{ 
    5.    @inline(never) 
    6.    open func foo(){} 
    7.  
    8. //派生類 
    9. class CB:CA{ 
    10. @inline(never) 
    11. override open func foo(){} 
    12.  
    13. //全局函數接收對象作為參數 
    14. @inline(never) 
    15. func testfunc(_ obj:CA){ 
    16.     obj.foo() 
    17.  
    18.  
    19. func main() { 
    20.   //對象的創建以及方法調用 
    21.   let objA = CA() 
    22.   let objB = CB() 
    23.   testfunc(objA) 
    24.   testfunc(objB) 
    25.  
    1. ////////C偽代碼 
    2.  
    3. //...........................................運行時定義部分 
    4.  
    5.  
    6. //Swift類描述 
    7. struct swift_class { 
    8.     ...   //其他的屬性,因為這里不關心就不列出了 
    9.     //這里也沒有虛表的信息。 
    10. }; 
    11.  
    12.  
    13. //...........................................源代碼中類的定義和方法的定義和實現部分 
    14.  
    15. //類定義 
    16. struct CA { 
    17.       struct swift_class *isa; 
    18. }; 
    19.  
    20. struct CB { 
    21.    struct swift_class *isa; 
    22. }; 
    23.  
    24. //Swift類的方法的實現 
    25. //基類CA的foo方法實現 
    26. void fooForA(){} 
    27. //派生類CB的foo方法實現 
    28. void fooForB(){} 
    29. //全局函數方法的實現 
    30. void testfunc(CA *obj) 
    31.     //這里并不是通過虛表來進行間接調用而實現多態,而是直接硬編碼通過類型判斷來進行函數調用從而實現多態的能力。 
    32.     asm("mov x20, obj"); 
    33.     if (obj->isa == &classCA) 
    34.          fooForA(); 
    35.     else if (obj->isa == &classCB) 
    36.         fooForB(); 
    37.  
    38. //類的描述信息構建,這些都是在編譯代碼時就明確了并且保存在數據段中。 
    39. struct swift_class classCA; 
    40. struct swift_class classCB; 
    41.  
    42. //...........................................源代碼中程序運行的部分 
    43.  
    44. void main() { 
    45.   //對象實例創建以及方法調用的代碼。 
    46.   CA *objA = CA.__allocating_init(classCA); 
    47.   objA->isa = &classCA; 
    48.   CB *objB = CB.__allocating_init(classCB); 
    49.   objB->isa = &classCB; 
    50.   testfunc(objA); 
    51.   testfunc(objB); 

    也許你會覺得這不是一個最優的解決方案,而且如果當再次出現一個派生類時,還會繼續增加條件分支的判斷。 這是一個多么低級的優化啊!但是為什么還是要這么做呢?個人覺得還是性能和包大小的問題。對于性能來說如果我們通過間接調用的形式可能需要增加更多的指令以及進行間接的尋址處理和指令跳轉,而如果采用簡單的類型判斷則只需要更少的指令就可以解決多態調用的問題了,這樣性能就會得到提升。至于第二個包大小的問題這里有必要重點說一下。

    編譯鏈接優化的一個非常重要的能力就是減少程序的體積,其中一個點即是鏈接時如果發現某個一個函數沒有被任何地方調用或者引用,鏈接器就會把這個函數的實現代碼整體刪除掉。這也是符合邏輯以及正確的優化方式。回過頭來Swift函數調用的虛函數表方式,因為根據虛函數表的定義需要把一個類的所有方法函數地址都存放到類的虛函數表中,而不管類中的函數是否有被調用或者使用。而通過虛函數表的形式間接調用時是無法在編譯鏈接時明確哪個函數是否會被調用的,所以當采用虛函數表時就不得不把類中的所有方法的實現都鏈接到可執行程序中去,這樣就有可能無形中增加了程序的體積。而前面提供的當編譯鏈接優化打開后,系統盡可能的對對象的方法調用改為內聯,同時對多態的支持改為根據類型來進行條件判斷處理,這樣就可以減少對虛函數表的使用,一者加快了程序運行速度,二者刪除了程序中那些永遠不會調用的代碼從而減少程序包的體積。但是這種減少包體積的行為又因為內聯的引入也許反而增加了程序包的體積。而這二者之間的平衡對于鏈接優化器是如何決策的我們就不得而知了。

    綜上所述,在編譯器優化模式下虛函數調用的間接模式改變為直接模式了,所以我們幾乎很難在運行時通過修改虛表來實現方法調用的替換。而且Swift本身又不再支持運行時從方法名到方法實現地址的映射處理,所有的機制都是在編譯時靜態決定了。正是因為Swift語言的特性,使得原本在OC中可以做的很多事情在Swift中都難以實現,尤其是一些公司的無痕埋點日志系統的建設,APM的建設,以及各種監控系統的建設,以及模擬系統的建設都將失效,或者說需要尋找另外一些途徑去做這些事情。對于這些來說,您準備好了嗎?

    本文的結論是在Swift5中通過程序運行時觀察匯編代碼所得出的結論。為了能讓大家更好的理解,我將大部分代碼翻譯為了用C語言偽代碼來實現。因為沒有參考任何官方文檔,所以難免可能有一些錯誤的描述,歡迎大家指正批評。

    【編輯推薦】

    1. Swift面向協議編程(POP)的一些Tips
    2. 構建第一個 Swift 區塊鏈應用
    3. 完全使用Swift編寫的Telegram X即將面向所有iOS用戶開放
    4. 只需一臺蘋果電腦,開始制作自己的iPhone APP吧! (使用Swift4)
    5. Swift 5將幫助iOS 12.2應用瘦身 啟動加快
    【責任編輯:未麗燕 TEL:(010)68476606】

    點贊 0
    分享:
    大家都在看
    猜你喜歡

    訂閱專欄+更多

    16招輕松掌握PPT技巧

    16招輕松掌握PPT技巧

    GET職場加薪技能
    共16章 | 曬書包

    326人訂閱學習

    20個局域網建設改造案例

    20個局域網建設改造案例

    網絡搭建技巧
    共20章 | 捷哥CCIE

    651人訂閱學習

    WOT2019全球人工智能技術峰會

    WOT2019全球人工智能技術峰會

    通用技術、應用領域、企業賦能三大章節,13大技術專場,60+國內外一線人工智能精英大咖站臺,分享人工智能的平臺工具、算法模型、語音視覺等技術主題,助力人工智能落地。
    共50章 | WOT峰會

    0人訂閱學習

    讀 書 +更多

    系統分析師技術指南

    本書對前沿而又成熟的系統分析技術和方法進行了討論,包括CMM與過程改進、J2EE與NET平臺、中間件及相關技術、應用服務器、Web 服務、數據...

    訂閱51CTO郵刊

    點擊這里查看樣刊

    訂閱51CTO郵刊

    51CTO服務號

    51CTO播客

    日韩大片