Swift之Runtime探究 & 错误处理 & Mirror源码分析

  • Runtime

在Swift中,通过class_copyMethodListclass_copyPropertyList来获取Swift类中的方法列表属性列表,例:

class YYTeacher {
    var age : Int = 18
    func teach() {
        print("teach")
    }
}

let t = YYTeacher()
func test() {
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(YYTeacher.self, &methodCount)
    for i in 0..

通过执行上面代码,可知:
①属性和方法前+@objc:获取到方法列表(:teach,:age, :setAge:)和属性列表(age)
特点:无意义,不能提供给OC类使用
②继承NSObject去掉@objc:只能获取到方法列表中的:init方法
特点:获取不到方法列表和属性列表
③继承NSObject且属性和方法前+@objc:获取到方法列表(:teach,:init,:age, :setAge:)和属性列表(age)

总结如下:

  • 对于纯Swift类来说,没有动态特性。方法和属性前加任何修饰符的情况下,该类不具备所谓的 Runtime 特性。
  • 对于纯Swift类来说,如果方法和属性前没有添加 @objc 标识,是能通过 Runtime API 来获取到方法列表属性列表的。
  • 在方法和属性前添加了 @objc 标识的前提下,想要在OC中使用,还必须继承自NSObject;如果想要方法交换,还需要加上dynamic标识,否则只是暴露给OC并没有动态性。
  • 扩展:通过查看源码可知Swift有默认的基类SwiftObject,且实现了NSObject;Swift为了和OC交互,内部保留了OC数据结构(isarefCounts);继承NSObject是为了声明这是一个和OC交互的类帮助编译器判断该类在编译过程中走哪些分支。
  • 反射

    反射就是在运行时允许动态访问类型成员信息等行为的特性,作用于对象;Swift标准库提供了反射机制,对于OCRuntime来说,有很多方式来动态获取修改代码,反射对其不值一提(所以OC中并无反射)。
class YYTeacher : NSObject {
    var age : Int = 18
    var height = 1.80
}

let t = YYTeacher()
let mirror = Mirror(reflecting: t)
for pro in mirror.children {
    print("\(pro.label!):\(pro.value)")
}

上面的代码可以获取到对象t成员属性

  • 反射的使用
1. 封装一个基础类的JSON解析方法
  • 将JSON解析方法放在协议中,需要解析JSON的及其属性实现该协议即可
protocol JSONMapProtocol {
    func jsonMap() -> Any
}
  • 为了方便使用,不用在需要的地方一一实现该方法,给协议一个默认实现
extension JSONMapProtocol {
    func jsonMap() -> Any {
        let mirror = Mirror(reflecting: self)
        guard !mirror.children.isEmpty else {
            return self
        }
        
        var resultDict : [String : Any] = [:]
        for child in mirror.children {
            if let value = child.value as? JSONMapProtocol {
                if let keyName = child.label {
                    // 递归调用,若有多层,则深度解析(所以需要解析的属性也要遵守该协议)
                    resultDict[keyName] = value.jsonMap() 
                } else {
                    print("No keys")
                }
            } else {
                print("No Comply JSONMapProtocol") // 未遵守JSONMapProtocol
            }
        }
        return resultDict
    }
}
  • 调用该方法
// 属性遵守协议
extension String:JSONMapProtocol{}
extension Int:JSONMapProtocol{}

// 类遵守协议
class YYTeacher: JSONMapProtocol {
    var age : Int = 18
    var name = "YY"
}

class YYMathTeacher: YYTeacher {
    var teachType = 1
    var t = YYTeacher()
}

// 调用
let mathT = YYMathTeacher()
print(mathT.jsonMap())

得到结果["t": ["name": "YY", "age": 18], "teachType": 1]

2. 在上面JSON解析基础类的基础上,封装错误处理
  • 定义错误
// 错误类型
enum JSONMapError:Error {
    case emptyError
    case noComformProtocolError
}
// 错误信息描述
extension JSONMapError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .emptyError:
            return "空错误"
        case .noComformProtocolError:
            return "未遵守协议"
        }
    }
}

  • 解析方法若出错,通过throws抛出错误
protocol JSONMapProtocol {
    func jsonMap() throws -> Any
}

extension JSONMapProtocol {
    func jsonMap() throws -> Any {
        let mirror = Mirror(reflecting: self)
        guard !mirror.children.isEmpty else {
            return self
        }
        
        var resultDict : [String : Any] = [:]
        for child in mirror.children { 
            if let value = child.value as? JSONMapProtocol {
                if let keyName = child.label {
                    resultDict[keyName] = try value.jsonMap()
                } else {
                    throw JSONMapError.emptyError
                }
            } else {
                throw JSONMapError.noComformProtocolError
            }
        }
        
        return resultDict
    }
}
  • 调用该方法
  1. 通过try、try?、try!处理错误
    try:将错误抛给上层函数,若上层函数未处理,则崩溃跳转至上层函数所在的文件;若要处理则通过catch代码块处理
/**
 Fatal error: Error raised at top level: FirstSwiftTest.JSONMapError.noComformProtocolError: file Swift/ErrorType.swift, line 200
 2021-08-08 10:45:07.768701+0800 FirstSwiftTest[6049:365271] Fatal error: Error raised at top level: FirstSwiftTest.JSONMapError.noComformProtocolError: file Swift/ErrorType.swift, line 200*/

try?:返回一个可选类型,会向抛出,成功则返回具体的字典值错误则统一返回nil
try!:保证绝对不会出错则用try!,如果出错则编译失败

/** 
 Fatal error: 'try!' expression unexpectedly raised an error: FirstSwiftTest.JSONMapError.noComformProtocolError: file FirstSwiftTest/main.swift, line 141
 2021-08-08 10:44:38.917009+0800 FirstSwiftTest[6037:364958] Fatal error: 'try!' expression unexpectedly raised an error: FirstSwiftTest.JSONMapError.noComformProtocolError: file FirstSwiftTest/main.swift, line 141*/
  1. 通过do catch代码块进行捕获并处理异常
extension String:JSONMapProtocol{}
extension Int:JSONMapProtocol{}

class YYTeacher: JSONMapProtocol {
    var age : Int = 18
    var name = "YY"
    var height = 1.80 //新加未遵守协议的属性
}

class YYMathTeacher: YYTeacher {
    var teachType = 1
    var t = YYTeacher()
}

let mathT = YYMathTeacher()
var dict : Any?
do {
    try dict = mathT.jsonMap()
    print(dict)
} catch { // 返回的是Any,将error转为JSONMapError再调用其errorDescription属性
    if let jsonMapError = error as? JSONMapError {
        print(jsonMapError.errorDescription!)
    } else {
        print(error.localizedDescription)
    }
}

得到结果为:未遵守协议(因为这里height属性为Double类型,遵守JSONMapProtocol

扩展CustomNSError用来桥接原来OC的NSError中的codedomainUserInfo;如果想让我们的自定义Error可以转成NSError,实现CustomNSError就可以完整的as成NSError。

extension JSONMapError : CustomNSError {
    var errorUserInfo : [String : Any] {
    }
    var errorCode: Int {
    }
    static var errorDomain: String {
    }
}
  • Mirror源码分析

通过查看源码可知:Mirror实际上是一个Struct

其中有一个初始化方法init(reflecting subject: Any)

方法里面在获取对象的类型属性个数

@_silgen_name:可以对于某些简单的代码,直接跳过桥接文件.h头文件C代码交互。

  • .c文件中声明并实现一个方法num_add
int num_add(int a, int b) {
    return  a + b;
}
  • Swift文件中,需要桥接文件和.h头文件时也能访问该方法
@_silgen_name("num_add")
func swift_num_add(a : Int32, b : Int32) -> Int32

var num = swift_num_add(a: 22, b: 11)
print(num) //33
  • 由于C语言中函数的符号是_ + 函数名,所以多个C文件中不可能存在多个名称一样的函数,Swift可以放心使用@_silgen_name来与之交互
  1. 通过源码分析Struct结构来了解在Swift的反射中,怎么确定类型?

模拟Struct源码结构来分析:是如何将一个自定义的struct的Metadata绑定到struct结构的(反射中取类型以及type(of:)的原理)?

struct StructMetadata {
    var kind : Int32
    var desc : UnsafeMutablePointer
}

// 对metadata的描述
struct StructMetadataDesc {
    var flags : Int32
    var parent : Int32
    var name : StructMetadataDescName
}

// type的名称
struct StructMetadataDescName {
    var offset : Int32
    // 其实就是取字符串存储的地址
    mutating func get() -> UnsafeMutablePointer {
        let offset = self.offset
        return withUnsafePointer(to: &self) { ptr in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(ptr).advanced(by: numericCast(offset)).assumingMemoryBound(to: T.self))
        }
    }
}

struct YYTeacher {
    var name = "YY"
    var age = 12
}

var t = YYTeacher.self

// 如何将YYTeacher的metadata绑定到StructMetadata?

//unsafeBitCast:谨慎使用,有更好的方法再来替换  按位强制转换
let ptr = unsafeBitCast(YYTeacher.self as Any.Type, to: UnsafeMutablePointer.self)

let namePtr = ptr.pointee.desc.pointee.name.get()
let nameStr = String(cString: namePtr)

print(nameStr) // YYTeacher

下面用一张图帮助理解:

其实,StructMetadataDesc中的name就是内存地址0x100(相当于char *),StructMetadataDescName中的offset为0x10。

  1. 模拟反射中获取属性个数的原理:
    更深入查看源码来补充StructMetadata的属性,
struct StructMetadataDesc {
    var flags : Int32
    var parent : Int32
    var name : StructMetadataDescName
    var AccessFunctionPtr : StructMetadataDescName
    var Fields : StructMetadataDescName
    var NumFields : Int32 // 属性的count
    var FieldOffsetVectorOffset : Int32
}
print(ptr.pointee.desc.pointee.NumFields) // 2 属性的个数

源码中也是这样实现的:

  1. 模拟反射中获取属性的名称和值的原理,先查看源码实现:

模拟实现之前,先查看源码中Fields字段的类型:

FieldDescriptor 中的 getFields()返回的是一个装FieldRecord的数组,其实就是返回的一块连续内存空间,存储每一个FieldRecord;所以在模拟实现时,直接用一个字段来表示这块连续内存。

struct StructMetadataDesc {
    var flags : Int32
    var parent : Int32
    var name : StructMetadataDescName
    var AccessFunctionPtr : StructMetadataDescName
    var Fields : StructMetadataDescName
    var NumFields : Int32 //TargetStructDescriptor
    var FieldOffsetVectorOffset : Int32
}

//class FieldDescriptor
struct StructMetadataDescFileds {
    var mangledTypeName : StructMetadataDescName
    var superclass : StructMetadataDescName

    var kind : UInt16
    var fieldRecordSize : Int16
    var numFields : Int32
    // ArrayRef getFields()一个数组
    var fields : StructMetadataDescFiledRecord // 存储的一块连续内存空间的首地址
}
// class FieldRecord
struct StructMetadataDescFiledRecord {
    var flags : Int32
    var MangledTypeName : StructMetadataDescName
    var FieldName : StructMetadataDescName
}

在上面获取类型及属性个数的基础上调用:

//unsafeBitCast:谨慎使用,有更好的方法再来替换  按位强制转换
let ptr = unsafeBitCast(YYTeacher.self as Any.Type, to: UnsafeMutablePointer.self)
//  存储的一块连续内存空间的首地址
var fieldsPtr = ptr.pointee.desc.pointee.Fields.get()
// 通过类型转换再移动步长的方式来获取每个属性
// 因为已经转换了类型,即已知类型,所以这里移动的步长为0、1、2...
let fieldRecordPtr = withUnsafePointer(to: &fieldsPtr.pointee.fields) {
    return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: StructMetadataDescFiledRecord.self).advanced(by: 0))
}

let fieldNameStr = String(cString: fieldRecordPtr.pointee.FieldName.get())
print(fieldNameStr)

优化:上面的方式,只能通过修改步长来一次获取到一个属性,如何一次性得到每一个属性?

struct StructMetadataDescFiledRecordT {
    var element : Element
    mutating func element(at i : Int) -> UnsafeMutablePointer {
        
        return withUnsafePointer(to: &self) { ptr in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(ptr).assumingMemoryBound(to:Element.self).advanced(by: i))
        }
    }
}

StructMetadataDescFileds中的fields字段改为:

var fields : StructMetadataDescFiledRecordT

调用时:

let fieldCount = fieldsPtr.pointee.numFields
for i in 0..

获取属性值,原理如下:

let fieldsCount = ptr.pointee.desc.pointee.NumFields

// 用来表示一块连续的内存空间
// start : structmetadata的首地址 + 偏移量---->连续内存空间的首地址
// count : 连续内存空间中存的个数
var bufferPtr = UnsafeBufferPointer(start: UnsafeRawPointer(UnsafeRawPointer(ptr).assumingMemoryBound(to: Int.self).advanced(by: numericCast(offset))).assumingMemoryBound(to: Int32.self), count: Int(fieldsCount))
 
// 结构体实例var t1 = YYTeacher() t1的内存地址
var valuePtr = withUnsafeMutablePointer(to: &t1) { $0 }

var bufferPtr1 = UnsafeRawPointer(UnsafeRawPointer(valuePtr).advanced(by: numericCast(bufferPtr[0]))).assumingMemoryBound(to: String.self)
print(bufferPtr1.pointee)   //YY

var bufferPtr2 = UnsafeRawPointer(UnsafeRawPointer(valuePtr).advanced(by: numericCast(bufferPtr[1]))).assumingMemoryBound(to: Int.self)
print(bufferPtr2.pointee)   //12

var bufferPtr3 = UnsafeRawPointer(UnsafeRawPointer(valuePtr).advanced(by: numericCast(bufferPtr[2]))).assumingMemoryBound(to: Double.self)
print(bufferPtr3.pointee)   //1.8

type(of:)dump(obj)就是基于Mirror反射的原理。

注意:源码调试的过程中所有dlopen错误都是由于路径错误(dyld、lldb.framework以及文件路径)。

  1. 将原来的LLDB.framework压缩为LLDB.framework.zip,再将此LLDB.framework删掉,然后将另外的LLDB.framework添加到该文件夹下。
  2. 将源码中的lldb修改为liblldb.dylib放到.vscode文件夹下的lib文件夹中将原来的liblldb.dylib替换
  • 元类型(Metadata)、AnyClass、Self

AnyObject:类的实例(一定能为nil),类的类型,仅能实现的协议AnyObject!表示为可选类型

// 此时代表YYTeacher类的实例对象
var t : AnyObject = YYTeacher()

// 此时代表YYTeacher类的类型
var t1 : AnyObject = YYTeacher.self
// 此时也代表一个类型 NSNumber类型
var age : AnyObject = 10 as NSNumber

// 此时代表仅class才能实现的协议
protocol TestPro : AnyObject {}
//如果这里改为struct实现该协议会报错
class TestCls : TestPro {}

Any:代表任意类型,比AnyObject广泛,包括function类型和Optional类型

// 此时如果使用AnyObject则会报错
var arr : [Any] = [1, true, "a"]

AnyClass:任意实例对象类型,通过查看源码可知typealias AnyClass = AnyObject.Type,即任意类的元类型,任意类的类型都隐式遵守这个协议。
T.self:如果T是实例,则返回的是它本身;如果T是,则返回的是其Metadata
T.Type:一种类型,如果T是T.selfT.Type类型(Metadata);如果T是实例T.self实例的类型。

//这个self就是YYTeacher.Type类型
var t = YYTeacher.self
// 此时self是YYTeacher类型
var t1 = t.self

type(of:):用来获取一个值运行时期的动态类型(dynamic type),返回的是Any.Type

// static type:编译器确定好的
// dynamic type:原本的类型

var age = 12
func test(_ value : Any) {
  // age原本类型为Int,age的dynamic type为Int
    let valueType = type(of: value)
    print(valueType)
}
// test的参数类型为Any,age作为参数传进去,static type为Any
test(age)

特殊情况:有协议&泛型的情况下,type(of:)无法拿到真实的类型,需要作as Any处理:

protocol YYProtocol {
    
}

class YYTeacher: YYProtocol {
    var age = 10
}

let t = YYTeacher()
let t1 : YYProtocol = YYTeacher()

func test(_ value : T) {
    let valueType = type(of: value as Any)
    print(valueType)
}

test(t) //无as Any时,YYTeacher;有as Any时,YYTeacher
test(t1)//无as Any时,YYProtocol;有as Any时,YYTeacher
  • 扩展:寄存器就是用来放东西的,既可以放地址,也可以放地址中的
    x8:取的是寄存器的地址 ==po withUnsafePointer(to: &t){print($0)}
    [x8]:取的是寄存器中的 ==po t

你可能感兴趣的:(Swift之Runtime探究 & 错误处理 & Mirror源码分析)