Swift中的类与结构体

Swift中的类与结构体,第1张

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

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源码分析输出抽象语法树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 {
 @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,

调用的是 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;

再点击跳转到 TargetHeapMetadata,

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

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

再继续点击跳转分析 TargetHeapMetadata 的父类 TargetMetadata,

/// The common structure of all type metadata.
template 
struct TargetMetadata { //  最终基类
  using StoredPointer = typename Runtime::StoredPointer;

  /// The basic header type.
  typedef TargetTypeMetadataHeader HeaderType;

  constexpr TargetMetadata()
    : Kind(static_cast(MetadataKind::Class)) {}
  constexpr TargetMetadata(MetadataKind Kind)
    : Kind(static_cast(Kind)) {}

#if SWIFT_OBJC_INTEROP
protected:
  constexpr TargetMetadata(TargetAnyClassMetadata *isa)
    : Kind(reinterpret_cast(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
  getTypeContextDescriptor() const {
    switch (getKind()) { // 根据 kind 区分不同的类
    case MetadataKind::Class: {
      const auto cls = static_cast *>(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 *>(this)
          ->Description;
    case MetadataKind::ForeignClass:
      return static_cast *>(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 Description;

这个 TargetClassDescriptor 是 Swift 类的描述 ,它有个别名 ClassDescriptor

using ClassDescriptor = TargetClassDescriptor;

根据 ClassDescriptor 全局搜索源码, 可以定位到一个 类 ClassContextDescriptorBuilder

// 类的Descriptor构建者, 创建 metadata 和 Descriptor 的地方
  class ClassContextDescriptorBuilder
    : public TypeContextDescriptorBuilderBase,
      public SILVTableVisitor
  {
  ....
    // 内存布局的赋值 *** 作
    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 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() 函数的地址:0x100086e24

函数是如何寻址的?

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

在类的描述 TargetClassDescriptor 的开始到 vtable 之间的有 13 * 4 = 52 字节,而 vtable 数组存储的是方法描述 TargetMethodDescriptor,所以找到一个方法的地址的公式如下:
方 法 描 述 的 M a c h O 偏 移 量 = 类 描 述 的 M a c h O 偏 移 量 + 52 字 节 + 方 法 位 置 × 8 字 节 方法描述的MachO偏移量 = 类描述的 MachO 偏移量 + 52 字节 + 方法位置 × 8字节 MachO=MachO+52+×8

方 法 的 M a c h O 地 址 = 方 法 描 述 的 M a c h O 偏 移 量 + 4 字 节 + I m p l O f f s e t 方法的 MachO 地址 = 方法描述的MachO偏移量 + 4字节 + Impl Offset MachO=MachO+4+ImplOffset

方 法 地 址 = 方 法 的 M a c h O 地 址 − 虚 拟 内 存 基 地 址 + 程 序 运 行 基 地 址 方法地址 = 方法的 MachO 地址 - 虚拟内存基地址 + 程序运行基地址 =MachO+

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

首先,通过 image list 命令,得到所有加载的镜像库的地址,其中第一个就等于程序运行的基地址:0x100080000

这里注意,因为 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 在 MachO 中的地址:0x10000B7E0

而虚拟内存基地址是 0x100000000, 所以 0x10000B7E0 - 0x100000000 = B7E0 就是 Description 在 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 的偏移量,就是 0x6E24

最后,使用程序运行的基地址 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()  // teach3
t.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() // foo
Teacher.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() // 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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/993714.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存