目前,在国内大多数公司及开发者还在使用Objective-C来开发iOS应用,大部分公司及开发者不愿意换Swift原因主要有三个:一是前期Swift ABI不稳定,每次版本发布,ABI有改动,开发适配的工作量都能让开发者头痛,不过随着Swift 5.0 ABI稳定之后,这一问题已经解决;二是Swift开源库较少,随着Swift的发展,目前主流的开源库绝大部分都有了Swift版本或者Swift开发的替代版本;三是Swift动态性缺失,在国内移动互联网高速发展的环境下,要求开发者能够及时响应,包括开发出来的应用,遇到问题能够及时修复,这就要求语言的动态性较好,这也是国内移动端开发的热修复技术探索及应用走在前面的原因。
前两个问题随着Swift的发展已经得到很好解决,国内许多大厂也开始拥抱Swift,但是Swift动态性还是阻碍Swift全面取代Objective-C的一道门槛,我们在这里讨论一下Swift动态性已经目前可行的热修复方案。
Swift Runtime
由于Swift Runtime资料较少,在Swift开源库文档里面也是对Swift Runtime API的简单介绍,由API可以知道,Swift Runtime system主要包括动态类型转换,泛型实例化和协议一致性注册,它仅用于描述编译器生成的代码应遵循的运行时接口,而不是有关如何实现的细节。
Swift中的方法派发分为静态派发和动态派发,与Objective-C的消息派发机制不同,静态派发会在编译时确定方法的实现,并且以内联的方式对方法进行优化,指定函数被调用的指针;其中Swift结构体被分配在堆区,其函数默认是静态派发模式,以及使用final
、private
、static
关键字修饰的类也是静态派发模式。动态派发是在程序运行时才确定方法的实现,Swift中使用dynamic
修饰的方法是使用动态派发模式(ps:@objc
修饰的方法不一定是动态派发,只是标明该方法对Objective-C可见)。
由于Swift为了性能,牺牲了它的动态性,使得我们在Swift层面上能做的事情很少。不过,由于Swift的类分为两种: 继承自NSObject的类以及默认继承自SwiftObject的类,既然Swift中有继承自NSObject的派生类,那么也就意味着OC的动态性也能在Swift里面应用
OC Runtime
在Swift中,继承自NSObject的类都保留了其动态性,所以我们可以通过OC runtime获取到其方法,所以,也可以通过这个方式对Swift代码进行hook。
我们从OC的运行时特性可知,所有的运行时方法都依赖TypeEncoding,也就是method_getTypeEncoding返回的结果,他指定了方法的参数类型以及在函数调用时参数入栈所要的内存空间,没有这个标识就无法动态的压入参数,而一些Swift特有的类型无法映射到OC的类型,也无法用OC的typeEncoding表示,就没法通过runtime获取。
除了继承自NSObject的类之外,继承自SwiftObject类也能开启其动态性,其开启方式是在属性或方法前加上@objc
和dynamic
。@objc
是用来将Swift的API导出给Objective-C和Objective-C runtime使用的,如果你的类继承自Objective-c的类(如NSObject)将会自动被编译器插入@objc标识。加了@objc
标识的方法、属性无法保证都会被运行时调用,因为Swift会做静态优化。要想完全被动态调用,必须使用dynamic
修饰。使用dynamic
修饰将会隐式的加上@objc
标识。
关于在Swift中使用OC Runtime可以参考:Swift Runtime分析:还像OC Runtime一样吗?
@_dynamicReplacement
Swift5.0发布了一个新特性:@_dynamicReplacement(for:)
,看特性名称应该能猜到大概是动态替换。在Swift源码里面找到其相关代码:
struct InitializeDynamicReplacementLookup {
InitializeDynamicReplacementLookup() {
initializeDynamicReplacementLookup();
}
};
SWIFT_ALLOWED_RUNTIME_GLOBAL_CTOR_BEGIN
static InitializeDynamicReplacementLookup initDynamicReplacements;
SWIFT_ALLOWED_RUNTIME_GLOBAL_CTOR_END
void DynamicReplacementDescriptor::enableReplacement() const {
auto *chainRoot = const_cast<DynamicReplacementChainEntry *>(
replacedFunctionKey->root.get());
// Make sure this entry is not already enabled.
for (auto *curr = chainRoot; curr != nullptr; curr = curr->next) {
if (curr == chainEntry.get()) {
swift::swift_abortDynamicReplacementEnabling();
}
}
// First populate the current replacement's chain entry.
auto *currentEntry =
const_cast<DynamicReplacementChainEntry *>(chainEntry.get());
currentEntry->implementationFunction = chainRoot->implementationFunction;
currentEntry->next = chainRoot->next;
// Link the replacement entry.
chainRoot->next = chainEntry.get();
chainRoot->implementationFunction = replacementFunction.get();
}
void DynamicReplacementDescriptor::disableReplacement() const {
const auto *chainRoot = replacedFunctionKey->root.get();
auto *thisEntry =
const_cast<DynamicReplacementChainEntry *>(chainEntry.get());
// Find the entry previous to this one.
auto *prev = chainRoot;
while (prev && prev->next != thisEntry)
prev = prev->next;
if (!prev) {
swift::swift_abortDynamicReplacementDisabling();
return;
}
// Unlink this entry.
auto *previous = const_cast<DynamicReplacementChainEntry *>(prev);
previous->next = thisEntry->next;
previous->implementationFunction = thisEntry->implementationFunction;
}
由Swift源码,这个特性由一个链表来实现其功能,链表里边保存了方法的实现以及下一个节点。
我们写一些代码来验证一下这个功能:
struct Person {
let name: String
let age: Int
dynamic func greeting() {
print("Hello, one")
}
}
extension Person {
@_dynamicReplacement(for: greeting())
func greeting2() {
print("Hello, two")
greeting()
}
@_dynamicReplacement(for: greeting())
func greeting3() {
print("Hello, three")
greeting()
}
}
let p = Person(name: "Tom", age: 32)
p.greeting()
打印出来的结果是:
Hello, three
Hello, one
由打印结果可知,我们使用@_dynamicReplacement
去hook一个方法时,放方法执行时,取该方法的实现的链表里最后一个节点执行;另外hook的方法必须要用dynamic
修饰。由这个特性,已经实现了一部分OC Runtime的一些特性,同时,我们可以想象它的应用场景,如使用AOP手动插桩,实现日志功能。
但是,这个特性的动态性不是太强,一些OC运行时的特性没办法完全实现,所以,暂时我们也没办法使用这个特性做更多事情。
第三方Runtime库
某次在github偶然发现了一个Swift库:Runtime.这个库是基于Swift开发的,赋予Swift以动态能力的库,包括获取类型的metadata,通过反射设置属性,通过类型构造纯Swift类型实例.
我们先看一下怎么使用:
// 创建一个User类型的结构体
struct User {
let id: Int
let username: String
let email: String
}
TypeInfo
公开关于Swift结构、协议、类、元组和枚举的元数据,它能捕获属性、泛型类型、继承级别等。所以我们在做其他操作之前,我们需要先获取到User
的TypeInfo
.
let info = try typeInfo(of: User.self)
在TypeInfo
对象中,包含了PropertyInfo
的列表,里面包含了该类型的所有属性。PropertyInfo
公开属性的名称和类型,它还允许获取和设置对象的值。
// 获取User的username属性
let property = try info.property(named: "username")
// 获取username属性值
let username = try property.get(from: user)
// 设置username属性值
try property.set(value: "newUsername", on: &user)
该库还支持动态创建类型实例,包括结构体和类。
let user = try createInstance(type: User.self)
除了属性取值设值之外,还支持获取函数的元数据,包括参数个数,参数类型,返回值类型以及是否能抛出错误。
func doSomething(a: Int, b: Bool) throws -> String {
return ""
}
let info = functionInfo(of: doSomething)
下面介绍一下wickwirew关于Runtime库实现原理,主要是对其文章的翻译。
内存分布
在Swift里面,struct
是静态类型,内存分配在堆区,其属性一般是分配在一块连续的内存中;class
分配在栈区,内存不一定连续。
下面我们看一个struct
的例子:
struct Example {
var a: Int = 1
var b: Int = 2
var c: Int = 3
}
// in memory
0x0100000000000000 // a
0x0200000000000000 // b
0x0300000000000000 // c
class
的内存分布和struct
非常类似,最主要的区别就是在属性之前有一个header, header主要保存了类型的isa
指针,以及其中强引用及弱引用数量。
例如:
class Example {
var a: Int = 1
var b: Int = 2
var c: Int = 3
}
// in memory
0x8347230987523408 // isa pointer
0x0200000001000000 // strong and weak reference counts
0x0100000000000000 // a
0x0200000000000000 // b
0x0300000000000000 // c
获取类型的元数据(Metadata)
协议类型在Swift中是一个非常重要的类型,它是由两个指针大小的字段组成(16 bytes),第一个字段是底层类型元数据记录的地址,第二个字段是所有的必须协议一致性的witness表。Any是一种特殊协议,每种类型都隐式地遵守这一协议,因为它没有必需的属性或函数,所以它不包含witness表。
Swift为每个对象都保存了一份元数据(metadata),其中包括了它的类型(struct
、class
、enum
等),字段名、字段偏移量等;由于没有提供API,所以获取这个元数据非常棘手。
struct ProtocolType {
let metadataAddress: Int
let witnessTable: Int
}
struct AnyProtocol {
let metadataAddress: Int
}
我们可以通过首先将所需的类型转换为Any.Type
,由于Any.Type
只是一个8字节的地址到元数据记录,我们可以将unsafeBitCast
转换为Int
,然后使用这个地址来获得一个指针。
let type: Any.Type = Int.self
let address = unsafeBitCast(type, to: Int.self)
let pointer = UnsafeRawPointer(bitPattern: address)!
至此,我们正式获得了一个指向类型元数据记录的指针!但是它仍然是不可读的,因为我们的指针没有类型来绑定内存,这时Swift文档才真正发挥作用。从文档中我们可以构建类型并开始读取值,在后续部分,我们将只关注struct
元数据。
Struct
元数据内存分布
所有元数据记录中都有一些公开字段,包括它的类型,value witness table的内存地址,
Struct
还包括一个指向类型 nominal type descriptor的指针,一个指向父元数据的记录的指针(通常是null),字段偏移向量和一个 generic parameter descriptor。
我们可以按照Swift的这些文档描述来创建struct
元数据,我们的整个目标就是直接模拟元数据记录,因此我们也有了绑定元数据指针的类型。为了便于举例,我们将省略一些字段以保持简洁,我们要看的只是类型和nominal type descriptor
。
从文档中我们知道,在偏移量0处是类型,在偏移量1处引用了nominal type descriptor
,这里的引用不是记录它的地址,而是对偏移量的记录。在运行时,我们有RelativePointer
类型,它的是用来读取偏移量,然后从该值向前移动指定的量,并获取绑定到指定类型的值。
struct StructMetadata {
var kind: Int
var ntd: RelativePointer<Int, NominalTypeDescriptor>
}
我们也可以构建nominal type descriptor
,有一个新的类型RelativeVectorPointer
,它与 RelativePointer
非常相似,但是它指向的是一个向量,这个向量类似于连续存储元素的数组。
struct NominalTypeDescriptor {
var mangledName: RelativePointer<Int32, CChar>
var numberOfFields: Int32
var offsetToTheFieldOffsets: RelativeVectorPointer<Int32, Int>
var fieldNames: RelativePointer<Int32, CChar>
var fieldTypeAccessor: RelativePointer<Int32, Int>
}
现在元数据类型已经创建,就像之前一样,我们可以获取一个指向类型元数据记录的指针,并将该内存绑定到它。
let type: Any.Type = Int.self
let address = unsafeBitCast(type, to: UInt.self)
let pointer = UnsafePointer<StructMetadata>(bitPattern: address)!
print(pointer.pointee.kind) // prints 1 for struct
现在指针也知道了它被绑定的类型,我们可以开始读取值了。有些值(如类型)就像读取Int一样简单,而有些值(如字段类型)就可能复杂得多,但都使用相同的技术。下面将介绍如何获取字段偏移量和字段类型,以显示不同范围的复杂性。
Field Offsets
The offset to the field offset vector is stored at offset 3. This is the offset in pointer-sized words of the field offset vector for the type in the metadata record. If no field offset vector is stored in the metadata record, this is zero.
更清楚的说,字段偏移量是元数据记录中的一个向量,并不是nominal type descriptor
,nominal type descriptor
中的值,是从元数据记录的基值到字段偏移向量的开始的偏移量(以指针大小的字表示)。
在Runtime中是这样实现的:
// NominalMetadataType.swift
func fieldOffsets() -> [Int] {
return nominalTypeDescriptor
.pointee // 1
.offsetToTheFieldOffsetVector // 2
.vector(metadata: base, n: numberOfFields()) // 3
}
- 从
StructMetadata
中获取名义类型描述符; - 获取字段偏移量的
RelativeVectorPointer
; - 从元数据记录的初始内存开始,按
nominal type descriptor
中指定的值,推进到字段偏移向量的开头并读取该值。
Field Types
The field type accessor is a function pointer at offset 5. If non-null, the function takes a pointer to an instance of type metadata for the nominal type, and returns a pointer to an array of type metadata references for the types of the fields of that instance. The order matches that of the field offset vector and field name list.
文档在这里有一点误导,field type accessor在偏移量5处引用,这个引用是值到field type accessor函数指针的偏移量。
首先,我们先创建一个C的函数指针:
typealias FieldTypeAccessor =
@convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
然后,从struct
的元数据中获取这个函数:
// NominalMetadataType.swift
func fieldTypeAccessor() -> FieldTypeAccessor {
let function = nominalTypeDescriptor
.pointee // 1
.fieldTypeAccessor.advanced() // 2
return unsafeBitCast(function, to: FieldTypeAccessor.self) // 3
}
- 获取
nominal type descriptor;
- 获取
field type accessor
引用,然后根据偏移量来计算该值的位移并获取该值的指针; - 由于这个值实际上是一个函数指针,我们需要对指针进行位转换,使其指向
FieldTypeAccessor
。
现在我们有了field type accessor
函数。我们可以使用这个函数获取字段类型。
// NominalMetadataType.swift
func fieldTypes() -> [Any.Type] {
let start = fieldTypeAccessor()(base) // 1
let types = start.vector(n: numberOfFields()) // 2
return types.map{ unsafeBitCast($0, to: Any.Type.self) } // 3
}
- 获取
field type accessor
函数,并以元数据记录指针作为参数运行它,这将返回一个指向field type vector
开头的指针; - 读取字段数量的向量,这将返回一个
[Int]
数组,其中每个值是每种类型的元数据地址; - 由于一个
Any.Type
是一个只有8个字节的值,包含了类型的元数据记录的内存地址,我们可以通过Int数组进行迭代,并将每个地址进行位强制转换为Any.Type
。
Reflection
Runtime也有一个反射API,允许您动态地获取和设置值。要做到这一点,我们需要两条信息,属性的类型和偏移量。偏移量是从对象的基础地址到属性的距离(以字节数为单位)。所以在较高的层次上,所有需要发生的事情就是获取一个指向对象的指针,进入属性,将内存重新绑定到属性的类型,然后获取或设置它。
例如,我们有一个struct
,它有三个Int类型属性a、b、c,我们将使用上面描述的方法将b设置为5,因为b是结构中的第二个Int属性,我们知道它的偏移量是8。
struct Example {
var a: Int = 1
var b: Int = 2
var c: Int = 3
}
var value = Example(a: 1, b: 2, c: 3)
// 1
withUnsafePointer(to: &value) { pointer in
// 2
let base = UnsafeRawPointer(pointer)
// 3
let rawBPointer = base.advanced(by: 8)
// 4
let b = rawBPointer.bindMemory(to: Int.self, capacity: 1)
// 5
let mutable = UnsafeMutablePointer<Int>(mutating: b)
mutable.pointee = 5
}
print(value.b) // prints 5
- 获取对象的指针;
- 这个指针指向一个
Example
对象,因此如果我们尝试前进8个字节,它将前进192个字节,因为示例结构体的步长为24个字节,将它转换为一个原始指针将允许我们一次前进一个字节; - 前进8个字节,现在我们的指针指向
b
,但是这个指针依旧是UnsafeRawPointer
,并且不知道它指向的值的类型; - 将这块内存绑定到
Int
类型,因为b
是Int
类型; - 由于
UnsafePointer
默认是不可变类型,我们将它转换为UnsafeMutablePointer
类型,并设值为5。
小结
简单介绍上面三种Swift动态性的实现,我们可以看出,目前工程中可用性比较高的依旧是OC Runtime,但是它要求对象继承NSObject
或者用dynamic
关键字修饰;而@_dynamicReplacement(for:)
特性在苹果官方文档并未公布,且应用范围较小,暂时看不到其在工程中大规模应用的价值,不过相信苹果后续会开放Swift更多的动态性,扩大其应用范围就方式;关于Runtime第三方库是对TypeInfo、FuntionInfo的实现,并且扩展Swift的反射机制,扩展了Swift的动态性,后续可能会有更多的应用场景。