关于ios:通过-SIL-看-Swift-的方法派发

3次阅读

共计 12056 个字符,预计需要花费 31 分钟才能阅读完成。

本文作者:柯布

一、SIL 介绍

依据文档的形容,SIL (Swift Intermediate Language) 基于 SSA 模式,它针对 Swift 语言设计,是一门具备高级语义信息的两头语言。

SIL is an SSA-form IR with high-level semantic information designed to implement the Swift programming language

Swift 和 Objective-C 应用的雷同的编译架构 LLVM,LLVM 分为前端、中端和后端三局部,通过两头语言 LLVM IR 将前端和后端串联起来。swiftc 作为 Swift 语言的的编译器,负责 LLVM 前端的工作。swiftc 与其它编译器工作相似,进行词法剖析、语法分析、语义剖析后构建形象语法树(AST),而后生成 LLVM IR 交由 LLVM 的中端和后端。在这个流程当中,swiftc 相比 Objective-C 应用的 clang,swiftc 在构建实现 AST 后,生成最终的 LLVM IR 之前,退出了 SIL。

SIL 具备更全的 Swift 语言信息,能更好的对代码进行优化。对于开发者,SIL 具备良好的可读性,能够作为理解 Swift 的底层细节的一个工具。

二、生成 SIL

首先,将上面的代码,生成 SIL,来看看 SIL 里具体有什么。

// Contents.swift
class Cat {func speak() {print("喵喵")
    }
}

let cat = Cat()
cat.speak()

如何生成 SIL?通过命令行,swiftc -emit-silgen >> result.sil 来生成 SIL 文件。增加 -Onone 告知编译器不要进行任何优化,有助于咱们理解残缺的细节。

同时增加 xcrun swift-demangle 命令将符号进行还原,加强 Swift 办法名、类型等符号的可读性。

swiftc -emit-silgen -Onone Contents.swift | xcrun swift-demangle >> result.sil

生成的 SIL 次要蕴含类型的 申明和定义 代码块 函数表 三个外围局部。

申明和定义

文件的最上方,是申明和定义局部。

// 1
sil_stage raw 

// 2
import Builtin
import Swift
import SwiftShims

import Foundation

class Cat {func speak()
  @objc deinit
  init()}

@_hasStorage @_hasInitialValue let cat: Cat {get}

// 3
// cat
sil_global hidden [let] @Contents.cat : Contents.Cat : $Cat 
  1. sil_stage 分为 raw 和 canonical,两种类型,raw 示意以后的 SIL 是未经优化的,而 canonical 代表的则是优化后。raw 更适宜咱们对照代码进行剖析。
  2. 进行类型申明,和源代码差别不大。
  3. 定义了一个变量 cat,sil global 阐明这是一个全局变量,hidden 则代表以后变量只在以后模块可见。若将 Cat 和该变量申明为 public,则不会存在 hidden 关键字。
代码块

紧接着,是一系列办法和它的代码块,它们依据代码中的办法逐个生成。

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {...}
// Cat.speak()
sil hidden [ossa] @Contents.Cat.speak() -> () : $@convention(method) (@guaranteed Cat) -> () { ...}
// Cat.deinit
sil hidden [ossa] @Contents.Cat.deinit : $@convention(method) (@guaranteed Cat) -> @owned Builtin.NativeObject {...}
// Cat.__deallocating_deinit
sil hidden [ossa] @Contents.Cat.__deallocating_deinit : $@convention(method) (@owned Cat) -> () { ...}
// Cat.__allocating_init()
sil hidden [exact_self_class] [ossa] @Contents.Cat.__allocating_init() -> Contents.Cat : $@convention(method) (@thick Cat.Type) -> @owned Cat {...}
// Cat.init()
sil hidden [ossa] @Contents.Cat.init() -> Contents.Cat : $@convention(method) (@owned Cat) -> @owned Cat {...}

每个代码块上方,都正文了对应的办法名称。除了 speak 办法,还包含了 main 函数、init、deinit 等办法。

main 函数作为整个代码的入口,通过 @convention 关键字约定函数的调用形式,@convention(c) 示意应用 C 语言的的调用规定来进行调用。上面的几个办法则约定调用形式为 method,此办法调用时会将 self 作为该实例办法第一个参数。此外还有 swift/objc_method/witness_method 多种约定。

接下来开展 main 函数来看一下具体的实现。

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  // 1
  alloc_global @Contents.cat : Contents.Cat          // id: %2
  %3 = global_addr @Contents.cat : Contents.Cat : $*Cat // users: %8, %7
  %4 = metatype $@thick Cat.Type                  // user: %6
  
  // 2
  // function_ref Cat.__allocating_init()
  %5 = function_ref @Contents.Cat.__allocating_init() -> Contents.Cat : $@convention(method) (@thick Cat.Type) -> @owned Cat // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick Cat.Type) -> @owned Cat // user: %7
  store %6 to [init] %3 : $*Cat                   // id: %7
  
  // 3
  %8 = load_borrow %3 : $*Cat                     // users: %11, %10, %9
  %9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method) (@guaranteed Cat) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed Cat) -> ()
  end_borrow %8 : $Cat                            // id: %11
  
  %12 = integer_literal $Builtin.Int32, 0         // user: %13
  %13 = struct $Int32 (%12 : $Builtin.Int32)      // user: %14
  return %13 : $Int32                             // id: %14
} // end sil function 'main'

SIL 代码,通过各种指令来形成整个流程。代码整体十分易懂,咱们分为三局部来解析:

  1. 分配内存空间

    1. alloc_global 指令调配了全局变量 cat 所须要的内存空间,其类型为 Cat。
    2. 通过 global_addr 读取该变量的内存地址,存入 %3 寄存器中。
    3. metatype 指令获取 Cat 的元类型信息,存入 %4 寄存器中。
  2. 初始化实例

    1. 通过 function_ref 指令,援用了 Cat.\__allocating_init() 办法。
    2. 紧接着通过 apply 指令执行 Cat.\__allocating_init() 办法,创立出对应的实例,并存储到 %3 的内存地址上。
  3. 办法调用

    1. 在实现了全局变量 cat 创立之后,SIL 通过 load_borrow 指令从 %3 所存储的内存地址上读取对应的值。
    2. 接着应用 class_method 指令,查问实例对应的函数表,获取到须要执行的办法。
    3. 最终调用 apply 办法实现办法调用。
函数表

在整个 SIL 文件的开端,咱们能够看到函数表局部。Swift 中 class 类型最常见的办法派发形式就是通过 函数表派发,通过查问函数表里的办法后进行调用。

sil_vtable Cat {\#Cat.speak: (Cat) -> () -> () : @Contents.Cat.speak() -> ()    // Cat.speak()
  \#Cat.init!allocator: (Cat.Type) -> () -> Cat : @Contents.Cat.__allocating_init() -> Contents.Cat    // Cat.__allocating_init()
  \#Cat.deinit!deallocator: @Contents.Cat.__deallocating_deinit    // Cat.__deallocating_deinit
}

通过下面的例子,能够感触到 SIL 的可读性十分强,整体流程也十分的清晰。不必太理解具体的指令操作也能大略了解其内容。

三、Swift 办法派发

Swift 办法派发形式,有间接派发、函数表派发和动静派发三种形式。

间接派发

Swift 的一大劣势是反对值类型,值类型无奈被继承,值类型中的办法都是通过间接派发的形式进行调用。

查看以下代码:

struct Dog {func speak() {print("汪汪")
    }
}

let dog = Dog()
dog.speak()

在 SIL 中 function_ref 指令用于生成函数的援用。找到 speak 办法调用的局部,此处通过 function_ref 间接获取了 Dog.speak 办法的援用,随之调用。

// function_ref Dog.speak()
%9 = function_ref @Contents.Dog.speak() -> () : $@convention(method) (Dog) -> () // user: %10
%10 = apply %9(%8) : $@convention(method) (Dog) -> ()
// ...

能够得出结论,间接派发在 SIL 中的体现是,通过 function_ref 指令援用函数并调用。

不是只有值类型办法才进行间接派发,援用类型通过增加 final 关键字,办法也会通过间接派发的形式实现调用。此外在 extension 中实现的办法,因为无奈被重写,也是间接派发的。读者能够本人生成 SIL 后验证。

class Dog {final func speak() {//...}
}
// 或者
extension Dog {func speak() {//...} 
}
函数表派发

对于援用类型,未增加 final/dynamic 关键字且不在 extension 中实现的办法,会通过函数表派发的形式来调用办法。在下面的例子里呈现的 VTable(Virtual Method Table),就是一种函数表。SIL 应用 class_method 指令去获取对应 VTable 中的办法进行调用,此处不再反复介绍。

在 Swift 中还有另一个函数表,WTable(Witness Table 用于存储 Protocol 中定义方法。查看以下代码:

protocol Animal {func speak()
}

class Cat: Animal {func speak() {print("喵喵")
    }
}

let cat = Cat()
cat.speak()

将代码生成 SIL 之后,SIL 最底下函数表局部发现 Cat 类型多了一个 Witness Table,外面有咱们协定中定义的 speak 办法。

sil_witness_table hidden Cat: Animal module Contents {method #Animal.speak: <Self where Self : Animal> (Self) -> () -> () : @protocol witness for Contents.Animal.speak() -> () in conformance Contents.Cat : Contents.Animal in Contents    // protocol witness for Animal.speak() in conformance Cat}

可是当咱们查看 main 函数中调用的指令,仍然是通过 class_method 指令去获取办法,WTable 如同没起作用?

%9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method) (@guaranteed Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(method) (@guaranteed Cat) -> ()

这是因为 Swift 主动进行类型推导,cat 变量被推导成了 Cat 类型,而 WTable 只有类型为 Protocol 时才会应用。申明 cat 为 Animal,从新生成 SIL:

let cat: Animal = Cat()
// 注:// %3 为 cat 实例的内存地址
// %6 为 cat 实例

// 1
%7 = init_existential_addr %3 : $*Animal, $Cat  // user: %8
store %6 to [init] %7 : $*Cat                   // id: %8
%9 = open_existential_addr immutable_access %3 : $*Animal to $*@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal // users: %11, %11, %10

// 2
%10 = witness_method $@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal, #Animal.speak : <Self where Self : Animal> (Self) -> () -> (), %9 : $*@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9; user: %11
%11 = apply %10<@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal>(%9) : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9
  
  1. 类型擦除

    1. init_existential_addr 指令初始化了一个容器,该容器蕴含了实例(实现协定的对象)的援用。
    2. 通过 open_existential_addr 获取到上述容器,实现了类型擦除。在之后 SIL 拜访都是 @opened(“XXX”) Animal 这一具体的协定类型。
  2. 办法调用

    1. 通过 witness_method 查找协定的办法进行调用。
    2. 获取到的办法的调用形式为:@convention(witness_method: Animal),代表该办法是在 WTable 表中的办法,须要通过函数表派发的形式执行。

从 SIL 中能够剖析,类型会影响函数的调用形式,相比于类办法,协定办法还需创立额定的内存空间。而要判断办法是否通过函数表派发,能够由 class_method/witness_method 来判断。

音讯派发

音讯派发,也属于动静派发形式中的一种。咱们最为相熟 Objective-C 的办法都是通过音讯派发的形式进行调用的。

在 Swift 中,能够通过增加 @objc 关键字将办法裸露给 OC,但在 Swift 中调用 @objc 办法会通过音讯派发的形式来调用办法吗?通过 以下例子进行试验。

@objc
class Cat: NSObject {@objc func speak() {print("喵喵")
    }
}

let cat = Cat()
cat.speak()

// SIL
%9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method) (@guaranteed Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(method) (@guaranteed Cat) -> ()

查看 SIL 能够留神到,即时是增加 @objc 的办法,仍然是通过函数表进行派发的。那增加 @objc 关键字它的作用体现在哪?

查看办法的代码块会发现,多了一个针对 @objc 办法的代码块,而外部的实现间接援用了对应的办法,通过间接派发的形式进行调用。

// @objc Cat.speak()
sil hidden [thunk] [ossa] @@objc Contents.Cat.speak() -> () : $@convention(objc_method) (Cat) -> () {
  // ...
  %3 = function_ref @Contents.Cat.speak() -> () : $@convention(method) (@guaranteed Cat) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@guaranteed Cat) -> () // user: %7
  // ...
}

因而仅仅是增加 @objc 关键字,不会影响办法的派发形式,只是生成了一个 OC 可见的版本。

而要让 Swift 的办法在运行时以音讯派发的形式调用,还须要增加 dynamic 关键字。

// 增加 dynamic 关键字
@objc dynamic func speak() {}

// SIL
%9 = objc_method %8 : $Cat, #Cat.speak!foreign : (Cat) -> () -> (), $@convention(objc_method) (Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(objc_method) (Cat) -> ()

增加之后,办法由 objc_method 指令获取,同时办法被润饰为 @convention(objc_method),表明该办法就是一个 OC 的办法,上述流程等价于 objc_msgSend()。同时 SIL 底部的 VTable 之中不会蕴含该办法。

动静派发保留了灵活性,除了能与 Objective-C 进行交互以外,也是 @dynmaicCallable 等新个性的根底。这边不再开展。

总结

Swift 依据具体情况,别离用不同的形式进行办法派发,从 SIL 中更好的理解到这类细节:

  • 三种派发形式,别离是动态派发、函数表派发和音讯派发
  • 动态派发性能最好
  • 动静派发通过函数表查找办法,若调用协定办法还须要开拓额定的内存空间
  • 保留了 objc_msgSend 音讯派发的能力,以兼容 OC 的个性

四、解决问题

场景一:Protocol Extension

问:以下代码会输入什么?

protocol Animal {}

extension Animal {func speak() {print("adhansxkjaw")
    }
}

class Cat: Animal {func speak() {print("喵喵")
    }
}

let cat: Animal = Cat()
cat.speak() // adhansxkjaw

答案是:“adhansxkjaw”,猫猫不喵喵,胡说八道了。

问题出在哪?在 OC 中,都是通过音讯派发进行的办法调用,以 OC 的思考形式直觉上会输入“喵喵”。参考上述 Swift 派发形式的形容,此场景下的调用函数表动静派发调用十分相似,那就生成 SIL 来验证一下。

%10 = function_ref @(extension in Contents):Contents.Animal.speak() -> () : $@convention(method) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // user: %11
%11 = apply %10<@opened("FCCE8690-C00E-11ED-A4DD-56DB1A421F1A") Animal>(%9) : $@convention(method) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9

// ...

sil_witness_table hidden Cat: Animal module Contents {}

通过 SIL 咱们看到,此时的 WTable 是空的,而该办法是通过 动态派发 的形式调用的,起因是 Protocol Extension 中的办法若没有在协定中进行申明,则不会进入到 WTable 中。因为办法是在 extension 中实现的,因而通过 动态派发 的形式进行调用。要获取的正确的后果,须要将 speak 办法在协定中进行申明。

场景二:父类未实现协定办法

问:以下代码会输入什么?

protocol Animal {func speak()
}

extension Animal {func speak() {print("adhansxkjaw")
    }
}

class Cat: Animal {}

class PetCat: Cat {func speak() {print("meow~")
    }
}

let cat: Animal = PetCat()
cat.speak() // adhansxkjaw

答案仍然是:“adhansxkjaw”,继续胡说八道。

这更加反直觉了,曾经对 speak 办法进行了申明。并且实现了协定办法,怎么还是不行。生成 SIL 来看看:

%7 = init_existential_addr %3 : $*Animal, $PetCat // user: %8
store %6 to [init] %7 : $*PetCat                // id: %8
%9 = open_existential_addr immutable_access %3 : $*Animal to $*@opened("928F6BFC-C188-11ED-8314-56DB1A421F1A") Animal // users: %11, %11, %10
%10 = witness_method $@opened("928F6BFC-C188-11ED-8314-56DB1A421F1A") Animal, #Animal.speak : <Self where Self : Animal> (Self) -> () -> (), %9 : $*@opened("928F6BFC-C188-11ED-8314-56DB1A421F1A") Animal : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9; user: %11
%11 = apply %10<@opened("928F6BFC-C188-11ED-8314-56DB1A421F1A") Animal>(%9) : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9

查看 SIL 实现的调用形式,并没有什么故障,是动静派发,并且查问了 WTable 里的办法。那就看看那 WTable 吧。发现 WTable 只有其父类 Cat 的对应的函数表,没有 PetCat 的函数表。

sil_witness_table hidden Cat: Animal module Contents {method #Animal.speak: <Self where Self : Animal> (Self) -> () -> () : @protocol witness for Contents.Animal.speak() -> () in conformance Contents.Cat : Contents.Animal in Contents    // protocol witness for Animal.speak() in conformance Cat}

查阅 SIL 文档,外面形容到:

  • WTable 只会对合乎显示申明的对象生成。A witness table is emitted for every declared explicit conformance.
  • 并且还提到,SIL 只会援用父类的协定实现,若父类没有实现子类的实现则不会被援用到。If a derived class conforms to a protocol through inheritance from its base class, this is represented by an *inherited protocol conformance*, which simply references the protocol conformance for the base class.

依据文档能够解释下面的问题。解决办法:子类的实现的协定办法,父类也须要实现,才有方法被执行。

另一个问题:如果将 cat 的申明由 Animal 批改为 Cat 呢?后果还是一样的,但具体的起因稍有不同,读者能够本人尝试剖析。

场景三:OC 混编

如下代码,定义了一个 OC 的协定,该协定由 ModuleA 遵循但没有进行实现。ModuleB 进行继承后实现协定办法,此时调用协定办法能失常输入内容。因为 OC 的协定办法是通过音讯派发的形式调用的,只有 ModuleB 的办法对 OC 可见,就能够被调用到,一切正常。

@objc protocol XXXModuleProtocol: NSObjectProtocol {@objc optional func applicationDidFinishLanuch()
}

class ModuleA: NSObject, XXXModuleProtocol { }

class ModuleB: ModuleA {func applicationDidFinishLanuch() {print("ModuleB applicationDidFinishLanuch")
    }
}

// 在 OC 里调用
id<XXXModuleProtocol> module = [ModuleB new];
[module applicationDidFinishLanuch]; // ModuleB applicationDidFinishLanuch

查看 SIL,ModuleB 的 applicationDidFinishLanuch 办法即便没有增加 @objc,也生成了对应的代码块,因而该办法对 OC 可见,可能被失常执行。

// @objc ModuleB.applicationDidFinishLanuch()
sil hidden [thunk] [ossa] @@objc Contents.ModuleB.applicationDidFinishLanuch() -> () : $@convention(objc_method) (ModuleB) -> () { ...}

但在我的项目中遇到两个问题:

  • 问题一,若 ModuleA 类型应用 泛型,则 ModuleB 的 applicationDidFinishLanuch 办法不会生成的 @objc 版本的办法,因而无奈被调用。

    • 起因:因为 Swift 泛型是无奈导出到 OC 的,因而无奈主动生成 @objc 办法也合乎状况。
    • 解决:对办法手动申明 @objc,使其对 OC 可见。
  • 问题二,ModuleA 和 ModuleB 分属于两个组件库,若 ModuleA 所在的组件库为动态库,则无奈失常调用;若为源码库则一切正常。

    • 起因:具体起因未知,只能推断是编译器存在依据源码进行推断的行为。
    • 解决:对办法手动申明 @objc,使其对 OC 可见。
  • 通用的解决办法

    • 在场景二中提到,编译器对只会援用父类的实现,那在父类中实现对应的协定办法能不能行呢?
    • 通过尝试,也是可行的,增加后能失常调用到子类的协定办法。

五、总结

计算机科学畛域的任何问题都能够通过减少一个间接的中间层来 解决”,在 Swift 语言和 LLVM IR 之间,swiftc 里退出了 SIL。通过 SIL,可能对 Swift 进一步的优化。SIL 相比汇编,更容易读懂。咱们将其作为工具,理解和学习 Swift 语言的办法派发机制。也借助于 SIL 解释编码过程中遇到的问题。本文的内容只是 SIL 中对于办法调用的一小部分,欢送参考斧正。

六、参考资料

  1. 图源:https://unsplash.com/photos/LGG5P7KCziU
  2. Swift SIL 官网文档
  3. swift-c-llvm-compiler-optimization
  4. LLVM 概述——基础架构
  5. LLVM 编译流程
  6. Swift 底层是怎么调度办法的
  7. 试着读一下 SIL

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0