14年Swift推出时的主打口号就是“快”。这也十分高调地被体现在这门语言的名字上了。其实快字主要体现在编译器运行时系统。新的Swift编译器更智能,能够识别对象和方法的调用关系以及层级关系,减少对象调用方法的查找时间;同时在内存管理上也有所提高。
首先简要回顾一下ObjC runtime的原理,ObjC使用Messaging策略——选择器向接收器发送消息,编译阶段无法得知接收器对象或类是否有对应的方法,仅将自定义的面向对象类型编译生成对应的运行时类型,即大量C结构体和C函数。程序运行时阶段,运行时系统根据对象的isa指针找到对象所属的类结构体,然后通过结合类中的缓存方法列表指针和虚函数表指针进行查找选择器对应的SEL选择器类型变量,如果找到则根据SEL变量对应的IMP指针找到方法实现。若找不到对应的方法,则会启动消息转发机制,如果仍然失败,则抛出运行时异常。
这个Messaging的过程是ObjC runtime消耗资源的一大因素。而大部分的方法调用,尤其是重复的方法调用,并没有必要次次都从头开始进行查找,消息分发、转发。而Swift一定程度上减少了这种重复的劳动。
当Swift的运行时的一个对象在调用方法时,编译器会使用类似C++的虚表("vtable")来查找方法实现。vtable是组成Swift类的一个存储该类所有方法的函数指针的数组,运行时可以简单地通过下标访问这些函数指针。于是当你的自定义代码第一次调用方法:
object.myMethod()
编译器大概会执行如下的伪代码:
methodImplementation = object->class.vtable[indexOfMyMethod];
methodImplementation();
可以看出简单地使用数组下标访问函数指针来获取方法实现代码会比消息分发的若干步骤要快捷一些。如果你的Swift方法声明是一个final关键字修饰过的:
@final func myMethod() { //lots of implementation code here }
那对其进行查找会更快,因为Swift编译器会直接访问该方法实现的地址,这就无需运行时再去查找,完全继承使用了静态语言的优势。
现在如果把上面的方法中的实现代码删除,变成一个空方法:
@final func myMethod() { //nothing here, blank }
然后再去尝试调用这个方法,Swift编译器可以第一时间发现这个方法没有实现代码,即使你写了相应的调用代码,编译器也完全不会理会,直接在完成了方法的前一句编译就结束。
通常情况下如果将某个类的实例作为方法入口参数进行传递,编译器只能够判断这个参数的类型要么是这个类,要么是这个类的子类。因此在方法调用的时候,Swift运行时会对这个类及其子类的vtable进行查找。但如果在实例进行初始化时明确了类型,编译阶段便清楚调用的方法在哪个类中,则在运行时调用时会直接跳转到该方法的实现代码中去,省去了动态查找的步骤。
以上是Swift的编译器和运行时系统对方法调用的一些优化,此外对于内存管理,Swift除了彻底抛弃MRC使用ARC外,还对对象的生命周期进行了一定的优化。
比如你使用了一个循环,循环次数是一百万次,循环内会创建一个局部的类实例,同时发送一个消息给实例对象:
for _ in 0...1000000 { obj.myMethod() }
这在ObjC中,选择器发送消息的次数也会达到一百万次。这在Swift中会有极大的不同,并且可以分成几种情况:
第一种情况,如果myMethod的方法体中没有方法实现代码,是空函数,则Swift根本不会进入循环执行方法,而是直接跳过这个循环执行下面的代码;
第二种情况,如果方法体中有方法,同时这个obj对象自创建以来除了调用过myMethod方法外,并没有被其他变量或方法使用,同时obj仅仅是在外部的函数体的花括号内被作为局部变量创建和释放,而且myMethod的作用范围也不超过obj创建和释放的范围。在ObjC的编译运行时环境下,obj依然会被发送一百万次重复的消息。
但是,在Swift编译环境下,编译器拥有足够的信息能够推断出这个obj对象只会在创建和释放之间进行一百万次的方法调用这个结论后,编译器不会分配堆内存给obj进行初始化。相反的,编译器会将obj创建在栈上。这样,作为一个局部变量,生来仅仅是为调用myMethod()一百万次,在栈上被分配内存显然会比在堆中更快更合理。
此外,Swift还有对寄存器存取方法参数选择上的优化,把本会使用寄存器来存取的ObjC的方法中默认隐式参数self和_cmd中的_cmd去掉,相当于增加了一个给自定义入口参数的使用比栈内存更快的寄存器存取的指标。