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..<numericCast(methodCount) {
        if let method = methodList?[i]{
            let methodName = method_getName(method);
            print("方法列表 :\(String(describing: methodName))")
        } else {
            print("not found method");
        }
    }
    
    var proCount : UInt32 = 0
    let proList = class_copyPropertyList(YYTeacher.self, &proCount)
    for i in 0..<numericCast(proCount) {
        if let property = proList?[i] {
            let propertyName = property_getName(property)
            print("属性成员属性:\(String(utf8String: propertyName)!)")
        } else {
            print("没有找到你要的属性")
        }
    }
}

test()

通过执行上面代码,可知:
①属性和方法前+@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<StructMetadataDesc>
}

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

// type的名称
struct StructMetadataDescName<T> {
    var offset : Int32
    // 其实就是取字符串存储的地址
    mutating func get() -> UnsafeMutablePointer<T> {
        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<StructMetadata>.self)

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

print(nameStr) // YYTeacher

下面用一张图帮助理解:

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

  1. 模拟反射中获取属性个数的原理:
    更深入查看源码来补充StructMetadata的属性,
struct StructMetadataDesc {
    var flags : Int32
    var parent : Int32
    var name : StructMetadataDescName<CChar>
    var AccessFunctionPtr : StructMetadataDescName<UnsafeRawPointer>
    var Fields : StructMetadataDescName<UnsafeRawPointer>
    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<CChar>
    var AccessFunctionPtr : StructMetadataDescName<UnsafeRawPointer>
    var Fields : StructMetadataDescName<StructMetadataDescFileds>
    var NumFields : Int32 //TargetStructDescriptor
    var FieldOffsetVectorOffset : Int32
}

//class FieldDescriptor
struct StructMetadataDescFileds {
    var mangledTypeName : StructMetadataDescName<CChar>
    var superclass : StructMetadataDescName<CChar>

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

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

//unsafeBitCast:谨慎使用,有更好的方法再来替换  按位强制转换
let ptr = unsafeBitCast(YYTeacher.self as Any.Type, to: UnsafeMutablePointer<StructMetadata>.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<Element> {
    var element : Element
    mutating func element(at i : Int) -> UnsafeMutablePointer<Element> {
        
        return withUnsafePointer(to: &self) { ptr in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(ptr).assumingMemoryBound(to:Element.self).advanced(by: i))
        }
    }
}

StructMetadataDescFileds中的fields字段改为:

var fields : StructMetadataDescFiledRecordT<StructMetadataDescFiledRecord>

调用时:

let fieldCount = fieldsPtr.pointee.numFields
for i in 0..<fieldCount {
    let filedRecordPtr = fieldsPtr.pointee.fields.element(at: i)
    let fieldName = String(cString: filedRecordPtr.pointee.FieldName.get())
    print("属性名\(i)" + fieldName)
}

获取属性值,原理如下:

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<T>(_ 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容