Swift中,类和构造体有许多相似之处,但也有不同本,文联合源码探索类和构造体的实质。

咱们都晓得,内存调配能够分为堆区(Heap)和栈区(Stack)。因为栈区内存是间断的,内存的调配和销毁是通过入栈和出栈操作进行的,速度远高于堆区。堆区存储高级数据类型,在数据初始化时,查找没有应用的内存,销毁时再从内存中革除,所以堆区的数据存储不肯定是间断的。并且 retain 操作不可避免要遍历堆,而Swift的堆是通过双向链表实现的,实践上能够缩小retain时的遍历,把效率进步一倍,然而还是比不过栈,所以苹果把一些放在堆里的类型改成了值类型,比方字符串、数组、字典等等。

其中,类(class)和构造体(struct)在内存调配上是不同的,根本数据类型和构造体默认调配在栈区,而类存储在堆区,且堆区数据存储不是线程平安的,在频繁的数据读写操作时,要进行加锁操作。

构造体除了属性的存储更平安、效率更高之外,其函数的派发也更高效。因为构造体的类型被 final 润饰,不能被继承,其外部函数属于动态派发,在编译期就确定了函数的执行地址,其函数的调用通过内联(inline)的形式进行优化,其内存间断,缩小了函数的寻址过程以及内存地址的偏移计算,其运行相比于动静派发更加高效。

另外,援用技术也会对类的应用效率产生耗费,所以在可选的状况下应该尽可能的应用构造体

1、类和构造体的异同

相同点:

  • 都能定义属性、办法、初始化器;
  • 都能增加extension扩大;
  • 都能遵循协定;

不同点:

  • 类是援用类型,存储在堆区;构造体是值类型,存储在栈区。
  • 类有继承个性;构造体没有。
  • 类实例能够被屡次援用,有援用计数。构造体没有援用计数,赋值都是值拷贝
  • 类有反初始化器(deinit)来开释资源。
  • 类型转换容许你在运行时检查和解释一个类实例的类型。

2、值类型 vs 援用类型

构造体是值类型,实际上,Swift 中所有的根本类型:整数,浮点数,布尔量,字符串,数组和字典,还有枚举,都是值类型,并且都以构造体的模式在后盾实现。

这意味着字符串,数组和字典在被赋值到一个新的常量或变量,或者它被传递到一个函数或办法中的时候,其实是传递了值的拷贝。这不同于 OC 的 NSString,NSArray 和 NSDictionary,他们是类,属于援用类型,赋值和传递都是援用。

值类型存储的是值,赋值时都是进行值拷贝,相互之间不会影响。而援用类型存储的是对象的内存地址,赋值时拷贝指针,都是指向同一个对象,即同一块内存空间。

构造体是值类型

struct Book {    var name: String    var high: Int    func turnToPage(page:Int) {        print("turn to page \(page)")    }}var s = Book(name: "程序员的自我涵养", high: 8)var s1 = ss1.high = 10print(s.high, s1.high) // 8 10

这段代码中初始化构造体high为18,赋值给s1时拷贝整个构造体,相当于s1是一个新的构造体,批改s1的high为10后,s的age依然是8,s和s1互不影响。

通过 lldb 调试, 也可能看出 s 和 s1 是不同的构造体. 一个在 0x100008080, 一个在 0x100008098.

(lldb) frame variable -L s0x0000000100008080: (SwiftTest.Book) s = {0x0000000100008080:   name = "程序员的自我涵养"0x0000000100008090:   high = 8}(lldb) frame variable -L s10x0000000100008098: (SwiftTest.Book) s1 = {0x0000000100008098:   name = "程序员的自我涵养"0x00000001000080a8:   high = 10}

类是援用类型

class Person {    var age: Int = 22    var name: String?    init(_ age: Int, _ name: String) {        self.age = age        self.name = name    }    func eat(food:String) {        print("eat \(food)")    }    func jump() {        print("jump")    }}var c = Person(22, "jack")var c1 = cc1.age = 30print(c.age, c1.age) // 30 30

如果是类,c1=c的时候拷贝指针,产生了一个新的援用,但都指向同一个对象,批改c1的age为30后,c的age也会变成30。

(lldb) frame variable -L cscalar: (SwiftTest.Person) c = 0x0000000100679af0 {0x0000000100679b00:   age = 300x0000000100679b08:   name = "jack"}(lldb) frame variable -L c1scalar: (SwiftTest.Person) c1 = 0x0000000100679af0 {0x0000000100679b00:   age = 300x0000000100679b08:   name = "jack"}(lldb) cat address 0x0000000100679af0address:0x0000000100679af0, (String) $R1 = "0x100679af0 heap pointer, (0x30 bytes), zone: 0x7fff8076a000"

通过lldb调试,发现类的实例 c 和 c1 实际上是同一个对象, 再通过自定义命令 address 能够得出这个对象是在 heap 堆上.

而 c 和 c1 自身是2个不同的指针, 他们外面都存的是 0x0000000100679af0 这个地址.

(lldb) po withUnsafePointer(to: &c, {print($0)})0x00000001000082980 elements(lldb) po withUnsafePointer(to: &c1, {print($0)})0x00000001000082a00 elements

3、编译过程

为了探索实质,咱们须要借助编译器的两头语言进行剖析。

clang

OC 和 C 这类语言,会应用 clang 作为编译器前端, 编译成两头语言 IR, 再交给后端 LLVM 生成可执行文件.

Clang编译过程有以下几个毛病:

  • 源代码与LLVM IR之间有微小的形象鸿沟
  • IR不适宜源码级别的剖析
  • CFG(Control Flow Graph)短少精准度
  • CFG偏离主道
  • 在CFG和IR降级中会呈现反复剖析

swiftc

为了解决这些毛病, Swift开发了专属的Swift前端编译器 swiftc , 其中最要害的就是引入 SIL。

SIL

Swift Intermediate Language,Swift高级两头语言,Swift 编译过程引入SIL有以下长处:

  • 齐全保留程序的语义
  • 既能进行代码的生成,又能进行代码剖析
  • 处在编译管线的主通道 (hot path)
  • 架起桥梁连贯源码与LLVM,缩小源码与LLVM之间的形象鸿沟

SIL会对Swift进行高级别的语意剖析和优化。像LLVM IR一样,也具备诸如Module,Function和BasicBlock之类的构造。与LLVM IR不同,它具备更丰盛的类型零碎,无关循环和错误处理的信息依然保留,并且虚函数表和类型信息以结构化模式保留。它旨在保留Swift的含意,以实现弱小的谬误检测,内存治理等高级优化。

swift编译步骤

Swift前端编译器先把Swift代码转成SIL, 再转成IR.

上面是每个步骤对应的命令和解释

// 1 Parse: 语法分析组件, 从Swift源码剖析输入形象语法树ASTswiftc main.swift -dump-parse         // 2 语义剖析组件: 对AST进行类型查看,并对其进行类型信息正文swiftc main.swift -dump-ast      // 3 SILGen组件: 生成中间体语言,未优化的 raw SIL (生SIL)// 一系列在 生 SIL上运行的,用于确定优化和诊断合格,对不合格的代码嵌入特定的语言诊断。// 这些操作肯定会执行,即便在`-Onone`选项下也不例外swiftc main.swift -emit-silgen         // 4 生成中间体语言(SIL),优化后的// 个别状况下,是否在正式SIL上运行SIL优化是可选的,这个检测能够晋升后果可执行文件的性能.// 能够通过优化级别来管制,在-Onone模式下不会执行.swiftc main.swift -emit-sil         // 5 IRGen会将正式SIL降级为 LLVM IR(.ll文件)swiftc main.swift -emit-ir                // 6 LLVM后端优化, 生成LLVM中间体语言 (.bc文件)swiftc main.swift -emit-bc                // 7 生成汇编swiftc main.swift -emit-assembly    // 8 生成二进制机器码, 编译成可执行.out文件swiftc -o main.o main.swift                

生成 sil 文件

个别咱们在剖析的时候,能够通过上面这条命令把 swift 文件间接转成 sil 文件:

swiftc -emit-sil main.swift > main.sil

上面咱们也会借助这条命令生成的 sil 进行剖析。

4、类

(1)类的暗藏基类

import Foundationclass Person {    var age: Int = 0}class Student : Person {    var no: Int = 0}print("Person superClass:", class_getSuperclass(Person.self)!)print("Student superClass:", class_getSuperclass(Student.self)!)

Swift 官网文档中指出,如果一个类没有继承,那么他就叫做基类,比方下面的 Person 就是一个基类。

但真实情况 Person 在底层会继承一个类叫做 Swift._SwiftObject , 这个类对外是暗藏的.

看一下源码中的定义:

// Source code: "SwiftObject"// Real class name: mangled "Swift._SwiftObject"#define SwiftObject _TtCs12_SwiftObject#if __has_attribute(objc_root_class)__attribute__((__objc_root_class__))#endifSWIFT_RUNTIME_EXPORT @interface SwiftObject<NSObject> { @private  Class isa; // 类类型/元类型, 寄存metadata的指针  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; //纯swift类 援用计数}

所以下面的代码中, 如果咱们打印下父类, 会发现:

Person superClass: _TtCs12_SwiftObjectStudent superClass: Person

依据源码中的宏定义:#define SwiftObject _TtCs12_SwiftObject_TtCs12_SwiftObject 就是 SwiftObject

所以,Swift 类都会隐式的继承一个基类 SwiftObject,她是 Swift 类的最终基类,相似于 OC 的 NSObject。

(2)类的初始化过程

上面剖析一下类的创立过程, 如下代码

class Human {    var name: String    init(_ name: String) {        self.name = name    }    func eat(food:String) {        print("eat \(food)")    }}var h = Human("hali")

转成sil, swiftc -emit-sil main.swift > human.sil

剖析sil文件, 能够看到如下代码, 是 __allocating_init 初始化办法

// Human.__allocating_init(_:)sil hidden [exact_self_class] @$s4main5HumanCyACSScfC : $@convention(method) (@owned String, @thick Human.Type) -> @owned Human {// %0 "name"                                      // user: %4// %1 "$metatype"bb0(%0 : $String, %1 : $@thick Human.Type):  %2 = alloc_ref $Human                           // user: %4  // function_ref Human.init(_:)  %3 = function_ref @$s4main5HumanCyACSScfc : $@convention(method) (@owned String, @owned Human) -> @owned Human // user: %4  %4 = apply %3(%0, %2) : $@convention(method) (@owned String, @owned Human) -> @owned Human // user: %5  return %4 : $Human                              // id: %5} // end sil function '$s4main5HumanCyACSScfC'

接下来在Xcode打上符号断点 __allocating_init,

调用的是 swift_allocObject 这个办法, 而如果 Human继承自NSObject, 会调用objc的 objc_allocWithZone 办法, 走OC的初始化流程.

剖析Swift源码, 搜寻 swift_allocObject, 定位到 HeapObject.cpp 文件,

外部调用 swift_slowAlloc,

至此, 通过剖析 sil, 汇编, 源代码,咱们能够得出swift对象的初始化过程如下:

__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> Malloc

(3)类的内存构造

通过下面的源码, 发现初始化办法返回的是一个 HeapObject类型的指针, 所以Swift对象的内存构造就是 HeapObject, 它有2个属性 metadatarefCounts, 它的定义如下:

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \  InlineRefCounts refCounts // 援用计数struct HeapObject {  HeapMetadata const *metadata; // 8字节  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; //64位的位域信息, 8字节, 援用计数; metadata 和 refCounts 一起形成默认16字节实例对象的内存大小    ....};

refCounts 是一个64位的位域信息, 存储援用计数。

metadata是一个HeapMetadata类型, 实质上是 TargetHeapMetadata, 咱们能够在源码中找到这个定义

using HeapMetadata = TargetHeapMetadata<InProcess>;

再点击跳转到 TargetHeapMetadata,

template <typename Runtime>struct TargetHeapMetadata : TargetMetadata<Runtime> { //继承自TargetMetadata  using HeaderType = TargetHeapMetadataHeader<Runtime>;// 上面是初始化  TargetHeapMetadata() = default;  constexpr TargetHeapMetadata(MetadataKind kind) // 纯swift    : TargetMetadata<Runtime>(kind) {}#if SWIFT_OBJC_INTEROP //和objc交互  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa) //isa    : TargetMetadata<Runtime>(isa) {}#endif};

这里能够看到, 如果是纯swift,就会给入 kind, 如果是OC就给入 isa.

再持续点击跳转剖析 TargetHeapMetadata 的父类 TargetMetadata,

/// The common structure of all type metadata.template <typename Runtime>struct TargetMetadata { //  最终基类  using StoredPointer = typename Runtime::StoredPointer;  /// The basic header type.  typedef TargetTypeMetadataHeader<Runtime> HeaderType;  constexpr TargetMetadata()    : Kind(static_cast<StoredPointer>(MetadataKind::Class)) {}  constexpr TargetMetadata(MetadataKind Kind)    : Kind(static_cast<StoredPointer>(Kind)) {}#if SWIFT_OBJC_INTEROPprotected:  constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)    : Kind(reinterpret_cast<StoredPointer>(isa)) {}#endifprivate:  /// The kind. Only valid for non-class metadata; getKind() must be used to get  /// the kind value.  StoredPointer Kind;//Kind成员变量public:    // ......  /// Get the nominal type descriptor if this metadata describes a nominal type,  /// or return null if it does not.  ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor>  getTypeContextDescriptor() const {    switch (getKind()) { // 依据 kind 辨别不同的类    case MetadataKind::Class: {      const auto cls = static_cast<const TargetClassMetadata<Runtime> *>(this);//把this强转成TargetClassMetadata类型      if (!cls->isTypeMetadata())        return nullptr;      if (cls->isArtificialSubclass())        return nullptr;      return cls->getDescription();    }    case MetadataKind::Struct:    case MetadataKind::Enum:    case MetadataKind::Optional:      return static_cast<const TargetValueMetadata<Runtime> *>(this)          ->Description;    case MetadataKind::ForeignClass:      return static_cast<const TargetForeignClassMetadata<Runtime> *>(this)          ->Description;    default:      return nullptr;    }  }    // ......};

TargetMetadata 就是最终的基类, 其中有个 Kind 的成员变量, 不同的 kind 有不同的固定值:

TargetMetadata 中依据 kind 品种强转成其它类型, 所以 这个 TargetMetadata 就是所有元类类型的最终基类.

在强转成类的时候, 强转类型是 TargetClassMetadata, TargetClassMetadata是所有类的元类的基类, 点击跳转而后剖析它的继承连如下

TargetClassMetadata : TargetAnyClassMetadata : TargetHeapMetadata : TargetMetadata

通过剖析源码, 能够得出关系图

所以综合继承链上的成员变量, 能够得出类的内存构造:

struct ClassMetadata {    var kind: Int    var superClass: Any.Type    var cacheData: (Int, Int)    var data: Int    var classFlags: Int32    var instanceAddressPoint: UInt32    var instanceSize: UInt32    var instanceAlignmentMask: UInt16    var reserved: UInt16    var classSize: UInt32    var classAddressPoint: UInt32    var Description: TargetClassDescriptor //类的形容,公有属性    var iVarDestroyer: UnsafeRawPointer}

(4)类的形容

依据下面的剖析,的构造 TargetClassMetadata 有个属性 Description

ConstTargetMetadataPointer<Runtime, TargetClassDescriptor> Description;

这个 TargetClassDescriptor 是 Swift 类的形容 ,它有个别名 ClassDescriptor

using ClassDescriptor = TargetClassDescriptor<InProcess>;

依据 ClassDescriptor 全局搜寻源码, 能够定位到一个 类 ClassContextDescriptorBuilder

// 类的Descriptor构建者, 创立 metadata 和 Descriptor 的中央  class ClassContextDescriptorBuilder    : public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,                                              ClassDecl>,      public SILVTableVisitor<ClassContextDescriptorBuilder>  {  ....    // 内存布局的赋值操作    void layout() {      super::layout(); // 父类中有一些赋值      addVTable();  // 增加 vtable      addOverrideTable();      addObjCResilientClassStubInfo();    }  ....    // 增加 vtable    void addVTable() {      if (VTableEntries.empty()) // VTableEntries 是一个数组        return;      // Only emit a method lookup function if the class is resilient      // and has a non-empty vtable.      if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal))        IGM.emitMethodLookupFunction(getType());      // 计算偏移量      auto offset = MetadataLayout->hasResilientSuperclass()                      ? MetadataLayout->getRelativeVTableOffset()                      : MetadataLayout->getStaticVTableOffset();      B.addInt32(offset / IGM.getPointerSize()); // B是Descriptor构造体, 把偏移量增加到B      B.addInt32(VTableEntries.size()); // 增加vtable的size大小            for (auto fn : VTableEntries)        emitMethodDescriptor(fn); // 遍历数组VTableEntries,增加函数指针    }    void emitMethodDescriptor(SILDeclRef fn) {      ...    }  ....  };

其中在进行内存布局的赋值操作时, 会调用父类的办法

// 父类的 layout办法    void layout() {      asImpl().computeIdentity();      super::layout();      asImpl().addName();      asImpl().addAccessFunction();      asImpl().addReflectionFieldDescriptor();      asImpl().addLayoutInfo();      asImpl().addGenericSignature();      asImpl().maybeAddResilientSuperclass();      asImpl().maybeAddMetadataInitialization();    }

而后就去调用 void addVTable() 办法增加vtable。 再联合继承连,能够剖析出 TargetClassDescriptor 的内存构造:

struct TargetClassDescriptor {       var flags: UInt32       var parent: UInt32       var name: Int32 // 类/构造体/enum 的名称      var accessFunctionPointer: Int32       var fieldDescriptor: FieldDescriptor // 属性的形容,属性信息存在这里      var superClassType: Int32       var metadataNegativeSizeInWords: UInt32       var metadataPositiveSizeInWords: UInt32       var numImmediateMembers: UInt32       var numFields: UInt32       var fieldOffsetVectorOffset: UInt32       var Offset: UInt32 // 偏移量      var size: UInt32   // V-Table的size大小      var vtable: Array  // V-Table, 函数表}

name 是类/构造体/enum 的名;

fieldDescriptor 是属性的形容;

vtable 是函数表,他是一个数组。

(5)属性的形容

FieldDescriptor 记录属性信息,它也是一个构造体

// FieldDescriptor 构造struct FieldDescriptor {  var MangledTypeName: Int32  var Superclass: Int32  var Kind: UInt16  var FieldRecordSize: UInt16 // 大小  var NumFields: UInt32 // 有多少个属性  var FieldRecords: [FieldRecord] // 记录了每个属性的信息}

FieldRecords 是存储属性信息的数组,它的元素是 FieldRecord 构造体

// FieldRecord 构造struct FieldRecord {  var Flags: UInt32 //标记位  var MangledTypeName: Int32 // 属性的类型信息  var FieldName: Int32 // 属性的名称}

(6)办法的形容

函数表 vtable 中存储着的是办法形容 TargetMethodDescriptor

struct TargetMethodDescriptor {  // 4字节, 标识办法的品种, 初始化/getter/setter等等  MethodDescriptorFlags Flags;   // 绝对地址, Offset   TargetRelativeDirectPointer<Runtime, void> Impl; };

TargetMethodDescriptor 是对办法的形容;

Flags 示意办法的品种,占据 4 个字节;

Impl 外面并不是真正的办法imp,而是一个绝对偏移量;

5、Swift办法调度

(1)Swift函数的3种派发机制

Swift有3种函数派发机制:

  • 动态派发

    是在编译期就能确定调用办法的派发形式, Swift中的动态派发间接应用函数地址.

  • 虚函数表派发 (动静派发)

    动静派发是指编译期无奈确定应该调用哪个办法,须要在运行时能力确定办法的调用, 通过虚函数表查找函数地址再调用.

  • 音讯派发

    应用objc的音讯派发机制, objc采纳了运行时objc_msgSend进行音讯派发,所以Objc的一些动静个性在Swift外面也能够被限度的应用。

动态派发相比于动静派发更快,而且动态派发还会进行内联等一些优化,缩小函数的寻址过程, 缩小内存地址的偏移计算等一系列操作,使函数的执行速度更快,性能更高。

个别状况下, 不同类型的函数调度形式如下

类型调度形式extension
值类型动态派发动态派发
函数表派发动态派发
NSObject 子类函数表派发动态派发

(2)函数寻址

通过一个案例探索 动静派发/虚函数表派发 表这种形式中, 程序是如何找到函数地址的。

class Teacher {    var age: Int = 30    var name: String = "Jack"    func teach(){        print("teach")    }    func teach1(){        print("teach1")    }    func teach2(){        print("teach2")    }}

汇编读取函数地址

一般来讲, Swift 会把所有的办法都被存在函数表(vtable)中, 咱们能够在 sil 文件中发现这个 vtable.

而后,把我的项目跑在真机上,便于剖析 arm64 汇编

    override func viewDidLoad() {        super.viewDidLoad()        let t = Teacher()        t.teach()    }

在程序中, 断点在 t.teach() 处,通过 Xcode【Debug - Debug Workflow - Always Show Disassembly】,进入汇编代码,单步命令 si 走到 blr x8 处,这一行汇编就是在调用 teach() 函数。(bl 和 blr 都是汇编中跳转到函数执行的命令)

此时,x8寄存器中存储的就是 teach() 函数的地址,读取寄存器汇中的值,register read x8 ,就失去 teach() 函数的地址:<u>0x100086e24</u>

函数是如何寻址的?

为了节俭存储空间,Swift 大量使用了偏移量来间接寻址。

在类的形容 TargetClassDescriptor 的开始到 vtable 之间的有 13 * 4 = 52 字节,而 vtable 数组存储的是办法形容 TargetMethodDescriptor,所以找到一个办法的地址的公式如下:

$$办法形容的MachO偏移量 = 类形容的 MachO 偏移量 + 52 字节 + 办法地位 × 8字节$$

$$办法的 MachO 地址 = 办法形容的MachO偏移量 + 4字节 + Impl Offset$$

$$办法地址 = 办法的 MachO 地址 - 虚拟内存基地址 + 程序运行基地址$$

刚刚下面的剖析中,从寄器中读取的 teach() 函数的地址是:<u>0x100086e24</u> ,上面从可执行文件中探索函数的寻址过程。

首先,通过 image list 命令,失去所有加载的镜像库的地址,其中第一个就等于程序运行的基地址:<u>0x100080000</u> 。

这里留神,因为 ASLR 的机制,每次运行时镜像库的加载地址都不同,也就是每次程序运行的基地址都不同。

为了探索函数的寻址过程,咱们须要剖析可执行文件 MachO.

MachO 文件有很多段(Segment),各个段有不同的性能,每个段又分为很多 Section。

TEXT.text : 机器码

TEXT.cstring : 硬编码的字符串

TEXT.const: 初始化过的常量

DATA.data: 初始化过的可变的(动态/全局)数据

DATA.const: 没有初始化过的常量

DATA.bss: 没有初始化的(动态/全局)变量

DATA.common: 没有初始化过的符号申明

Swift 中新增了一些段

__swift5_types:类的形容、构造体的形容、枚举的形容

__swift5_fieldmd:属性 fieldDescriptor

__swift5_refstr:属性名称

__swift5_typeref:managedname?

在 .app 文件中显示包内容,把可执行文件用 MachOView 关上进行剖析。

首先,到 __PAGEZERO 段,记录下虚拟内存基地址:0x100000000

在可执行文件中,Class、Struct、Enum 的形容信息的地址个别存在 _TEXT,_swift5_types 段:

iOS上是小端模式, 所以咱们读到地址信息+偏移量 0xFFFFFB7C + 0xBC64 = 0x10000B7E0 失去 Teacher Description<TargetClassDescriptor> 在 MachO 中的地址:0x10000B7E0

而虚拟内存基地址是 0x100000000, 所以 0x10000B7E0 - 0x100000000 = B7E0 就是 Description<TargetClassDescriptor> 在 MachO 的偏移量。

找到 B7E0,

依据 TargetClassDescriptor 的内存构造,从 B7E0 往后读 52个字节就是 vtable。

vtable 是个数组,对应到 sil 中就是函数列表:

vtable 外面的每个元素是办法的形容 TargetMethodDescriptor ,占 8 个字节。

能够看到,teach() 函数位于第 7 个,所以从 MachO 中 vtable 的开始往后读到第 7 个 TargetMethodDescriptor ,所以 teach() 函数的办法形容偏移量 B844 。

再依据办法形容的内存构造,后面4字节是Flags,前面4字节就是 Impl 的偏移量 Offset FFFFB5DC

所以 0xB844 + 4 + FFFFB5DC = 0x100006E24 ,失去 teach() 函数在 MachO 的地址,再减去虚构基地址 0x100006E24 - 0x100000000 = 0x6E24 失去在 MachO 的偏移量,就是 <u>0x6E24</u> 。

最初,应用程序运行的基地址 0x100080000 加上 0x6E24 失去 teach() 函数在运行时的实在地址:0x100086E24 ,与咱们再寄存器中读取的地址是统一的。

(3)构造体函数动态派发

如果上述案例中改为 Struct

struct Teacher {    var age: Int = 30    var name: String = "Jack"    func teach(){        print("teach")    }    func teach1(){        print("teach1")    }    func teach2(){        print("teach2")    }}

查看汇编调用,

都是间接调用明确的函数地址,属于动态派发。

(4)extension动态派发

不论是 Class 或者 Struct,他们的 extension 里的函数都是动态派发,无奈在运行时做任何替换和扭转,因为其外面的办法都是在编译期确定好的,程序中以硬编码的形式存在,甚至不会放在 vtable 中。

extension Teacher{    func teach3(){    print("teach3")  }} var t = Teacher()t.teach3()

都是间接调用函数地址:

所以,Swift 无奈通过 extension 反对多态。

那么为什么 Swift 会把 extension 设计成动态的呢?

OC 中子类继承后不重写办法的话是去父类中找办法实现,然而 Swift 类在继承的时候,是把父类的办法造成一张vtable 存在本人身上,这样做也是为了节俭办法的查找时间,如果想让 extension 加到 vtable 中,并不是间接在子类 vtable 的最初间接追加就能够的,须要在子类中记录下父类办法的 index,把父类的 extension 办法插入到子类 vtable 中父类办法 index 后相邻的地位,再把子类本人的办法往后挪动,这样的一番操作耗费是很大的。

(5)关键字对派发形式的影响

不同的函数润饰关键字对派发形式也有这不同的影响

final 动态派发

final: 增加了 final 关键字的函数无奈被重写,无奈被继承,应用动态派发,不会在 vtable 中呈现,且对 objc 运行时不可见。

dynamic 函数表派发

dynamic: 函数均可增加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发形式还是函数表派发。

办法替换
class Teacher {  dynamic func teach(){    print("teach")  }}extension Teacher {    @_dynamicReplacement(for: teach())    func teach3() {        print("teach3")    }}

如上代码中, teach() 函数是函数表派发, 存在 vtable 中, 并且 dynamic 赋予了动态性, 与 @_dynamicReplacement(for: teach()) 关键字配合应用, 把 teach() 函数的实现改为 teach3() 的实现, 相当于OC中把 teach() 的SEL对应为 teach3() 的imp,实现办法的替换。

然而须要留神,这里与办法替换不同

var t = Teacher()t.teach()  // teach3t.teach3() // teach3

运行构造都是 teach3,只是把 teach() 函数的实现指向了 teach3,teach3() 函数自身的实现并没有扭转。

这个具体的实现是 llvm 编译器解决的, 在两头语言 IR 中, teach() 函数中有2个分支, 一个 original, 一个 forward, 如果咱们有替换的函数, 就走 forward 分支.

# 转成 IR 两头语言 .ll 文件swiftc -emit-ir main.swift > dynamic.ll 

@objc 函数表派发

@objc: 该关键字能够将Swift函数裸露给Objc运行时,仍旧是函数表派发。

@objc dynamic 音讯派发

@objc dynamic: 音讯派发的形式,和 OC 一样。理论开发中 Swift 和 OC 交互大多会应用这种形式。

对于纯Swift类, @objc dynamic 能够让办法和OC一样应用 Runtime API.

如果须要和OC进行交互, 须要把类继承自 NSObject.

static/class 动态派发

staticclass 润饰的办法相似于 OC 的类办法,Swift 中都应用动态派发。

class Teacher {    static func foo() {        print("foo")    }    class func bar() {        print("bar")    }}Teacher.foo() // fooTeacher.bar() // bar

都是间接调用的函数地址:

static 与 class 区别

下面提到,这 2 个关键字的函数都是用动态派发,而 class 关键字只能润饰类办法, static 关键字能够润饰类办法和构造体办法

其它的不同点在于继承上的区别。

class Teacher {    static func foo() {        print("foo")    }    class func bar() {        print("bar")    }}class Student: Teacher {    func foo() {        print("student foo")    }    override class func bar() {        print("student bar")    }}

执行上面代码,输入什么?

Teacher.foo()Teacher.bar()Student.foo()Student.bar()

static 润饰的办法应用动态派发,但不会进入 vtable,无奈被子类继承和重写。

class 润饰的办法也应用动态派发,进入 vtable,能够被子类继承和重写。

如下是他们的 sil :

对于 static 润饰的办法,子类容许存在一个同名的函数,然而没有意义,因为这个同名函数并不会被执行。

如上汇编,察看到 static 润饰的 foo 办法在父类和子类中都是调用同一个函数地址,也就是说子类的 foo 办法并没有意义,执行的永远是父类中 static 的 foo 办法。

class 润饰的 bar 办法,尽管也是动态派发,然而能够被子类重写,所以子类和父类调用的 bar 函数地址不一样。

所以下面的输入是

Teacher.foo() // fooTeacher.bar() // barStudent.foo() // fooStudent.bar() // student bar

参考资料

《Swift高级进阶班》

GitHub: apple - swift源码

《跟戴铭学iOS编程: 理顺外围知识点》

《程序员的自我涵养》

Swift编程语言 - 类和构造体

Swift Intermediate Language 初探

Swift性能高效的起因深入分析

Swift编译器两头码SIL

Swift的高级两头语言:SIL