iOS-Swift之Mirror源码解析

4,544 阅读6分钟

前言

Objective-C 中相信每一个 iOS 开发都知道 Runtime , 现在 Swift 已经更新到 5.6 版本了,在学习 Swift 的过程中就有一个疑问,Swift 有没有 Runtime 呢?

什么是 Runtime

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发,而这些特性就是源于 Runtime。Swift 是静态类型的语言,元数据类型允许代码在运行时检查和操作任意值,而这些是如何实现的?通过下面的内容进行介绍:

Swift 有没有 Runtime 特性呢?

下面通过一个例子来验证下:

  1. 创建了一个类 Animal, 通过 OC 语言 RunTime 的写法来获取类的属性和方法
class Animal {
    var age: Int = 2
    func eat(){
       print("eat")
    }
}
func test(){
    var methodCount:UInt32 = 0
    let methodlist = class_copyMethodList(Animal.self, &methodCount)
    for i in 0..<numericCast(methodCount) {
        if let method = methodlist?[i]{
            let methodName = method_getName(method);
            print("⽅法列表:\(String(describing: methodName))")
        } else {
            print("not found method");
        }
    }

    var count:UInt32 = 0
    let proList = class_copyPropertyList(Animal.self, &count)
    for i in 0..<numericCast(count) {
        if let property = proList?[i]{
            let propertyName = property_getName(property);
            print("属性成员属性:\(String(utf8String: propertyName)!)")
        } else {
            print("没有找到你要的属性");
        }
    }
    print("调用这个方法")
}

test()

运行后发现没有获取到任何属性和方法:

截屏2022-01-12 下午5.12.40.png

  1. 在类的属性和方法前面加上 @objc 关键字
class Animal {
    @objc var age: Int = 2
    @objc func eat(){
        print("eat")
    }
}

运行后发现获取到了属性和方法

截屏2022-01-12 下午5.12.08.png

  1. 假如当前类继承于 NSObject
class Animal: NSObject {
    var age: Int = 18
    func eat(){
        print("eat")
    }
}

运行结果显示只获取带了 init 方法 截屏2022-01-12 下午5.14.23.png

我们也可以查看,当前 Swift 类继承于 NSObject ,Swift 类暴露给 OC 的信息

截屏2022-01-12 下午5.26.55.png

发现确实只有一个 init 方法

截屏2022-01-12 下午5.28.38.png

  1. 当前 Swift 类继承于 NSObject,并且属性和方法用 @objc 修饰
class Animal: NSObject {
    @objc var age: Int = 2
    @objc func eat(){
       print("eat")
    }
}

可以看到运行的结果,打印和属性和方法 截屏2022-01-12 下午5.26.26.png

查看确实暴露出了属性和方法,这样才导致通过 Runtime 的 API 获取到了属性和方法。可想而知 Runtime 的 Hook、关联属性这些 Swift 也是使用不了的。

截屏2022-01-12 下午5.29.24.png

根据上面的例子,就可以得到如下的结论:

  1. 对于纯 Swift 类来说,⽅法和属性在不加任何修饰符的情况下。这个时候其实已经 不具备 Runtime 特性了。

  2. 对于纯 Swift 类,⽅法和属性添加 @objc 标识的情况下,当前我们可以通过 Runtime API 拿到, 但是在 OC 中我们是没法进⾏调度的。

  3. 对于继承⾃ NSObject 类来说,如果我们想要动态的获取当前的属性和⽅法,必须在其声明前添加 @objc 关键字,否则也是没有办法通过 Runtime API 获取的。

  4. 纯swift类没有动态性,但在⽅法、属性前添加dynamic修饰,可获得动态性。 继承⾃NSObject的swift类,其继承⾃⽗类的⽅法具有动态性,其它⾃定义⽅法、属性想要获得动 态性,需要添加dynamic修饰。

  5. 若⽅法的参数、属性类型为 swift特有、⽆法映射到 objective-c 的类型(如Character、Tuple),则 此⽅法、属性⽆法添加dynamic修饰(编译器报错)

那么 Swift 类没有 Runtime 的特性,怎么去获取属性、方法呢?

Mirror 反射机制

所谓反射就是可以动态获取类型、成员信息,在运⾏时可以调⽤⽅法、属性等⾏为的特性。在使⽤OC开发时很少强调其反射概念,因为OC的Runtime要⽐其他语⾔中的反射强⼤的多。但是 Swift 是⼀⻔类型安全的语⾔,不⽀持我们像 OC 那样直接操作,它的标准库仍然提供了反射机制来让我们访问成员信息。

Swift 的反射机制是基于⼀个叫 Mirror 的结构体来实现的。为具体的实例创建⼀个 Mirror 对象,然后就可以通过它查询这个实例。

Mirror的简单使用

let animal = Animal()

//⾸先通过构造⽅法构建⼀个Mirror实例,这⾥传⼊的参数是 Any,也就意味着当前可以是类,结构体,枚举等。返回结果是一个提供该值子元素集合 `Children` 的相关信息的实例
let mirror = Mirror(reflecting: animal)

//在Child的值上用Mirror去遍历整个对象的层级视图
for pro in mirror.children{
    //然后我们可以直接通过 label 输出当前的名称,value 输出当前反射的值
    print("\(pro.label):\(pro.value)")
}

截屏2022-01-12 下午5.49.10.png

假如我们不使用 Mirror ,这时打印实例信息会是这样的,没有具体的属性信息的

截屏2022-01-12 下午9.11.56.png

根据上面创建的 Mirror 实例,跟进去看到对应的定义

//常量类型是这个
public let children: Mirror.Children

//Children是一个集合类型,参数是泛型
public typealias Children = AnyCollection<Mirror.Child>

//泛型参数是一个元组类型
public typealias Child = (label: String?, value: Any)

Mirror源码窥探

Swift 的运行时的底层是使用 C++ 实现的,但是在 Swift 中不能直接访问 C++ 的类,所以有一个 C 的连接层。反射的 Swift 实现在 ReflectionMirror.swift,C++ 实现在 ReflectionMirror.cpp

⾸先我们现在源⽂件⾥⾯搜索 Mirror.Swift ,在源码中我们可以很清晰的看到 Mirror 是由结构体实现的,快速定位到初始化的⽅法

截屏2022-01-12 下午5.59.51.png

可以看到,这⾥接受⼀个 Any 类型的参数,同样的这⾥有⼀个 if case 的写法来判断当前的 subject 是否遵循了 customReflectable 协议,如果是我们就直接调⽤ customMirror , 否则就进⾏下级函数的调⽤。

这⾥有两个需要注意的点 if case 的写法,这⾥其实枚举 Case 的模式匹配,和我们的 Switch ⼀样,这⾥是只有⼀个 case 的 switch 语句。与此同时这⾥出现了⼀个 customRefletable 的协议。

customRefletable 的具体用法

⾸先我们遵循 customReflectable 协议,并实现其中的属性 customMirror,customMirror会返回⼀个 Mirror 对象。代码实现如下:

class Animal: CustomReflectable {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }

    var customMirror: Mirror {
        let info = KeyValuePairs<String, Any>.init(dictionaryLiteral: ("age", age), ("name", name))
        let mirror = Mirror.init(self, children: info, displayStyle: .**class**, ancestorRepresentation: .generated)
        return mirror
    }
}

var animal = Animal(age: 2, name: "pig")

运行下,打印下 animal

截屏2022-01-12 下午6.09.53.png

平常是不能打印出信息的

截屏2022-01-12 下午6.12.14.png

这个是 Mirror 结构体的初始化方法

截屏2022-01-12 下午5.59.51.png

如果没有遵循这个协议,走的是下面的方法。全局搜索 internalReflecting ,然后在 ReflectionMirror.swift 发现了 internalReflecting

截屏2022-01-12 下午7.10.18.png

获取当前 subject 的类型

let subjectType = subjectType ?? _getNormalizedType(subject, type: type(of: subject))

全局搜索 _getNormalizedType,在 ReflectionMirror.cpp 文件发现了

截屏2022-01-12 下午7.12.43.png

getNormalizedType<T>(_: T, type: Any.Type) -> Any.Type 最终调⽤的是ReflectionMirror.cpp 中的 C++ 代码,这⾥使⽤了⼀个编译器字段 @_silgen_name ,其实是 Swift的⼀个隐藏符号,作⽤是将某个C/C++语⾔函数直接映射为Swift函数

call 的实现并没有想象中那么令人激动。主要是一个大型的 switch 声明和一些额外的代码去处理特殊的情况。重要的是它会用一个 ReflectionMirrorImpl 的子类实例去结束调用 f,然后会调用这个实例上的方法去让真正的工作完成。接着跟进去ReflectionMirrorImpl

截屏2022-01-12 下午7.15.48.png

ReflectionMirrorImpl 结构体的具体实现(可以看到这是⼀个抽象类,也就意味着不同类型的 反射都需要去实现 ReflectionMirrorImpl ),在下面代码我们页发现了TupleImpl、StructImpl 等,也就是我们常见类型class, struct, enum, Tuple 的具体实现。

结构体的反射

  • 首先是一个帮助方法去检查结构体是否完全支持反射。结构体元数据里储存这样一个可被访问的标志位。跟上面元组的代码类似,可以知道 type 实际上是一个 StructMetadata 指针,所以我们可以自由的传入:
struct StructImpl : ReflectionMirrorImpl {
  bool isReflectable() {
    const auto *Struct = static_cast<const StructMetadata *>(type);
    const auto &Description = Struct->getDescription();
    return Description->isReflectable();
  }
}
  • 结构体的显示样式是 s :
char displayStyle() override {
    return 's';
  }
  • 子元素的数量是元数据给出的字段的数量,也可能是 0(如果这个类型实际上不能支持反射的话)
intptr_t count() override {
    if (!isReflectable()) {
      return 0;
    }

    auto *Struct = static_cast<const StructMetadata *>(type);
    return Struct->getDescription()->NumFields;
  }
  • 像之前那样,subscript 方法是比较复杂的部分。它开始也是类似的,做边界检查和查找偏移值:
intptr_t childOffset(intptr_t i) override {
    auto *Struct = static_cast<const StructMetadata *>(type);
    if (i < 0 || (size_t)i > Struct->getDescription()->NumFields)
      swift::crash("Swift mirror subscript bounds check failure");
    // Load the offset from its respective vector.
    return Struct->getFieldOffsets()[i];
  }
  • 通过 _swift_getFieldAt获取类型信息,一但它有字段信息,一切就会进行得和元组对应部分的代码类似。填写名字和计算字段储存的指针:
const FieldType childMetadata(intptr_t i, const char **outName,

                                void (**outFreeFunc)(const char *)) override {
    StringRef name;
    FieldType fieldInfo;
    std::tie(name, fieldInfo) = getFieldAt(type, i);
    assert(!fieldInfo.isIndirect() && "indirect struct fields not implemented");

    *outName = name.data();
    *outFreeFunc = nullptr;
    return fieldInfo;
  }

读取 Struct 结构体源码

下面我们就尝试以 struct 为例来看⼀下 Mirror 都是如何获取到这些数据的。当然的属性数量 (可以看到的是,这⾥通过 Metadata 中的 getDescription 查询字段 NumFields )

截屏2022-01-12 下午7.32.36.png

const TargetStructDescriptor<Runtime> *getDescription() const {
    return llvm::cast<TargetStructDescriptor<Runtime>>(this->Description);
}

swift_getField 的帮助函数可以查找给定类型相应的字段描述符

截屏2022-01-12 下午7.37.12.png

可以看到是这⾥通篇都是通过 Metadata ,getDescription() ,FieldDescrition  这⼏个东⻄来去实现的,⼀个是当前类型的元数据,⼀个是当前类型的描述,⼀个是对当前类型属性的  描述。所以看到这⾥我们能够明⽩ Mirror 是如何⼯作的。 同时我们在前⾯的探索过程中也探索了类,结构体和枚举,接下来我们尝试能不能⾃⼰利⽤ Metadata 来还原获取当前类型的各种属性。

Struct 的代码实现

TargetStructMetadata结构

打开 Swift 源码,定位到 Metadata.h文件,搜索 TargetStructMetadata,然后一步步的去跟进定位结构,这里有整理出来的一张流程图,很清晰的看出各种结构:

6333164-b9f6ea6e62136bbc-2.png

把上面的流程图转换成伪代码:

struct StructMetaData{
    var kind : Int32
    //从TargetValueMetadata继承
    var typeDescriptor : UnsafeMutablePointer<StructDescriptor>
}

struct StructDescriptor {
    //Flags、Parent从TargetContextDescriptor继承
    //描述上下文的标志,包括其Kind和Version
    let flags: Int32
    //父上下文,如果为顶级上下文,则为null
    let parent: Int32
    //结构体的名称
    var name: RelativePointer<CChar>
    var AccessFunctionPtr: RelativePointer<UnsafeRawPointer>
    //记录属性内容
    var Fields: RelativePointer<FieldDescriptor>
    //结构中存储的属性数
    var NumFields: Int32
    //属性在元数据中字段偏移向量的偏移量
    var FieldOffsetVectorOffset: Int32
}

//记录结构体内所有属性的结构
struct FieldDescriptor {
    var MangledTypeName: RelativePointer<CChar>
    var Superclass: RelativePointer<CChar>
    var kind: UInt16
    var fieldRecordSize: Int16
    var numFields: Int32
    //连续存储空间 (有几个数据,就会在后面添加几个记录,通过内存平移读取)
    var fields: FieldRecord
}

struct FieldRecordT<Element> {
    var element: Element
    mutating func element(at i: Int) -> UnsafeMutablePointer<Element> {

        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating:  UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}

//每个属性的内容
struct FieldRecord {
    var Flags: Int32
    var MangledTypeName: RelativePointer<CChar>
    var FieldName: RelativePointer<CChar>
}

// 相对位移指针,相当于RelativeIndirectPointer和RelativeDirectPointer
struct RelativePointer<T> {
    var offset: Int32
    // 偏移offset位置,获取内容指针
    mutating func get() -> UnsafeMutablePointer<T>{
        let offset = self.offset
        //withUnsafePointer获取指针
        // UnsafeMutablePointer 返回T类型对象的指针
        // UnsafeRawPointer将p指针转换为未知类型
        // advanced进行内存偏移
        // numericCast将offset转换为偏移单位数
        // assumingMemoryBound绑定指针为T类型
        return withUnsafePointer(to: &self) { p in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: T.self))
        }
    }
}

解析自定义的 Struct

我们可以根据还原出来的代码,来解析自定义的 Struct

struct Animal {
    var age: Int = 2
    var name: String = "pig"
}

//UnsafeMutablePointer<StructMetaData>.self  获取当前 struct 的 Metadata
//利⽤强转函数 unsafeBitCast 按位转换内存指针
//把 Animal Metadata 转成还原出来的 StructMetaData
let ptr = unsafeBitCast(Animal.self as Any.Type, to: UnsafeMutablePointer<StructMetaData>.self)

//通过属性内存的访问,获取到 name 字段的内存地址
let namePtr = ptr.pointee.typeDescriptor.pointee.name.get()

//经过 String 函数输出,得到我们结构体的名字
print("current class name: \(String(cString: namePtr))")

//获取属性的个数
let numFields = ptr.pointee.typeDescriptor.pointee.NumFields
print("当前类属性的个数: \(numFields)")

//获取属性的描述信息
let fieldDespritor = ptr.pointee.typeDescriptor.pointee.Fields.get()

//遍历属性
for i in 0..<numFields {
    //获取属性的信息
    let record = withUnsafePointer(to: &fieldDespritor.pointee.fields){
        return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: FieldRecord.self).advanced(by: numericCast(i)))
    }
    
    //读取属性的名称
    let recordNameStr = String(cString: record.pointee.FieldName.get())
    //读取属性的类型
    let manNameStr = String(cString: record.pointee.MangledTypeName.get())
    print("类型名称:\(recordNameStr)---- 类型:\(manNameStr)")
}

运行就可以看到确实获取到了 Struct 的名字,属性的名称以及属性的类型

截屏2022-01-12 下午8.41.11.png

这里的 Si 就是 Int 类型,SS 就代表 String 类型。这样就完成了 Struct 的源码还原,其实 Enum 和结构体的 Metadata 数据结构差不多,而 Class 的实现起来麻烦一些。

总结:

结合起来整体分为这几个步骤:

  1. 获取当前 struct 的 Metadata,然后转成 StructMetaData

  2. 通过属性内存的访问,获取结构体的名称

  3. 获取属性的个数

  4. 获取属性的描述信息

  5. 读取属性的信息

非常感谢这位同学整理的流程图:www.jianshu.com/p/30dc12515…

参考文章: swift.gg/2018/11/15/…