关于ios:Swift-中的类与结构体

8次阅读

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

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<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,

调用的是 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_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 有不同的固定值:

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()  // 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

正文完
 0