Swift之Runtime探究 & 錯(cuò)誤處理 & 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()

通過執(zhí)行上面代碼,可知:
①屬性和方法前+@objc:獲取到方法列表(:teach,:age, :setAge:)和屬性列表(age)
特點(diǎn):無意義,不能提供給OC類使用
②繼承NSObject去掉@objc:只能獲取到方法列表中的:init方法
特點(diǎn):獲取不到方法列表和屬性列表
③繼承NSObject且屬性和方法前+@objc:獲取到方法列表(:teach,:init,:age, :setAge:)和屬性列表(age)

總結(jié)如下:

  • 對(duì)于純Swift類來說,沒有動(dòng)態(tài)特性。方法和屬性前加任何修飾符的情況下,該類不具備所謂的 Runtime 特性。
  • 對(duì)于純Swift類來說,如果方法和屬性前沒有添加 @objc 標(biāo)識(shí),是能通過 Runtime API 來獲取到方法列表屬性列表的。
  • 在方法和屬性前添加了 @objc 標(biāo)識(shí)的前提下,想要在OC中使用,還必須繼承自NSObject;如果想要方法交換,還需要加上dynamic標(biāo)識(shí),否則只是暴露給OC并沒有動(dòng)態(tài)性。
  • 擴(kuò)展:通過查看源碼可知Swift有默認(rèn)的基類SwiftObject,且實(shí)現(xiàn)了NSObject;Swift為了和OC交互,內(nèi)部保留了OC數(shù)據(jù)結(jié)構(gòu)(isarefCounts);繼承NSObject是為了聲明這是一個(gè)和OC交互的類幫助編譯器判斷該類在編譯過程中走哪些分支。
  • 反射

    反射就是在運(yùn)行時(shí)允許動(dòng)態(tài)訪問類型、成員信息等行為的特性,作用于對(duì)象;Swift標(biāo)準(zhǔn)庫提供了反射機(jī)制,對(duì)于OCRuntime來說,有很多方式來動(dòng)態(tài)獲取修改代碼,反射對(duì)其不值一提(所以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)")
}

上面的代碼可以獲取到對(duì)象t成員屬性。

  • 反射的使用
1. 封裝一個(gè)基礎(chǔ)類的JSON解析方法
  • 將JSON解析方法放在協(xié)議中,需要解析JSON的及其屬性實(shí)現(xiàn)該協(xié)議即可
protocol JSONMapProtocol {
    func jsonMap() -> Any
}
  • 為了方便使用,不用在需要的地方一一實(shí)現(xiàn)該方法,給協(xié)議一個(gè)默認(rèn)實(shí)現(xiàn)
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 {
                    // 遞歸調(diào)用,若有多層,則深度解析(所以需要解析的屬性也要遵守該協(xié)議)
                    resultDict[keyName] = value.jsonMap() 
                } else {
                    print("No keys")
                }
            } else {
                print("No Comply JSONMapProtocol") // 未遵守JSONMapProtocol
            }
        }
        return resultDict
    }
}
  • 調(diào)用該方法
// 屬性遵守協(xié)議
extension String:JSONMapProtocol{}
extension Int:JSONMapProtocol{}

// 類遵守協(xié)議
class YYTeacher: JSONMapProtocol {
    var age : Int = 18
    var name = "YY"
}

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

// 調(diào)用
let mathT = YYMathTeacher()
print(mathT.jsonMap())

得到結(jié)果["t": ["name": "YY", "age": 18], "teachType": 1]

2. 在上面JSON解析基礎(chǔ)類的基礎(chǔ)上,封裝錯(cuò)誤處理
  • 定義錯(cuò)誤
// 錯(cuò)誤類型
enum JSONMapError:Error {
    case emptyError
    case noComformProtocolError
}
// 錯(cuò)誤信息描述
extension JSONMapError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .emptyError:
            return "空錯(cuò)誤"
        case .noComformProtocolError:
            return "未遵守協(xié)議"
        }
    }
}

  • 解析方法若出錯(cuò),通過throws拋出錯(cuò)誤
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
    }
}
  • 調(diào)用該方法
  1. 通過try、try?、try!處理錯(cuò)誤
    try:將錯(cuò)誤拋給上層函數(shù),若上層函數(shù)未處理,則崩潰跳轉(zhuǎn)至上層函數(shù)所在的文件;若要處理則通過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?:返回一個(gè)可選類型,會(huì)向拋出,成功則返回具體的字典值;錯(cuò)誤則統(tǒng)一返回nil
try!:保證絕對(duì)不會(huì)出錯(cuò)則用try!,如果出錯(cuò)則編譯失敗

/** 
 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代碼塊進(jìn)行捕獲并處理異常
extension String:JSONMapProtocol{}
extension Int:JSONMapProtocol{}

class YYTeacher: JSONMapProtocol {
    var age : Int = 18
    var name = "YY"
    var height = 1.80 //新加未遵守協(xié)議的屬性
}

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轉(zhuǎn)為JSONMapError再調(diào)用其errorDescription屬性
    if let jsonMapError = error as? JSONMapError {
        print(jsonMapError.errorDescription!)
    } else {
        print(error.localizedDescription)
    }
}

得到結(jié)果為:未遵守協(xié)議(因?yàn)檫@里height屬性為Double類型,遵守JSONMapProtocol

擴(kuò)展CustomNSError用來橋接原來OC的NSError中的code、domain、UserInfo;如果想讓我們的自定義Error可以轉(zhuǎn)成NSError,實(shí)現(xiàn)CustomNSError就可以完整的as成NSError。

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

通過查看源碼可知:Mirror實(shí)際上是一個(gè)Struct

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

方法里面在獲取對(duì)象的類型屬性個(gè)數(shù)

@_silgen_name:可以對(duì)于某些簡單的代碼,直接跳過橋接文件.h頭文件C代碼交互。

  • .c文件中聲明并實(shí)現(xiàn)一個(gè)方法num_add
int num_add(int a, int b) {
    return  a + b;
}
  • Swift文件中,需要橋接文件和.h頭文件時(shí)也能訪問該方法
@_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語言中函數(shù)的符號(hào)是_ + 函數(shù)名,所以多個(gè)C文件中不可能存在多個(gè)名稱一樣的函數(shù),Swift可以放心使用@_silgen_name來與之交互
  1. 通過源碼分析Struct結(jié)構(gòu)來了解在Swift的反射中,怎么確定類型?

模擬Struct源碼結(jié)構(gòu)來分析:是如何將一個(gè)自定義的struct的Metadata綁定到struct結(jié)構(gòu)的(反射中取類型以及type(of:)的原理)?

struct StructMetadata {
    var kind : Int32
    var desc : UnsafeMutablePointer<StructMetadataDesc>
}

// 對(duì)metadata的描述
struct StructMetadataDesc {
    var flags : Int32
    var parent : Int32
    var name : StructMetadataDescName<CChar>
}

// type的名稱
struct StructMetadataDescName<T> {
    var offset : Int32
    // 其實(shí)就是取字符串存儲(chǔ)的地址
    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:謹(jǐn)慎使用,有更好的方法再來替換  按位強(qiáng)制轉(zhuǎn)換
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

下面用一張圖幫助理解:

其實(shí),StructMetadataDesc中的name就是內(nèi)存地址0x100(相當(dāng)于char *),StructMetadataDescName<T>中的offset為0x10。

  1. 模擬反射中獲取屬性個(gè)數(shù)的原理:
    更深入查看源碼來補(bǔ)充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 屬性的個(gè)數(shù)

源碼中也是這樣實(shí)現(xiàn)的:

  1. 模擬反射中獲取屬性的名稱和值的原理,先查看源碼實(shí)現(xiàn):

模擬實(shí)現(xiàn)之前,先查看源碼中Fields字段的類型:

FieldDescriptor 中的 getFields()返回的是一個(gè)裝FieldRecord的數(shù)組,其實(shí)就是返回的一塊連續(xù)內(nèi)存空間,存儲(chǔ)每一個(gè)FieldRecord;所以在模擬實(shí)現(xiàn)時(shí),直接用一個(gè)字段來表示這塊連續(xù)內(nèi)存。

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()一個(gè)數(shù)組
    var fields : StructMetadataDescFiledRecord // 存儲(chǔ)的一塊連續(xù)內(nèi)存空間的首地址
}
// class FieldRecord
struct StructMetadataDescFiledRecord {
    var flags : Int32
    var MangledTypeName : StructMetadataDescName<CChar>
    var FieldName : StructMetadataDescName<CChar>
}

在上面獲取類型及屬性個(gè)數(shù)的基礎(chǔ)上調(diào)用:

//unsafeBitCast:謹(jǐn)慎使用,有更好的方法再來替換  按位強(qiáng)制轉(zhuǎn)換
let ptr = unsafeBitCast(YYTeacher.self as Any.Type, to: UnsafeMutablePointer<StructMetadata>.self)
//  存儲(chǔ)的一塊連續(xù)內(nèi)存空間的首地址
var fieldsPtr = ptr.pointee.desc.pointee.Fields.get()
// 通過類型轉(zhuǎn)換再移動(dòng)步長的方式來獲取每個(gè)屬性
// 因?yàn)橐呀?jīng)轉(zhuǎn)換了類型,即已知類型,所以這里移動(dòng)的步長為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)

優(yōu)化:上面的方式,只能通過修改步長來一次獲取到一個(gè)屬性,如何一次性得到每一個(gè)屬性?

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>

調(diào)用時(shí):

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

// 用來表示一塊連續(xù)的內(nèi)存空間
// start : structmetadata的首地址 + 偏移量---->連續(xù)內(nèi)存空間的首地址
// count : 連續(xù)內(nèi)存空間中存的個(gè)數(shù)
var bufferPtr = UnsafeBufferPointer(start: UnsafeRawPointer(UnsafeRawPointer(ptr).assumingMemoryBound(to: Int.self).advanced(by: numericCast(offset))).assumingMemoryBound(to: Int32.self), count: Int(fieldsCount))
 
// 結(jié)構(gòu)體實(shí)例var t1 = YYTeacher() t1的內(nèi)存地址
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反射的原理。

注意:源碼調(diào)試的過程中所有dlopen錯(cuò)誤都是由于路徑錯(cuò)誤(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:類的實(shí)例(一定能為nil),類的類型,僅能實(shí)現(xiàn)的協(xié)議。AnyObject!表示為可選類型

// 此時(shí)代表YYTeacher類的實(shí)例對(duì)象
var t : AnyObject = YYTeacher()

// 此時(shí)代表YYTeacher類的類型
var t1 : AnyObject = YYTeacher.self
// 此時(shí)也代表一個(gè)類型 NSNumber類型
var age : AnyObject = 10 as NSNumber

// 此時(shí)代表僅class才能實(shí)現(xiàn)的協(xié)議
protocol TestPro : AnyObject {}
//如果這里改為struct實(shí)現(xiàn)該協(xié)議會(huì)報(bào)錯(cuò)
class TestCls : TestPro {}

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

// 此時(shí)如果使用AnyObject則會(huì)報(bào)錯(cuò)
var arr : [Any] = [1, true, "a"]

AnyClass:任意實(shí)例對(duì)象類型,通過查看源碼可知typealias AnyClass = AnyObject.Type,即任意類的元類型,任意類的類型都隱式遵守這個(gè)協(xié)議。
T.self:如果T是實(shí)例,則返回的是它本身;如果T是,則返回的是其Metadata。
T.Type:一種類型,如果T是,T.selfT.Type類型(Metadata);如果T是實(shí)例,T.self實(shí)例的類型。

//這個(gè)self就是YYTeacher.Type類型
var t = YYTeacher.self
// 此時(shí)self是YYTeacher類型
var t1 = t.self

type(of:):用來獲取一個(gè)值運(yùn)行時(shí)期的動(dòng)態(tài)類型(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的參數(shù)類型為Any,age作為參數(shù)傳進(jìn)去,static type為Any
test(age)

特殊情況:有協(xié)議&泛型的情況下,type(of:)無法拿到真實(shí)的類型,需要作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時(shí),YYTeacher;有as Any時(shí),YYTeacher
test(t1)//無as Any時(shí),YYProtocol;有as Any時(shí),YYTeacher
  • 擴(kuò)展:寄存器就是用來放東西的,既可以放地址,也可以放地址中的。
    x8:取的是寄存器的地址 ==po withUnsafePointer(to: &t){print($0)}
    [x8]:取的是寄存器中的 ==po t
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容