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语言伪代码来实现。由于没有参考任何官方文档,因此不免可能有一些错误的描述,欢迎你们指正批评。


更多文章请关注欧阳大哥的:简书掘金帐号

相关文章
相关标签/搜索