Swift的静态派发和动态派发机制

image

原文地址:Static vs Dynamic Dispatch in Swift: A decisive choice
首发地址: Swift的静态派发和动态派发机制

参考文献:Swift的方法派发机制

如果你了解面向对象,对于 方法派发机制 应该不陌生。

基础知识

首先说下第一个结论:静态派发机制 同时支持 值类型引用类型

然而,动态派发机制仅支持 引用类型(reference types), 比如 Class 。简而言之: 对于动态性或者动态派发,我们需要用到继承特性,而这是值类型不支持的。

牢记这一点,我们接着往下看!

image

首先全面了解一下,由4种派发机制,而不是两种(静态和动态):

  1. 内联(inline) (最快)
  2. 静态派发 (Static Dispatch)
  3. 函数表派发 (Virtual Dispatch)
  4. 动态派发 (Dynamic Dispatch)(最慢)

由编译器决定应该使用哪种派发技术。当然,优先选择内联函数, 然后按需选择。

静态派发 vs 动态派发 或者 Swift vs Objective-C

Objective-C默认支持动态派发, 这种派发技术以多态的形式为开发人员提供了灵活性。比如子类可以重写父类的方法,这很棒,然而,这也是需要代价的。

动态派发以一定量的运行时开销为代价,提高了语言的灵活性。这意味着,在动态派发机制下,对于每个方法的调用,编译器必须在方法列表(witness table(虚函数表或者其他语言中的动态表))中查找执行方法的实现。编译器需要判断调用方,是选择父类的实现,还是子类的实现。而且由于所有对象的内存都是在运行时分配的,因此编译器只能在运行时执行检查。

而静态调用,则没有这个问题。在编译期的时候,编译器就知道要为某个方法调用某种实现。因此, 编译器可以执行某些优化,甚至在可能的情况下,可以将某些代码转换成inline函数,从而使整体执行速度异常快。

如何在Swift中实现动态派发和静态派发?

  1. 要实现动态派发,我们可以使用继承,重写父类的方法。另外我们可以使用dynamic关键字,并且需要在@objc关键字前面加上关键字,以便将方法公开给OC runtime使用。

  2. 要实现静态派发,我们可以使用finalstatic关键字,保证不会被覆写。

让我们继续深入下去:

注: 编译性语言有3种基础的函数派发方式: 直接派发(Direct Dispatch),函数表派发(Table Dispatch), 消息机制派发(Message Dispatch)

静态派发(或者直接派发)

如上面所说,他们和动态派发相比,非常快。编译器可以在编译期定位到函数的位置。因此,当函数被调用时,编译器能通过函数的内存地址,直接找到它的函数实现。这极大的提高了性能,可以到达类似inline的编译期优化。

动态派发

如前所述, 在这种类型的派发中,在运行时而不是编译时选择实现方法,这会增加一下性能开销。

这里也许你会有这样的疑问?既然动态派发有性能开销,我们为什么还要使用它?

image

因为它具有灵活性。实际上,大多数的OOP语言都支持动态派发,因为它允许多态。

动态派发有两种形式:

  1. 函数表派发( Table dispatch )

这种调用方式利用一个表,该表是一组函数指针,称为witness table,以查找特性方法的实现。

witness table如何工作?

  • 每个子类都有它自己的表结构
  • 对于类中每个重写的方法,都有不同的函数指针
  • 当子类添加新方法时,这些方法指针会添加在表数组的末尾
  • 最后,编译器在运行时使用此表来查找调用函数的实现

由于编译器必须从表中读取方法实现的内存地址,然后跳转到该地址,因此它需要两条附加指令,因此它比静态分派慢,但仍比消息分派快。

注意:我不太确定,但是这种特殊的派发技术可以是虚拟派发,因为它利用了虚拟表,但是我找不到具体的参考。

  1. 消息派发( Message dispatch )

这种动态派发方式是最动态的。事实上,它表现优异(省去了优化部分),目前,Cocoa框架在KVO,CoreData等很多地方在使用它。

此外,它还可以使用method swizzling, 我们可以在运行时更改函数的实现。

eg:
let original = #selector(getter: UIViewController.childForStatusBarStyle)
let swizzled = #selector(getter: UIViewController.swizzledChildForStatusBarStyle)
let originalMethod = class_getInstanceMethod(UIViewController.self, original)
let swizzled = class_getInstanceMethod(UIViewController.self, swizzled)
method_exchangeImplementations(originalMethod, swizzledMethod)

目前,Swift本身不支持这种功能,而是利用Objective-C的runtime特性,间接实现这种动态性。

要使用动态性,我们需要使用dynamic关键字。在Swift4.0之前,我们需要一起使用dynamic@objc. Swift4.0之后,我们需要表明@objc让我们的方法支持Objective-C的调用,以支持消息派发。

由于我们使用了Objective-C的runtime特性, 当一个message被发送时, runtime会去动态查找方法的实现(implemention)。这很慢,为了提供效率,我们使用缓存来尽可能的让常用的方法被快速找到。

举例:

值类型 (Value type)
struct Person {
   func isIrritating() -> Bool { }  // Static
}
extension Person {
   func canBeEasilyPissedOff() -> Bool { } // Static
}

由于structenum都是值类型, 不支持继承,编译器将他们置为静态派发下,因为他们永远不可能被子类化。

协议 (Protocol)
Protocol Animal {
   func isCute() -> Bool { } // Table
}
extension Animal {
   func  canGetAngry() -> Bool { } // Static
}

这里的重点是在extenison(扩展)里面定义的函数,使用静态派发(static dispatch)

类 (Class)
class Dog: Animal {
   func isCute() -> Bool { } // Tablel
   @objc dynamic func hoursSleep() -> Int { } // Message
}
extenison Dog {
   func canBite() -> Bool { } // Static
   @objc func goWild() { } // Message
}
final class Employee {
   func canCode() -> Bool { } // Static
}
  • 普通方法声明遵循协议的规则
  • 当我们将方法公开给Objecitve-C runtime时用@objc,使用静态派发(static dispatch)
  • 当一个类被标记为final时,该类不能被子类化,因为使用静态派发(static dispatch)
image

好吧,现在这只是我在讲,您相信我所说的一切,对吗?

现在如何证明这些方法实际上是使用我上面解释的派发技术?

为此,我们必须看一下Swift中间语言(SIL)。通过我在网上可以进行的研究,我发现有一种方法:

  1. 如果函数使用Table派发,则它会出现在vtable(或witness_table)中
sil_vtable Animal { 
#Animal.isCute!1:(Animal)->()->():main.Animal.isCute()->()// Animal.isCute()
…… 
}
  1. 如果函数使用Message Dispatch,则关键字volatile应该存在于调用中。另外,您将找到两个标记foreignobjc_method,指示使用Objective-C运行时调用了该函数。
%14 = class_method [volatile]%13:$ Dog,#Dog.goWild!1.foreign:(Dog)->()->(),$ @ convention(objc_method)(Dog)->() 
  1. 如果没有以上两种情况的证据,答案是静态派发

image

好吧,就是我这边!我计划这是一个由两篇文章组成的系列文章,而下一篇文章(现在可以在此处获得)将涉及通过测试用例进行静态和动态派发之间的性能比较。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容