Swift底层进阶--008:Mirror反射 & 错误处理

Mirror反射

Mirror(反射):可以动态获取类型、成员信息,在运⾏时可以调⽤⽅法、属性等⾏为的特性。
对于⼀个纯Swift类来说,并不⽀持直接像OC那样使用Runtime操作。但Swift标准库依然提供了反射机制,用来访问成员信息。

访问成员信息
class LGTeacher {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()

let mirror = Mirror(reflecting: t)

for pro in mirror.children{
    print("\(pro.label):\(pro.value)")
}

//输出以下内容:
//Optional("age"):18
//Optional("name"):Zang

上述代码中,Mirror反射的是实例对象的成员信息,传入的参数必须是实例对象

传入一个类对象Mirror(reflecting: LGTeacher),编译报错

传入类对象

传入一个类的类型Mirror(reflecting: LGTeacher.self),获取不到任何成员信息

传入类的类型

查看Mirror定义

Mirror是一个结构体

Mirror

Mirrorinit方法,接收一个Any类型参数

init

Children是一个AnyCollection,接收一个泛型Mirror.Child

Children

Mirror.Child是一个元组类型

Mirror.Child

JSON解析
class LGTeacher {
    var age: Int = 18
    var name: String = "Zang"
}

func test(_ obj : Any) -> Any {
    
    let mirror = Mirror(reflecting: obj)
    
    guard !mirror.children.isEmpty else {
        return obj
    }
    
    var keyValue: [String: Any] = [:]
    
    for children in mirror.children {
        
        if let keyName = children.label {
            keyValue[keyName] = test(children.value)
        }
        else {
            print("children.label 为空")
        }
    }
    
    return keyValue
}

var t = LGTeacher()
print(test(t))

//输出以下内容:
//["name": "Zang", "age": 18]

上述代码中,成功的将实例对象t转为字典并输出,但在实际开发中,这样的代码写的相对丑陋,下面就来对它做一个简单的封装

抽取协议

我们预期在每一个属性下都能调用JSON解析的方法,所以可以将它抽取成一个协议,然后提供一个默认实现,让类遵守协议

protocol CustomJSONMap {
    func jsonMap() -> Any
}

extension CustomJSONMap{
    func jsonMap() -> Any{

        let mirror = Mirror(reflecting: self)
    
        guard !mirror.children.isEmpty else {
            return self
        }
    
        var keyValue: [String: Any] = [:]
    
        for children in mirror.children {
    
            if let value = children.value as? CustomJSONMap {
                
                if let keyName = children.label {
                    keyValue[keyName] = value.jsonMap()
                }
                else {
                    print("key是nil")
                }
            }
            else {
                print("当前-\(children.value)-没有遵守协议")
            }
        }
    
        return keyValue
    }
}

class LGTeacher : CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()
print(t.jsonMap())

//输出以下内容:
//当前-18-没有遵守协议
//当前-Zang-没有遵守协议
//[:]

上述代码中,因为agename分别为IntString类型,这些类并没有遵守CustomJSONMap协议,所以无法输出正确结果

extension Int: CustomJSONMap{}
extension String: CustomJSONMap{}

class LGTeacher: CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()
print(t.jsonMap())

//输出以下内容:
//["age": 18, "name": "Zang"]

修改代码,增加IntString类型的extension,让它们也遵守CustomJSONMap协议,打印结果符合预期

上述代码在实际开发中用来做JSON解析还有很大差距。代码本身并不完善,也无法支持复杂的嵌套类型,所以它只是基于Mirror做的简单案例,用来理解Mirror到底能做些什么。

错误处理
Error协议

Swift提供Error协议来标识当前应⽤程序发⽣错误的情况,不管是structClassenum都可以通过遵循这个协议来表示⼀个错误。Error的定义如下:

public protocol Error{
}

上述JSON解析案例中,有两个print分别打印未遵守协议key是nil,下面演示如何通过Error协议处理这两种异常情况

return
上述代码中,将JSONMapError.emptyKeyJSONMapError.notConformProtocol进行return,以此代替之前两个print。但对于jsonMap方法来说,由于返回值是Any类型,故此我们无法区分返回结果是解析成功的字典,还是错误的枚举

对于异常情况,可以使用throw关键字将错误抛出,将代码中的return改为throw

throw

上图使用throw编译报错,因为方法还没有声明成throws。需要在方法返回值前面增加throws关键字,告诉方法有错误抛出

throws

方法使用throws关键字修饰,调用该方法的代码编译报错。对于有错误抛出的方法,需要在调用方法前使用try关键字

enum JSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

protocol CustomJSONMap {
    func jsonMap() throws-> Any
}

extension CustomJSONMap{
    func jsonMap() throws-> Any{

        let mirror = Mirror(reflecting: self)
    
        guard !mirror.children.isEmpty else {
            return self
        }
    
        var keyValue: [String: Any] = [:]
    
        for children in mirror.children {
    
            if let value = children.value as? CustomJSONMap {
                
                if let keyName = children.label {
                    keyValue[keyName] = try value.jsonMap()
                }
                else {
                    throw JSONMapError.emptyKey
                }
            }
            else {
                throw JSONMapError.notConformProtocol
            }
        }
    
        return keyValue
    }
}

extension Int : CustomJSONMap{}
extension String : CustomJSONMap{}

class LGTeacher : CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()
print(try t.jsonMap())

到这⾥一个完整的Swift错误表达⽅式就完成了

try关键字

使⽤try关键字是Swift中错误处理最简便的方式,相当于帅锅。将异常向上抛出,抛给上层函数。使⽤try关键字有两个注意点:

  • try?:返回⼀个可选类型,这⾥的结果要么是成功,返回具体结果。要么是错误,返回nil。这种方式我们不关心具体是哪⼀类错误,统⼀返回nil
  • try!:表示你对这段代码有绝对的⾃信,这⾏代码绝对不会发⽣错误

IntString遵守CustomJSONMap协议的代码注释,打印try? t.jsonMap(),直接返回nil,看不出具体错误原因

try?

相同代码,使用try! t.jsonMap()测试,程序直接闪退

try!

使用try t.jsonMap(),将异常抛给上层函数,如果全程没有函数处理异常,最终抛给main函数也没办法处理,程序直接闪退

try

do...catch

Swiftdo...catch是错误处理的另一种方式

//extension Int : CustomJSONMap{}
//extension String : CustomJSONMap{}

class LGTeacher : CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()

do{
    try t.jsonMap()
}catch{
    print(error)
}

//输出以下内容:
//notConformProtocol

上述代码中,通过do作用域捕获异常,通过catch作用域处理异常,最终打印出错误类型notConformProtocol

LocalError协议

如果使⽤Error协议不能详尽表达错误信息,可以使⽤LocalError协议,定义如下:

public protocol LocalizedError : Error {

    //错误的描述
    /// A localized message describing what error occurred.
    var errorDescription: String? { get }

    //失败的原因
    /// A localized message describing the reason for the failure.
    var failureReason: String? { get }

    //恢复的建议
    /// A localized message describing how one might recover from the failure.
    var recoverySuggestion: String? { get }

    //给开发者的帮助
    /// A localized message providing "help" text if the user requests help.
    var helpAnchor: String? { get }
}

修改JSON解析案例,使用LocalizedError协议,打印具体的错误描述

enum JSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

extension JSONMapError: LocalizedError{
    var errorDescription: String?{
        switch self {
            case .emptyKey:
                return "key为空"
            case .notConformProtocol:
                return "没有遵守协议"
        }
    }
}

//extension Int : CustomJSONMap{}
//extension String : CustomJSONMap{}

class LGTeacher : CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()

do{
    try t.jsonMap()
}catch{
    print(error.localizedDescription)
}

//输出以下内容:
//没有遵守协议

上述代码中,为JSONMapError增加extension扩展,并遵守LocalizedError协议,在catch作用域中打印error.localizedDescription,最终输出错误描述:没有遵守协议

CustomError协议

CustomNSError相当于OC中的NSError,有三个默认属性:

public protocol CustomNSError : Error {

    /// The domain of the error.
    static var errorDomain: String { get }

    /// The error code within the given domain.
    var errorCode: Int { get }

    /// The user-info dictionary.
    var errorUserInfo: [String : Any] { get }
}

修改JSON解析案例,使用CustomNSError协议,打印错误码

enum JSONMapError: Error{
    case emptyKey
    case notConformProtocol
}

extension JSONMapError: CustomNSError{
    var errorCode: Int{
        switch self {
        case .emptyKey:
            return -1
        case .notConformProtocol:
            return -2
        }
    }
}

//extension Int : CustomJSONMap{}
//extension String : CustomJSONMap{}

class LGTeacher : CustomJSONMap {
    var age: Int = 18
    var name: String = "Zang"
}

var t = LGTeacher()

do{
    try t.jsonMap()
}catch{
    print("\(String(describing: (error as? CustomNSError)?.errorCode))")
}

//输出以下内容:
//Optional(-2)

上述代码中,为JSONMapError增加extension扩展,并遵守CustomNSError协议,在catch作用域中使用as?关键字将error强转为CustomNSError类型,并打印errorCode,最终输出错误码:Optional(-2)

Mirror源码解析
@_silgen_name关键字

@_silgen_name关键字用来改变当前方法的调用方式

swift项目中,创建test.c,里面声明lg_add函数,传入两个参数,返回参数相加的结果

lg_add

main.swift中,定义swift_lg_add方法,参数、返回值和test.clg_add一致,并加入@_silgen_name("lg_add")关键字声明,尝试调用swift_lg_add方法,发现最终会调用lg_add

swift_lg_add

源码解析

打开Mirror.swift,可以看到Mirror是一个结构体类型

Mirror

结构体有一个初始化方法,传入一个Any。这里判断subject是否符合CustomReflectable类型,符合由customMirror确定属性,不符合由系统生成

init

首先看一下由系统生成的Mirror,来到internalReflecting方法定义,首先通过_getNormalizedType方法获取类型,再通过_getChildCount方法获取属性大小,最后通过遍历将属性存储到集合中

internalReflecting

搜索_getNormalizedType方法,进入ReflectionMirror.swift文件。该方法用到@_silgen_name关键字声明,最终会调用swift_reflectionMirror_normalizedType方法

_getNormalizedType

来到swift_reflectionMirror_normalizedType方法定义,有三个参数,valueMirror实例,type是调用时通过type(of:)获取动态类型,T是自己传入的类型。最后调用call方法返回impltype属性,而implReflectionMirrorImpl结构

swift_reflectionMirror_normalizedType

来到call方法定义,将方法TpassedValue参数,传给unwrapExistential方法拿到type。如果传入的passedType不为空,就将传入的passedType赋值给type

call

来到unwrapExistential方法定义,通过getDynamicType方法获取当前的动态类型。通过内部方法调用,最终发现获取类型的本质是依赖Metadata元数据。

unwrapExistential

找到Metadata,由于类结构的Metadata过于复杂,我们下面以结构体TargetStructMetadata为例,它继承于TargetValueMetadata

TargetStructMetadata

来到TargetValueMetadata定义,它继承于TargetMetadata,除了继承下来的kind属性,还有一个ConstTargetMetadataPointer类型的description属性,而description属性记录的就是有关元数据的描述。ConstTargetMetadataPointer实际是一个泛型,需要重点分析的是TargetValueTypeDescription

TargetValueMetadata

下面介绍元数据的描述,进入TargetValueTypeDescription定义,它继承于TargetTypeContextDescription

TargetValueTypeDescription

进入TargetTypeContextDescription定义,里面有一个TargetRelativeDirectPointer类型的Name属性,用于记录当前类型的名称

TargetTypeContextDescription

进入TargetRelativeDirectPointer定义,它是RelativeDirectPointer类型的别名

TargetRelativeDirectPointer

进入RelativeDirectPointer定义,它是一个模板类,继承于RelativeDirectPointerImpl,传入类型TNullabletrueOffsetint32_t

RelativeDirectPointer

进入RelativeDirectPointerImpl定义,它有一个int32_t类型的RelativeOffset属性

RelativeDirectPointerImpl

还包含ValueTyPointerTy,其中ValueTy代表T的类型,PointerTy代表T的指针类型

ValueTy、PointerTy

回到RelativeDirectPointer定义,获取值ValueTy、获取指针PointerTy调用的都是this->get()方法

获取ValueTy、PointerTy

进入get方法,又回到RelativeDirectPointerImpl定义,方法内调用applyRelativeOffset方法

get

进入applyRelativeOffset方法,返回base + extendOffsetbase是当前指针地址,extendOffset是相对地址(偏移地址),最终相加得到偏移后的地址存储的是类型

applyRelativeOffset

回到call函数,可以看到很多依赖都是基于call函数。比如获取Class数据时,通过callClass方法得到一个ClassImpl。如果是元组类型,会得到一个TupleImpl。如果是结构体,会得到StructImpl

call

以结构体为例,进入StructImpl定义,继承于ReflectionMirrorImpl。其中isReflectable方法,也是通过Metadata获取getDescriotion,找到isReflectable判断是否支持反射

isReflectable

count方法,同样通过Metadata获取getDescriotion,找到NumFields获取属性总数

count

subscipt方法,获取属性名称和属性值。通过getFieldOffests()[i]得到一个属性的偏移值。通过getFiledAt方法拿到属性名称,bytes是当前value地址,加上fieldOffset偏移地址,就是属性的值

subscipt

进入getFiledAt方法,通过baseDescFields.get()获取fields,将fields赋值给FieldDescriptor类型的descriptor,通过descriptor.geyFields[index]获取一个属性field,最终通过field.getFieldName()拿到属性值的名称

getFiledAt

Fields也是一个相对指针的存储,类型是FieldDescriptor

FieldDescriptor

getFieldName方法调用的是FieldName.get(),在FieldRecord类里

FieldRecord

下面通过Swift代码仿写Mirror的实现进行原理解析

原理解析

定义RelativePointer结构体,仿照RelativeDirectPointerImplget方法,实现当前指针的偏移

struct RelativePointer<T> {
    var offset: Int32

    mutating func get() -> UnsafeMutablePointer<T>{
        let offset = self.offset

        return withUnsafePointer(to: &self) { p in
            return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: T.self))
        }
    }
}
  • offset:偏移地址
  • UnsafeMutablePointer:转换为UnsafeMutablePointer类型指针
  • UnsafeRawPointer(p):当前this
  • advanced:移动步长
  • numericCast:位的强转
  • assumingMemoryBound:假定内存绑定为T的类型

定义FieldRecordT结构体,实现类似getFiledAt方法,在连续内存空间中,移动步长,拿到每一个FieldRecord

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

定义StructMetadata结构体,相当于TargetStructMetadata

struct StructMetadata{
    var kind: Int
    var typeDescriptor: UnsafeMutablePointer<StructDescriptor>
}

定义StructDescriptor结构体,相当于TargetValueTypeDescription

struct StructDescriptor {
    let flags: Int32
    let parent: Int32
    var name: RelativePointer<CChar>
    var AccessFunctionPtr: RelativePointer<UnsafeRawPointer>
    var Fields: RelativePointer<FieldDescriptor>
    var NumFields: Int32
    var FieldOffsetVectorOffset: Int32
}

定义FieldDescriptor结构体,相当于FieldDescriptor

struct FieldDescriptor {
    var MangledTypeName: RelativePointer<CChar>
    var Superclass: RelativePointer<CChar>
    var kind: UInt16
    var fieldRecordSize: Int16
    var numFields: Int32
    //连续的存储空间
    var fields: FieldRecordT<FieldRecord>
}

定义FieldRecord结构体,相当于FieldRecord

struct FieldRecord {
    var Flags: Int32
    var MangledTypeName: RelativePointer<CChar>
    var FieldName: RelativePointer<CChar>
}

定义LGTeacher结构体

struct LGTeacher{
    var age = 18
    var name = "Zang"
}

使用unsafeBitCast内存按位转换,将LGTeacher的类型绑定到StructMetadata

let ptr = unsafeBitCast(LGTeacher.self as Any.Type, to: UnsafeMutablePointer<StructMetadata>.self)

获取类型

let namePtr = ptr.pointee.typeDescriptor.pointee.name.get()
print("类型:\(String(cString: namePtr))")

//输出以下结果:
//类型:LGTeacher

获取属性大小

let fieldDescriptorPtr = ptr.pointee.typeDescriptor.pointee.Fields.get()
print("属性大小:\(ptr.pointee.typeDescriptor.pointee.NumFields)")

//输出以下结果:
//属性大小:2

遍历出所有属性名称

for index in 0..<ptr.pointee.typeDescriptor.pointee.NumFields {
    
    let recordPtr = fieldDescriptorPtr.pointee.fields.element(at: Int(index))
    let valOffset=recordPtr.pointee.FieldName.get().pointee

    print("属性\(index):\(String(cString: recordPtr.pointee.FieldName.get()))")
}

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

推荐阅读更多精彩内容