iOS runtime 機制解讀(結合 objc4 源碼)

歡迎訪問個人博客原文html

Runtime 是指將數據類型的肯定由編譯時推遲到了運行時。它是一套底層的純 C 語言 API,咱們平時編寫的 Objective-C 代碼,最終都會轉換成 runtime 的 C 語言代碼。ios

不過,runtime API 的實現是用 C++ 開發的(源碼中的實現文件都是 .mm 文件)。json

爲了更全面地理解 runtime 機制,咱們結合最新的objc4 源碼來進行解讀。數組

消息傳遞

咱們知道 Objective-C 是面向對象開發的,而 C 語言則是面向過程開發,這就須要將面向對象的類轉變成面向過程的結構體緩存

在 Objective-C 中,全部的消息傳遞中的「消息」都會被編譯器轉化爲:bash

id objc_msgSend ( id self, SEL op, ... );
複製代碼

好比執行一個對象的方法:[obj foo];,底層運行時會被編譯器轉化爲:objc_msgSend(obj, @selector(foo));併發

那麼方法內部的執行流程到底是怎麼樣的呢?我先來了解一些概念。app

概念

objc_object

Objective-C 對象是由 id 類型表示的,它本質上是一個指向 objc_object 結構體的指針。框架

typedef struct objc_object *id;

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

struct objc_object {
private:
    isa_t isa;
// public & private method...
}
複製代碼

咱們看到 objc_object 的結構體中只有一個對象,就是指向其類的 isa 指針。ide

當向一個對象發送消息時,runtime 會根據實例對象的 isa 指針找到其所屬的類。

objc_class

Objective-C 的類是由 Class 類型來表示的,它其實是一個指向 objc_class 結構體的指針。

typedef struct objc_class *Class;
複製代碼

objc_class 結構體中定義了不少變量:

struct objc_class : objc_object {
    // 指向類的指針(位於 objc_object)
    // Class ISA;
    // 指向父類的指針
    Class superclass;
    // 用於緩存指針和 vtable,加速方法的調用
    cache_t cache;             // formerly cache pointer and vtable
    // 存儲類的方法、屬性、遵循的協議等信息的地方
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // class_data_bits_t 結構體的方法,用於返回class_rw_t 指針()
    class_rw_t *data() { 
        return bits.data();
    }
    // other methods...
}

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;
    
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    
    Class firstSubclass;
    Class nextSiblingClass;
    
    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    // other methods
}
複製代碼

objc_class 繼承自 objc_object,所以它也擁有了 isa 指針。除此以外,它的結構體中還保存了指向父類的指針、緩存、實例變量列表、方法列表、遵照的協議等。

元類

元類(metaclass)是類對象的類,它的結構體和 objc_class 是同樣的。

因爲全部的類自身也是一個對象,咱們能夠向這個對象發送消息,好比調用類方法。那麼爲了調用類方法,這個類的 isa 指針必須指向一個包含類方法的一個 objc_class 結構體。而類對象中只存儲了實例方法,卻沒有類方法,這就引出了元類的概念,元類中保存了建立類對象以及類方法所需的全部信息。

爲了更方便理解,舉個例子:

- (void)eat;    // 一個實例方法
+ (void)sleep;  // 一個類方法

// 那麼實例方法須要由類對象來調用:
[person eat];
// 而類方法須要由元類來調用:
[Person sleep];
複製代碼

假如 person 對象也能調用 sleep 方法,那咱們就沒法區分它調用的就到底是 + (void)sleep; 仍是 - (void)sleep;

類對象是元類的實例,類對象的 isa 指針指向了元類。

這個說法可能有點繞,藉助這張經典的圖來理解:

當向對象發消息,runtime 會在這個對象所屬類方法列表中查找發送消息對應的方法,但當向類發送消息時,runtime 就會在這個類的 meta class 方法列表裏查找。全部的 meta class,包括 Root class,Superclass,Subclass 的 isa 都指向 Root class 的 meta class,這樣可以造成一個閉環。

Method(method_t)

Method 是一個指向 method_t 結構體的指針,咱們在 objc-private.hobjc-runtime-new.h 中找到關於它的定義:

typedef struct method_t *Method;
複製代碼
struct method_t {
    // 方法選擇器
    SEL name;
    // 類型編碼
    const char *types;
    // 方法實現的指針
    MethodListIMP imp;
}
複製代碼

因此 Method 和 SEL、IMP 的關係就是 Method = SEL + IMP + types。

關於 types 的寫法,參考 Type Encodings

SEL(objc_selector)

SEL 又稱方法選擇器,是一個指向 objc_selector 結構體的指針,也是 objc_msgSend 函數的第二個參數類型。

typedef struct objc_selector *SEL;
複製代碼

方法的 selector 用於表示運行時方法的名稱。代碼編譯時,會根據方法的名字(不包括參數)生成一個惟一的整型標識( Int 類型的地址),即 SEL。

一個類的方法列表中不能存在兩個相同的 SEL,這也是 Objective-C 不支持重載的緣由。

不一樣類之間能夠存在相同的 SEL,由於不一樣類的實例對象執行相同的 selector 時,會在各自的方法列表中去尋找本身對應的 IMP。

獲取 SEL 的方式有三種:

  • sel_registerName 函數
  • Objective-C 編譯器提供的 @selector() 方法
  • NSSeletorFromString() 方法

IMP

IMP 本質上就是一個函數指針,指向方法實現的地址

typedef void (*IMP)(void /* id, SEL, ... */ ); 
複製代碼

參數說明:

  • id:指向 self 的指針(若是是實例方法,則是類實例的內存地址;若是是類方法,則是指向元類的指針)
  • SEL:方法選擇器
  • ...:方法的參數列表

SEL 與 IMP 的關係相似於哈希表中 key 與 value 的關係。採用這種哈希映射的方式能夠加快方法的查找速度。

cache_t

cache_t 表示類緩存,是 object_class 的結構體變量之一。

struct cache_t {
    // 存放方法的數組
    struct bucket_t *_buckets;
    // 能存儲的最多數量
    mask_t _mask;
    // 當前已存儲的方法數量
    mask_t _occupied;
    // ...
}
複製代碼

爲了加速消息分發,系統會對方法和對應的地址進行緩存,就放在 cache_t 中。

實際運行中,大部分經常使用的方法都是會被緩存起來的,runtime 系統實際上很是快,接近直接執行內存地址的程序速度。

category_t

category_t 表示一個指向分類的結構體的指針。

struct category_t {
    // 是指類名,而不是分類名
    const char *name;
    // 要擴展的類對象,編譯期間是不會定義的,而是在運行時階段經過name對應到相應的類對象
    classref_t cls;
    // 實例方法列表
    struct method_list_t *instanceMethods;
    // 類方法列表
    struct method_list_t *classMethods;
    // 協議列表
    struct protocol_list_t *protocols;
    // 實例屬性
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    // 類(元類)屬性列表
    struct property_list_t *_classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
複製代碼

這裏涉及到一個經典問題:

分類中能夠添加實例變量/成員變量/屬性嗎?

首先,分類中沒法直接添加實例變量和成員變量

實踐一下,咱們就會發現,在分類中添加實例變量/成員變量,在編譯階段,就會報錯,但添加屬性是容許的。

這是由於在分類的結構體當中,沒有「實例變量/成員變量」的結構,可是有「屬性」的結構

那麼分類中就能夠直接添加屬性嗎?

其實也否則,雖然分類的 .h 中沒有報錯信息,.m 中卻報出了以下的警告,且運行時會報錯。

警告提示上代表有兩種解決方法:

第一種:用 @dynamic修飾。但實際上,@dynamic 修飾只是告訴編譯器,屬性的 setter 和 getter 方法會由用戶自行實現。但這樣作只能消除警告,沒法解決問題,運行時依然會崩潰。

第二種:給分類手動添加 setter 和 getter 方法,這是一種有效的方案。

咱們知道 @property = ivar + setter + getter

能夠經過 objc_setAssociatedObjectobjc_getAssociatedObject 向分類中動態添加屬性,具體實現見下文中的「關聯對象給分類增長屬性」

流程

消息傳遞流程

也就是查找 IMP 的過程:

  • 先從當前 class 的 cache 方法列表裏去查找。
  • 若是找到了,若是找到了就返回對應的 IMP 實現,並把當前的 class 中的 selector 緩存到 cache 裏面。
  • 若是類的方法列表中找不到,就到父類的方法列表中查找,一直找到 NSObject 類爲止。
  • 最後再找不到,就會進入動態方法解析和消息轉發的機制。

消息轉發

若是消息傳遞後仍沒法找到 IMP,就進入了消息轉發流程。

  1. 經過運行期的動態方法解析功能,咱們能夠在須要用到某個方法時再將其加入類中。
  2. 對象能夠把其沒法解讀的某些選擇子轉交給備用接受者來處理。
  3. 通過上述兩步以後,若是仍是沒有辦法處理選擇子,那就啓動完整的消息轉發機制。

動態方法解析

動態方法解析的兩個方法:

// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 添加實例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼

咱們再看看這兩個方法在源碼中的調用:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
	// 判斷是否是元類
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
		// 調用類的 resolveInstanceMethod 方法,動態添加實例方法
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
		// 調用元類的 resolveClassMethod 方法,動態添加類方法
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

下面看一個動態方法解析的例子。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");
}
複製代碼

能夠看到雖然沒有實現 foo 這個函數,可是咱們經過 class_addMethod 動態添加 fooMethod 函數,並執行 fooMethod 這個函數的IMP。

若是 resolveInstanceMethod: 方法返回 NO ,運行時就會移到下一步:forwardingTargetForSelector:

備用接收者

若是目標對象實現了 forwardingTargetForSelector: 方法,runtime 就會調用這個方法,給你把這個消息轉發給其餘接受者的機會。

實現一個備用接收者的例子以下:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函數
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 返回 NO,進入下一步轉發。
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        //返回 Person對象,讓 Person 對象接收這個消息
        return [Person new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end
複製代碼

上面的實現就是利用 forwardingTargetForSelector 把當前 ViewController 類的方法 foo 轉發給了備用接受者 Person 類去執行了。

完整的消息轉發

若是在上一步還沒法處理未知消息,惟一能作的就是啓用完整的消息轉發機制。

主要涉及到兩個方法:

  • 發送 methodSignatureForSelector進行方法簽名,這能夠將函數的參數類型和返回值封裝。若是返回 nil,runtime 會發出 doesNotRecognizeSelector 消息,程序同時崩潰。
  • 若是返回了一個函數簽名,runtime 就會建立一個 NSInvocation 對象併發送 forwardInvocation 消息給目標對象。

實現一個完整轉發的例子以下:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");
}

@end


@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 返回 NO,進入下一步轉發。
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 返回 nil,進入下一步轉發。
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 簽名,進入 forwardInvocation
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

@end
複製代碼

經過簽名,runtime 生成了一個對象 anInvocation,發送給方法 forwardInvocation,咱們在方法中讓 Person 對象執行 foo 函數。

消息轉發流程

以上就是 runtime 的三次轉發流程,下面列舉一下 runtime 的實際應用。

應用

關聯對象給分類增長屬性

關聯對象(Associated Objects) 是 Objective-C 運行時的特性,容許開發者向已經存在的類在擴展中添加自定義屬性。

關聯對象 runtime 提供了3個 API 接口:

// 獲取關聯的對象
id objc_getAssociatedObject(id object, const void *key);
// 設置關聯對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 移除關聯的對象
void objc_removeAssociatedObjects(id object);
複製代碼

參數說明:

  • object:被關聯的對象
  • key:關聯對象的惟一標識
  • value: 關聯的對象
  • policy:內存管理的策略

關於內存管理的策略,源碼中 runtime.h 這樣描述:

/* Associative References */

/** * Policies related to associative references. * These are options to objc_setAssociatedObject() */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. * The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object. * The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied. * The association is made atomically. */
};
複製代碼

咱們看看內存策略對應的屬性修飾。

內存策略 屬性修飾 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個關聯對象的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 指定一個關聯對象的強引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個關聯對象的 copy 引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個關聯對象的強引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個關聯對象的 copy 引用,能被原子化使用。

下面利用關聯對象實現一個「在分類中增長一個用 copy 修飾的非原子性屬性 prop的功能。

上文中,咱們已經知道分類中不能直接添加屬性,須要手動添加存取方法:

// NSObject+AssociatedObject.h

#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)

@property (nonatomic, copy) NSString *prop;

@end

// NSObject+AssociatedObject.m

#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>

// key 有三種常見寫法:
//
// 1. static void *propKey = &propKey;
// 2. static NSString *propKey = @"propKey";
// 3. static char propKey;

static NSString *propKey = @"propKey";

@implementation NSObject (AssociatedObject)

- (void)setProp:(NSString *)prop {
    objc_setAssociatedObject(self, &propKey, prop, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)prop {
    return objc_getAssociatedObject(self, &propKey);
}

@end
複製代碼

黑魔法添加和替換方法

黑魔法是方法交換(method swizzling),也就是交換方法的 IMP 實現。

通常是在 + (void)load; 中執行方法交換。由於它的加載時機較早,基本能確保方法已交換。

方法添加

在動態方法解析中已經提到了「方法添加」。

//class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
複製代碼

參數說明:

  • cls:被添加方法的類
  • name:添加的方法的名稱的 SEL
  • imp:方法的實現。該函數必須至少要有兩個參數,self,_cmd
  • types:類型編碼

方法替換

方法替換就是改變類的選擇子映射表。

若是要互換兩個已經寫好的方法實現,能夠用下面的函數

void method_exchangeImplementations(Method m1, Method m2);
複製代碼

方法實現能夠經過下面的函數得到:

void class_getInstanceMethod(Class aClass, SEL aSelector);
複製代碼

下面實現一個替換 ViewControllerviewDidLoad 方法的例子。

@implementation ViewController
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(msviewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        
        // 判斷 original 的方法是否已經實現,若是未實現,將 swizzledMethod 的實現和類型添加進 originalSelector 中
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            // 將 originalMethod 的實現和類型替換到 swizzledSelector 中
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            // 交換 originalMethod 和 swizzledMethod
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)msviewDidLoad {
    NSLog(@"msviewDidLoad");
    [self msviewDidLoad];
}

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];
}
@end
複製代碼

KVO 實現

KVO 全稱是 Key-value observing,也就是鍵值觀察者模式,它提供了一種當其它對象屬性被修改的時候能通知到當前對象的機制。

KVO 的實現也是依賴於 runtime 中的 isa-swizzling

當觀察某對象 A 時,KVO 機制動態建立一個新的名爲:NSKVONotifying_A 的新類,該類繼承自對象 A 的本類,且 KVO 爲 NSKVONotifying_A 重寫觀察屬性的 setter 方法,setter 方法會負責在調用原 setter 方法以前和以後,通知全部觀察對象屬性值的更改狀況。

舉個例子:

#import "ViewController.h"
#import <objc/runtime.h>
#import "A.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    A *a = [A new];
    NSLog(@"Before KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
    [a addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"After KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}

@end
複製代碼

程序運行的結果爲:

Before KVO: [a class] = A, a -> isa = A
After KVO: [a class] = A, a -> isa = NSKVONotifying_A
複製代碼

能夠看到當對 a 進行觀察後,雖然對象 aclass 仍是 A,isa 實際指向了它的子類 NSKVONotifying_A,來實現當前類屬性值改變的監聽;

因此當咱們從應用層面上看來,徹底沒有意識到有新的類出現,這是系統「隱瞞」了對 KVO 的底層實現過程,讓咱們誤覺得仍是原來的類。可是此時若是咱們建立一個新的名爲 NSKVONotifying_A 的類,就會發現系統運行到註冊 KVO 的那段代碼時程序就崩潰,由於系統在註冊監聽的時候動態建立了名爲 NSKVONotifying_A 的中間類,並指向這個中間類了。

那麼子類 NSKVONotifying_A 的 setter 方法裏具體實現了什麼?

KVO 的鍵值觀察通知依賴於 NSObject 的兩個方法:

  • -willChangeValueForKey::被觀察屬性發生改變之,改方法被調用,通知系統該 keyPath 的屬性值即將變動

  • -didChangeValueForKey::被觀察屬性發生改變之,改方法被調用,通知系統該 keyPath 的屬性值已經變動。方法 observeValueForKey:ofObject:change:context:也會被調用。且重寫觀察屬性的 setter 方法這種繼承方式的注入是在運行時而不是編譯時實現的。

所以,KVO 爲子類的觀察者屬性重寫調用存取方法的工做原理在代碼中至關於:

- (void)setName:(NSString *)name {
	// KVO 在調用存取方法以前總調用 
    [self willChangeValueForKey:@"name"];
	// 調用父類的存取方法 
    [super setValue:newName forKey:@"name"];
	// KVO 在調用存取方法以後總調用
    [self didChangeValueForKey:@"name"];
}
複製代碼

實現字典和模型之間的轉換(MJExtension)

原理

經過在 NSObject 的分類中添加方法 -initWithDict:

具體實現爲:用 runtime 提供的函數 class_copyPropertyList 獲取屬性列表,再遍歷 Model 自身全部屬性(經過 property_getName 函數得到屬性的名字,經過 property_getAttributes 函數得到屬性的類型)。若是屬性在 json 中有對應的值,則將其賦值。

源碼

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        // 一、獲取類的屬性及屬性對應的類型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /* * 例子 * name = value3 attribute = T@"NSString",C,N,V_value3 * name = value4 attribute = T^i,N,V_value4 */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            // 經過 property_getName 函數得到屬性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //經過 property_getAttributes 函數得到屬性類型
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        // 當即釋放properties指向的內存
        free(properties);

        // 二、根據類型給屬性賦值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}
複製代碼

實現 NSCoding 的自動歸檔和解檔

原理

Model 的基類中重寫方法:-initWithCoder:-encodeWithCoder:

具體實現爲:用 runtime 提供的函數 class_copyIvarList 獲取實例變量列表,再遍歷 Model 自身全部屬性,並對屬性進行 encodedecode 操做。

源碼

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}
複製代碼

JSPatch

JSPatch 是一款 iOS 動態更新框架,只須要在項目中引入引擎,就可使用 JavaScript 調用全部 Objective-C 原生接口,從而實現熱更新。

它經過完整的消息轉發實現了獲取參數的問題。

原理:當調用一個 NSObject 對象不存在的方法時,並不會立刻拋出異常,而是會通過多層轉發,層層調用對象的 -resolveInstanceMethod:-forwardingTargetForSelector:-methodSignatureForSelector:-forwardInvocation: 等方法,其中 -forwardInvocation: 裏的 NSInvocation 對象會保存了這個方法調用的全部信息,包括方法名、參數和返回值類型等。因此只須要讓被 JS 替換的方法最後都調用到 -forwardInvocation:,就能夠解決沒法拿到參數值的問題了。

相關文章
相關標籤/搜索