Swift中,类和结构体有许多相似之处,但也有不同本,文结合源码探究类和结构体的本质。
我们都知道,内存分配可以分为堆区(Heap)和栈区(Stack)。由于栈区内存是连续的,内存的分配和销毁是通过入栈和出栈操作进行的,速度远高于堆区。堆区存储高级数据类型,在数据初始化时,查找没有使用的内存,销毁时再从内存中清除,所以堆区的数据存储不一定是连续的。并且 retain 操作不可避免要遍历堆,而Swift的堆是通过双向链表实现的,理论上可以减少retain时的遍历,把效率提高一倍,但是还是比不过栈,所以苹果把一些放在堆里的类型改成了值类型,比如字符串、数组、字典等等。
其中,类(class)和结构体(struct)在内存分配上是不同的,基本数据类型和结构体默认分配在栈区,而类存储在堆区,且堆区数据存储不是线程安全的,在频繁的数据读写操作时,要进行加锁操作。
结构体除了属性的存储更安全、效率更高之外,其函数的派发也更高效。由于结构体的类型被 final
修饰,不能被继承,其内部函数属于静态派发,在编译期就确定了函数的执行地址,其函数的调用通过内联(inline)的方式进行优化,其内存连续,减少了函数的寻址过程以及内存地址的偏移计算,其运行相比于动态派发更加高效。
另外,引用技术也会对类的使用效率产生消耗,所以在可选的情况下应该尽可能的使用结构体。
相同点:
不同点:
结构体是值类型,实际上,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
为了探究本质,我们需要借助编译器的中间语言进行分析。
OC 和 C 这类语言,会使用 clang 作为编译器前端, 编译成中间语言 IR, 再交给后端 LLVM 生成可执行文件.
Clang编译过程有以下几个缺点:
为了解决这些缺点, Swift开发了专属的Swift前端编译器 swiftc , 其中最关键的就是引入 SIL。
Swift Intermediate Language,Swift高级中间语言,Swift 编译过程引入SIL有以下优点:
SIL会对Swift进行高级别的语意分析和优化。像LLVM IR一样,也具有诸如Module,Function和BasicBlock之类的结构。与LLVM IR不同,它具有更丰富的类型系统,有关循环和错误处理的信息仍然保留,并且虚函数表和类型信息以结构化形式保留。它旨在保留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
一般我们在分析的时候,可以通过下面这条命令把 swift 文件直接转成 sil 文件:
swiftc -emit-sil main.swift > main.sil
下面我们也会借助这条命令生成的 sil 进行分析。
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。
下面分析一下类的创建过程, 如下代码
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 文件,
至此, 通过分析 sil, 汇编, 源代码,我们可以得出swift对象的初始化过程如下:
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> Malloc
通过上面的源码, 发现初始化方法返回的是一个 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;
再点击跳转到 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
}
根据上面的分析,的结构 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 是函数表,他是一个数组。
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 // 属性的名称
}
函数表 vtable 中存储着的是方法描述 TargetMethodDescriptor
。
struct TargetMethodDescriptor {
// 4字节, 标识方法的种类, 初始化/getter/setter等等
MethodDescriptorFlags Flags;
// 相对地址, Offset
TargetRelativeDirectPointer Impl;
};
TargetMethodDescriptor
是对方法的描述;
Flags 表示方法的种类,占据 4 个字节;
Impl 里面并不是真正的方法imp,而是一个相对偏移量;
Swift有3种函数派发机制:
静态派发
是在编译期就能确定调用方法的派发方式, Swift中的静态派发直接使用函数地址.
虚函数表派发 (动态派发)
动态派发是指编译期无法确定应该调用哪个方法,需要在运行时才能确定方法的调用, 通过虚函数表查找函数地址再调用.
消息派发
使用objc的消息派发机制, objc采用了运行时objc_msgSend
进行消息派发,所以Objc的一些动态特性在Swift里面也可以被限制的使用。
静态派发相比于动态派发更快,而且静态派发还会进行内联等一些优化,减少函数的寻址过程, 减少内存地址的偏移计算等一系列操作,使函数的执行速度更快,性能更高。
一般情况下, 不同类型的函数调度方式如下
类型 | 调度方式 | extension |
---|---|---|
值类型 | 静态派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject 子类 | 函数表派发 | 静态派发 |
通过一个案例探究 动态派发/虚函数表派发 表这种方式中, 程序是如何找到函数地址的。
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 ,与我们再寄存器中读取的地址是一致的。
如果上述案例中改为 Struct
struct Teacher {
var age: Int = 30
var name: String = "Jack"
func teach(){
print("teach")
}
func teach1(){
print("teach1")
}
func teach2(){
print("teach2")
}
}
查看汇编调用,
都是直接调用明确的函数地址,属于静态派发。
不论是 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 后相邻的位置,再把子类自己的方法往后移动,这样的一番操作消耗是很大的。
不同的函数修饰关键字对派发方式也有这不同的影响
final
: 添加了 final 关键字的函数无法被重写,无法被继承,使用静态派发,不会在 vtable 中出现,且对 objc 运行时不可见。
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
: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
@objc dynamic
: 消息派发的方式,和 OC 一样。实际开发中 Swift 和 OC 交互大多会使用这种方式。
对于纯Swift类, @objc dynamic
可以让方法和OC一样使用 Runtime API.
如果需要和OC进行交互, 需要把类继承自 NSObject.
static
和 class
修饰的方法类似于 OC 的类方法,Swift 中都使用静态派发。
class Teacher {
static func foo() {
print("foo")
}
class func bar() {
print("bar")
}
}
Teacher.foo() // foo
Teacher.bar() // bar
都是直接调用的函数地址:
上面提到,这 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