Swift5.0 的 Runtime 機制淺析

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

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

多是蘋果意識到了這個問題,因此在推出的Swift語言中Runtime的能力獲得了限制,甚至能夠說是取消了這個能力,這就使得Swift成爲了一門靜態語言。Swift語言中對象的方法調用機制和OC語言徹底不一樣,Swift語言的對象方法調用基本上是在編譯連接時刻就被肯定的,能夠看作是一種硬編碼形式的調用實現。數組

Swfit中的對象方法調用機制加快了程序的運行速度,同時減小了程序包體積的大小。可是從另一個層面來看當編譯連接優化功能開啓時反而又會出現包體積增大的狀況。Swift在編譯連接期間採用的是空間換時間的優化策略,是以提升運行速度爲主要優化考慮點。具體這些我會在後面詳細談到。安全

經過程序運行時彙編代碼分析Swift中的對象方法調用,發現其在Debug模式下和Release模式下的實現差別巨大。其緣由是在Release模式下還同時會把編譯連接優化選項打開。所以更加確切的說是在編譯連接優化選項開啓與否的狀況下兩者的實現差別巨大。bash

在這以前先介紹一下OC和Swift兩種語言對象方法調用的通常實現。編程語言

OC類的對象方法調用

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

OC方法調用流程

Swift類的對象建立和銷燬

在Swift中能夠定義兩種類:一種是從NSObject或者派生類派生的類,一類是從系統Swift基類SwiftObject派生的類。對於後者來講若是在定義類時沒有指定基類則默認會從基類SwiftObject派生。SwiftObject是一個隱藏的基類,不會在源代碼中體現。函數

Swift類對象的內存佈局和OC類對象的內存佈局類似。兩者對象的最開始部分都有一個isa成員變量指向類的描述信息。Swift類的描述信息結構繼承自OC類的描述信息,可是並無徹底使用裏面定義的屬性,對於方法的調用則主要是使用其中擴展了一個所謂的虛函數表的區域,關於這部分會在後續中詳細介紹。佈局

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

//假設定義了一個CA類。
class CA {
   init(_ a:Int){}
}
複製代碼
//編譯生成的對象內存分配建立和初始化函數代碼
CA * XXX.CA.__allocating_init(swift_class  classCA,  int a)
{
    CA *obj = swift_allocObject(classCA);  //分配內存。
    obj->init(a);  //調用初始化函數。
}

//編譯時還會生成對象的析構和內存銷燬函數代碼
XXX.CA.__deallocating_deinit(CA *obj)
{
   obj->deinit()  //調用析構函數
   swift_deallocClassInstance(obj);  //銷燬對象分配的內存。
}
複製代碼

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

Swift對象的生命週期也和OC對象的生命週期同樣是經過引用計數來進行控制的。當對象初次建立時引用計數被設置爲1,每次進行對象賦值操做都會調用swift_retain函數來增長引用計數,而每次對象再也不被訪問時都會調用swift_release函數來減小引用計數。當引用計數變爲0後就會調用編譯時爲每一個類生成的析構和銷燬函數:模塊名.類名.__deallocating_deinit(對象)。這個函數的定義實如今前面有說明。

這就是Swift對象的建立和銷燬以及生命週期的管理過程,這些C函數都是在編譯連接時插入到代碼中並造成機器代碼的,整個過程對源代碼透明。下面的例子展現了對象建立和銷燬的過程。

////////Swift源代碼

let obj1:CA = CA(20);
let obj2 = obj1

複製代碼
///////C僞代碼

CA *obj1 = XXX.CA. __allocating_init(classCA, 20);
CA *obj2 = obj1;
swift_retain(obj1);
swift_release(obj1);
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僞代碼實現說明了這個狀況:

////////Swift源代碼

//類定義
class MyUIView:UIView {
    open func foo(){}   //常規方法
    override func layoutSubviews() {}  //重寫OC方法
}

func main(){
  let obj = MyUIView()
  obj.layoutSubviews()   //調用OC類重寫的方法
  obj.foo()   //調用常規的方法。
}
複製代碼
////////C僞代碼

//...........................................運行時定義部分

//OC類的方法結構體
struct method_t {
    SEL name;
    IMP imp;
};

//Swift類描述
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了。
    struct method_t  methods[1];
    ...   //其餘的屬性,由於這裏不關心就不列出了。
    //虛函數表恰好在結構體的第0x50的偏移位置。
    IMP vtable[1];
};


//...........................................源代碼中類的定義和方法的定義和實現部分

//類定義
struct MyUIView {
      struct swift_class *isa;
}

//類的方法函數的實現
void layoutSubviews(id self, SEL _cmd){}
void foo(){}  //Swift類的常規方法中和源代碼的參數保持一致。

//類的描述信息構建,這些都是在編譯代碼時就明確了而且保存在數據段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"layoutSubviews", &layoutSubviews};
classMyUIView.vtable[0] = {&foo};


//...........................................源代碼中程序運行的部分

void main(){
  MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
  obj->isa = &classMyUIView;
  //OC類重寫的方法layoutSubviews調用仍是用objc_msgSend來實現
  objc_msgSend(obj, @selector(layoutSubviews);
  //Swift方法調用時對象參數被放到x20寄存器中
  asm("mov x20, obj");
  //Swift的方法foo調用採用間接調用實現
  obj->isa->vtable[0]();
}
複製代碼

extension中定義的方法

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

////////Swift源代碼

//類定義
class CA {
    open func foo(){}
}

//類的extension定義
extension CA {
   open func extfoo(){}
}

func main() {
  let obj = CA()
  obj.foo()
  obj.extfoo()
}
複製代碼
////////C僞代碼

//...........................................運行時定義部分


//Swift類描述。
struct  swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了。
   //虛函數表恰好在結構體的第0x50的偏移位置。
    IMP vtable[1];
};


//...........................................源代碼中類的定義和方法的定義和實現部分


//類定義
struct CA {
      struct  swift_class *isa;
}

//類的方法函數的實現定義
void foo(){}
//類的extension的方法函數實現定義
void extfoo(){}

//類的描述信息構建,這些都是在編譯代碼時就明確了而且保存在數據段中。
//extension中定義的函數不會保存到虛函數表中。
struct swift_class classCA;
classCA.vtable[0] = {&foo};


//...........................................源代碼中程序運行的部分

void main(){
  CA *obj =  CA.__allocating_init(classCA)
  obj->isa = &classCA;
  asm("mov x20, obj");
  //Swift中常規方法foo調用採用間接調用實現
  obj->isa->vtable[0]();
  //Swift中extension方法extfoo調用直接硬編碼調用,而不是間接調用實現
  extfoo();
}

複製代碼

須要注意的是extension中是能夠重寫OC基類的方法,可是不能重寫Swift類中的定義的方法。具體緣由根據上面的解釋就很是清楚了。

類中定義的常規方法

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

////////Swift源代碼

//基類定義
class CA {
  open func foo1(_ a:Int){}
  open func foo1(_ a:Int, _ b:Int){}
  open func foo2(){}
}

//擴展
extension CA{
  open func extfoo(){} 
}

//派生類定義
class CB:CA{
  open func foo3(){}
  override open func foo1(_ a:Int){}
}

func testfunc(_ obj:CA){
  obj.foo1(10)
}

func main() {
  let objA = A()
  objA.foo1(10)
  objA.foo1(10,20)
  objA.foo2()
  objA.extfoo()

  let objB = B()
  objB.foo1(10)
  objB.foo1(10,20)
  objB.foo2()
  objB.foo3()
  objB.extfoo()

  testfunc(objA)
  testfunc(objB)
}

複製代碼
////////C僞代碼

//...........................................運行時定義部分

//Swift類描述。
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了
    //虛函數表恰好在結構體的第0x50的偏移位置。
    IMP vtable[0];
};


//...........................................源代碼中類的定義和方法的定義和實現部分


//基類定義
struct CA {
      struct swift_class *isa;
};

//派生類定義
struct CB {
   struct swift_class *isa;
};

//基類CA的方法函數的實現,這裏對全部方法名都進行修飾命名
void _$s3XXX2CAC4foo1yySiF(int a){}   //CA類中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA類中的兩個參數的foo1
void _$s3XXX2CAC4foo2yyF(){}   //CA類中的foo2
void _$s3XXX2CAC6extfooyyF(){} //CA類中的extfoo函數  

//派生類CB的方法函數的實現。
void _$s3XXX2CBC4foo1yySiF(int a){}   //CB類中的foo1,重寫了基類的方法,可是名字不同了。
void _$s3XXX2CBC4foo3yyF(){}             //CB類中的foo3

 //構造基類的描述信息以及虛函數表
struct swift_class classCA;
classCA.vtable[3] = {&_$s3XXX2CAC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF};

//構造派生類的描述信息以及虛函數表,注意這裏虛函數表會將基類的函數也添加進來並且排列在前面。
struct swift_class classCB;
classCB.vtable[4] = {&_$s3XXX2CBC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF, &_$s3XXX2CBC4foo3yyF};

void testfunc(A *obj){
   obj->isa->vtable[0](10);   //間接調用實現多態的能力。
}


//...........................................源代碼中程序運行的部分

void main(){
   CA *objA = CA.__allocating_init(classCA);
   objA->isa = &classCA;
   asm("mov x20, objA")
   objA->isa->vtable[0](10);
   objA->isa->vtable[1](10,20);
   objA->isa->vtable[2]();
   _$s3XXX2CAC6extfooyyF()

  CB *objB = CB.__allocating_init(classCB);
  objB->isa = &classCB;
  asm("mov x20, objB");
  objB->isa->vtable[0](10);
  objB->isa->vtable[1](10,20);
  objB->isa->vtable[2]();
  objB->isa->vtable[3]();
   _$s3XXX2CAC6extfooyyF();

  testfunc(objA);
  testfunc(objB);

}

複製代碼

從上面的代碼中能夠看出一些特色:

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

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

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

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

//這裏面的XXX就是你工程模塊的名稱。
void _$s3XXX2CAC4foo1yySiF(int a){}   //CA類中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA類中的兩個參數的foo1
複製代碼

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

方法調用和類結構圖

Swift類中成員變量的訪問

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

對於Swift來講,對成員變量的訪問獲得更加的簡化。系統會對每一個成員變量生成get/set兩個函數來實現成員變量的訪問。系統不會再爲類的成員變量生成變量偏移信息表,所以對於成員變量的訪問就是直接在編譯連接時肯定成員變量在對象的偏移位置,這個偏移位置是硬編碼來肯定的。下面展現Swift源代碼和C僞代碼對數據成員訪問的實現:

////////Swift源代碼

class CA
{
   var a:Int = 10
   var b:Int = 20
}

void main()
{
    let obj = CA()
    obj.b = obj.a
}

複製代碼
////////C僞代碼

//...........................................運行時定義部分

//Swift類描述。
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了
    //虛函數表恰好在結構體的第0x50的偏移位置。
    IMP vtable[4];
};


//...........................................源代碼中類的定義和方法的定義和實現部分

//CA類的結構體定義也是CA類對象在內存中的佈局。
struct CA
{
   struct swift_class *isa;
   long  reserve;   //這裏的值目前老是2
   int a;
   int b;
};

//類CA的方法函數的實現。
int getA(){
    struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。
    return obj->a;
}
void setA(int a){
 struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。
 obj->a = a;
}
int getB(){
    struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。
    return obj->b;
}
void setB(int b){
 struct CA *obj = x20;   //取x20寄存器的值,也就是對象的值。
 obj->b = b;
}

struct swift_class classCA;
classCA.vtable[4] = {&getA,&setA,&getB, &setB};


//...........................................源代碼中程序運行的部分

void main(){
   CA *obj =  CA.__allocating_init(classCA);
   obj->isa = &classCA;
   obj->reserve = 2;
   obj->a = 10;
   obj->b = 20;
   asm("mov x20, obj");
   obj->isa->vtable[3](obj->isa->vtable[0]());  // obj.b = obj.a的實現
}

複製代碼

從上面的代碼能夠看出,Swift類會爲每一個定義的成員變量都生成一對get/set方法並保存到虛函數表中。全部對對象成員變量的方法的代碼都會轉化爲經過虛函數表來執行get/set相對應的方法。 下面是Swift類中成員變量的實現和內存結構佈局圖:

對象內存佈局

結構體中的方法

在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的函數參數傳遞規則要求,最後再執行本地函數調用。整個過程的實現以下:

////////Swift源代碼

//Swift類定義
class MyUIView:UIView {
  @objc    
  open func foo(){}
}

func main() {
  let obj = MyUIView()
  obj.foo()
}

//////// OC源代碼
#import "工程-Swift.h"

void main() {
  MyUIView *obj = [MyUIView new];
  [obj foo];
}
複製代碼
////////C僞代碼

//...........................................運行時定義部分

//OC類的方法結構體
struct method_t {
    SEL name;
    IMP imp;
};

//Swift類描述
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了。
    struct method_t  methods[1];
    ...   //其餘的屬性,由於這裏不關心就不列出了。
    //虛函數表恰好在結構體的第0x50的偏移位置。
    IMP vtable[1];
};

//...........................................源代碼中類的定義和方法的定義和實現部分

//類定義
struct MyUIView {
      struct swift_class *isa;
}

//類的方法函數的實現

//本體函數foo的實現
void foo(){}
//跳板函數的實現
void trampoline_foo(id self, SEL _cmd){
     asm("mov x20, x0");
     self->isa->vtable[0](); //這裏調用本體函數foo
}

//類的描述信息構建,這些都是在編譯代碼時就明確了而且保存在數據段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"foo", &trampoline_foo};
classMyUIView.vtable[0] = {&foo};


//...........................................源代碼中程序運行的部分

//Swift代碼部分
void main()
{
  MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
  obj->isa = &classMyUIView;
   asm("mov x20, obj");
   //Swift方法foo的調用採用間接調用實現。
   obj->isa->vtable[0]();
}

//OC代碼部分
void main()
{
  MyUIView *obj = objc_msgSend(objc_msgSend(classMyUIView, "alloc"), "init");
  obj->isa = &classMyUIView;
  //OC語言對foo的調用仍是用objc_msgSend來執行調用。
  //由於objc_msgSend最終會找到methods中的方法結構並調用trampoline_foo 
  //而trampoline_foo內部則直接調用foo來實現真實的調用。
  objc_msgSend(obj, @selector(foo));
}

複製代碼

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

OC調用Swift方法實現

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的對象方法的調用機制作了很是大的改進。最主要的就是進一步弱化了經過虛函數表來進行間接方法調用的實現,而是大量的改用了一些內聯的方式來處理方法函數調用。同時對多態的支持也採用了一些別的策略。具體用了以下一些策略:

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

//類定義
class CA {
  open func foo(_ a:Int, _ b:Int) ->Int {
    return a + b
  }

func main() {
  let obj = CA()
  let a = obj.foo(10,20)
  let b = obj.foo(a, 40)
}

複製代碼
////////C僞代碼


//...........................................運行時定義部分


//Swift類描述。
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了
    //這裏也沒有虛表的信息。
};

//...........................................源代碼中類的定義和方法的定義和實現部分


//類定義
struct CA {
      struct swift_class *isa;
};

//這裏沒有方法實現,由於短方法被內聯了。

struct swift_class classCA;


//...........................................源代碼中程序運行的部分


void main() {
  CA *obj =  CA.__allocating_init(classCA);
  obj->isa = &classCA;
  int a = 10 + 20;  //代碼被內聯優化
  int b = a + 40;   //代碼被內聯優化
}
複製代碼
  1. 就是對多態的支持,也可能不是經過虛函數來處理了,而是經過類型判斷採用條件語句來實現方法的調用。就好比下面Swift語言源代碼和C語言僞代碼:
////////Swift源代碼

//基類
class CA{
   @inline(never)
   open func foo(){}
}

//派生類
class CB:CA{
@inline(never)
override open func foo(){}
}

//全局函數接收對象做爲參數
@inline(never)
func testfunc(_ obj:CA){
    obj.foo()
}


func main() {
  //對象的建立以及方法調用
  let objA = CA()
  let objB = CB()
  testfunc(objA)
  testfunc(objB)
}

複製代碼
////////C僞代碼

//...........................................運行時定義部分


//Swift類描述
struct swift_class {
    ...   //其餘的屬性,由於這裏不關心就不列出了
    //這裏也沒有虛表的信息。
};


//...........................................源代碼中類的定義和方法的定義和實現部分

//類定義
struct CA {
      struct swift_class *isa;
};

struct CB {
   struct swift_class *isa;
};

//Swift類的方法的實現
//基類CA的foo方法實現
void fooForA(){}
//派生類CB的foo方法實現
void fooForB(){}
//全局函數方法的實現
void testfunc(CA *obj)
{
    //這裏並非經過虛表來進行間接調用而實現多態,而是直接硬編碼經過類型判斷來進行函數調用從而實現多態的能力。
    asm("mov x20, obj");
    if (obj->isa == &classCA)
         fooForA();
    else if (obj->isa == &classCB)
        fooForB();
}

//類的描述信息構建,這些都是在編譯代碼時就明確了而且保存在數據段中。
struct swift_class classCA;
struct swift_class classCB;

//...........................................源代碼中程序運行的部分

void main() {
  //對象實例建立以及方法調用的代碼。
  CA *objA = CA.__allocating_init(classCA);
  objA->isa = &classCA;
  CB *objB = CB.__allocating_init(classCB);
  objB->isa = &classCB;
  testfunc(objA);
  testfunc(objB);
}

複製代碼

也許你會以爲這不是一個最優的解決方案,並且若是當再次出現一個派生類時,還會繼續增長條件分支的判斷。 這是一個多麼低級的優化啊!可是爲何仍是要這麼作呢?我的以爲仍是性能和包大小的問題。對於性能來講若是咱們經過間接調用的形式可能須要增長更多的指令以及進行間接的尋址處理和指令跳轉,而若是採用簡單的類型判斷則只須要更少的指令就能夠解決多態調用的問題了,這樣性能就會獲得提高。至於第二個包大小的問題這裏有必要重點說一下。

編譯連接優化的一個很是重要的能力就是減小程序的體積,其中一個點便是連接時若是發現某個一個函數沒有被任何地方調用或者引用,連接器就會把這個函數的實現代碼總體刪除掉。這也是符合邏輯以及正確的優化方式。回過頭來Swift函數調用的虛函數表方式,由於根據虛函數表的定義須要把一個類的全部方法函數地址都存放到類的虛函數表中,而無論類中的函數是否有被調用或者使用。而經過虛函數表的形式間接調用時是沒法在編譯連接時明確哪一個函數是否會被調用的,因此當採用虛函數表時就不得不把類中的全部方法的實現都連接到可執行程序中去,這樣就有可能無形中增長了程序的體積。而前面提供的當編譯連接優化打開後,系統儘量的對對象的方法調用改成內聯,同時對多態的支持改成根據類型來進行條件判斷處理,這樣就能夠減小對虛函數表的使用,一者加快了程序運行速度,兩者刪除了程序中那些永遠不會調用的代碼從而減小程序包的體積。可是這種減小包體積的行爲又由於內聯的引入也許反而增長了程序包的體積。而這兩者之間的平衡對於連接優化器是如何決策的咱們就不得而知了。

綜上所述,在編譯器優化模式下虛函數調用的間接模式改變爲直接模式了,因此咱們幾乎很難在運行時經過修改虛表來實現方法調用的替換。並且Swift自己又再也不支持運行時從方法名到方法實現地址的映射處理,全部的機制都是在編譯時靜態決定了。正是由於Swift語言的特性,使得本來在OC中能夠作的不少事情在Swift中都難以實現,尤爲是一些公司的無痕埋點日誌系統的建設,APM的建設,以及各類監控系統的建設,以及模擬系統的建設都將失效,或者說須要尋找另一些途徑去作這些事情。對於這些來講,您準備好了嗎?

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


更多文章請關注歐陽大哥的:簡書掘金帳號