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-20220105160837004Clang编译过程有以下几个缺点:
- 源代码与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
,
调用的是 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个属性 metadata
和 refCounts
, 它的定义如下:
#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
通过分析源码, 可以得出关系图
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 都是汇编中跳转到函数执行的命令)
此时,x8寄存器中存储的就是 teach() 函数的地址,读取寄存器汇中的值,register read x8
,就得到 teach() 函数的地址:<u>0x100086e24</u>
函数是如何寻址的?
为了节省存储空间,Swift 大量运用了偏移量来间接寻址。
在类的描述 TargetClassDescriptor
的开始到 vtable 之间的有 13 * 4 = 52 字节,而 vtable 数组存储的是方法描述 TargetMethodDescriptor
,所以找到一个方法的地址的公式如下:
刚刚上面的分析中,从寄器中读取的 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,
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 静态派发
static
和 class
修饰的方法类似于 OC 的类方法,Swift 中都使用静态派发。
class Teacher {
static func foo() {
print("foo")
}
class func bar() {
print("bar")
}
}
Teacher.foo() // foo
Teacher.bar() // bar
都是直接调用的函数地址:
image-20220126185113429static 与 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
修饰的方法,子类允许存在一个同名的函数,但是没有意义,因为这个同名函数并不会被执行。
如上汇编,观察到 static
修饰的 foo 方法在父类和子类中都是调用同一个函数地址,也就是说子类的 foo 方法并没有意义,执行的永远是父类中 static
的 foo 方法。
而 class
修饰的 bar 方法,虽然也是静态派发,但是可以被子类重写,所以子类和父类调用的 bar 函数地址不一样。
所以上面的输出是
Teacher.foo() // foo
Teacher.bar() // bar
Student.foo() // foo
Student.bar() // student bar
参考资料
《跟戴铭学iOS编程: 理顺核心知识点》
《程序员的自我修养》
Swift编程语言 - 类和结构体