Swift 中的类与结构体

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 = s
s1.high = 10
print(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 s
0x0000000100008080: (SwiftTest.Book) s = {
0x0000000100008080:   name = "程序员的自我修养"
0x0000000100008090:   high = 8
}
(lldb) frame variable -L s1
0x0000000100008098: (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 = c
c1.age = 30
print(c.age, c1.age) // 30 30

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

(lldb) frame variable -L c
scalar: (SwiftTest.Person) c = 0x0000000100679af0 {
0x0000000100679b00:   age = 30
0x0000000100679b08:   name = "jack"
}
(lldb) frame variable -L c1
scalar: (SwiftTest.Person) c1 = 0x0000000100679af0 {
0x0000000100679b00:   age = 30
0x0000000100679b08:   name = "jack"
}
(lldb) cat address 0x0000000100679af0
address: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)})
0x0000000100008298
0 elements
(lldb) po withUnsafePointer(to: &c1, {print($0)})
0x00000001000082a0
0 elements
image-20220120150915263

3、编译过程

为了探究本质,我们需要借助编译器的中间语言进行分析。

clang

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

image-20220105160837004

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.

image-20220105161043099

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

// 1 Parse: 语法分析组件, 从Swift源码分析输出抽象语法树AST
swiftc 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 Foundation
class 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__))
#endif
SWIFT_RUNTIME_EXPORT @interface SwiftObject<NSObject> {
 @private
  Class isa; // 类类型/元类型, 存放metadata的指针
  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; //纯swift类 引用计数
}

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

Person superClass: _TtCs12_SwiftObject
Student 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,

image-20220105170452791

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

image-20220105170228157

分析Swift源码, 搜索 swift_allocObject, 定位到 HeapObject.cpp 文件,

image-20220105172121917

内部调用 swift_slowAlloc,

image-20220105172521978

至此, 通过分析 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_INTEROP
protected:
  constexpr TargetMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : Kind(reinterpret_cast<StoredPointer>(isa)) {}
#endif

private:
  /// 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 有不同的固定值:

image-20220127120739852

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

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

TargetClassMetadata : TargetAnyClassMetadata : TargetHeapMetadata : TargetMetadata

通过分析源码, 可以得出关系图

image-20220119195624615

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

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.

image-20220126153658817

然后,把项目跑在真机上,便于分析 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 都是汇编中跳转到函数执行的命令)

asm

此时,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> 。

image list

这里注意,因为 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

pagezero

在可执行文件中,Class、Struct、Enum 的描述信息的地址一般存在 _TEXT,_swift5_types 段:

discription address

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

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

找到 B7E0,

Description

根据 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")
    }
}

查看汇编调用,

image-20220126181041627

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

(4)extension静态派发

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

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

都是直接调用函数地址:

image-20220126181719185

所以,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()  // teach3
t.teach3() // teach3

运行结构都是 teach3,只是把 teach() 函数的实现指向了 teach3,teach3() 函数本身的实现并没有改变。

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

# 转成 IR 中间语言 .ll 文件
swiftc -emit-ir main.swift > dynamic.ll 
image-20220126184015503

@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() // foo
Teacher.bar() // bar

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

image-20220126185113429
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 :

image-20220126190529187

对于 static 修饰的方法,子类允许存在一个同名的函数,但是没有意义,因为这个同名函数并不会被执行。

image-20220126192444046

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

class 修饰的 bar 方法,虽然也是静态派发,但是可以被子类重写,所以子类和父类调用的 bar 函数地址不一样。

所以上面的输出是

Teacher.foo() // foo
Teacher.bar() // bar
Student.foo() // foo
Student.bar() // student bar

参考资料

《Swift高级进阶班》

GitHub: apple - swift源码

《跟戴铭学iOS编程: 理顺核心知识点》

《程序员的自我修养》

Swift编程语言 - 类和结构体

Swift Intermediate Language 初探

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

Swift编译器中间码SIL

Swift的高级中间语言:SIL

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

推荐阅读更多精彩内容