來自一名大佬的runtime解讀

訪問某大神的博客,做以記錄,以備日後觀看找不到鏈接


一、前言:

如果你沒有Objective-C基礎,請學習了基礎的iOS開發再來,這個1小時是給有一定iOS基礎的童鞋的。如果你是大牛或者你感覺Objective-C Runtime太簡單不用1小時學習的,也請您繞道,這或許只是我的私人筆記了。

請跟着教程“一步步來”,請不要大概地掃兩眼就說看不懂——以這種態度寫成什麼樣你也看不懂。這是1小時入門教程,請不要試圖在1分鐘內入門!

二、本文目標:

1小時讓你知道什麼是Objective-C Runtime,並對它有一定的基本瞭解,可以在開發過程中運用自如。

三、Objective-C Runtime到底是什麼東西?

簡而言之,Objective-C Runtime是一個將C語言轉化爲面嚮對象語言的擴展。
我們將C++和Objective進行對比,雖然C++和Objective-C都是在C的基礎上加入面向對象的特性擴充而成的程序設計語言,但二者實現的機制差異很大。C++是基於靜態類型,而Objective-C是基於動態運行時類型。也就是說用C++編寫的程序編譯時就直接編譯成了可令機器讀懂的機器語言;用Objective-C編寫的程序不能直接編譯成可令機器讀懂的機器語言,而是在程序運行的時候,通過Runtime把程序轉爲可令機器讀懂的機器語言。也就是說用C++編寫的程序通過編譯器直接把函數地址硬編碼進入可執行文件;而Objective-C無法通過編譯器直接把函數地址硬編碼進入可執行文件,而是在程序運行的時候,利用Runtime根據條件判斷作出決定。函數標識與函數過程的真正內容之間的關聯可以動態修改。Runtime是Objective不可缺少的重要一部分。

傳送門->runtime源碼

四、Objective-C的元素認知

4.1 id和Class

打開/Public Headers/objc.h文件可以看到如下定義:

#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

Class是一個指向objc_class結構體的指針,而id是一個指向objc_object結構體的指針,其中的isa是一個指向objc_class結構體的指針。其中的id就是我們所說的對象,Class就是我們所說的類。

打開/Public Headers/runtime.h文件
objc_class的定義如下:

typedef struct objc_class *Class;
struct objc_class { 
 Class isa                                 OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
 Class super_class                         OBJC2_UNAVAILABLE; // 父類
 const char *name                          OBJC2_UNAVAILABLE; // 類名
 long version                              OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0,可以通過runtime函數class_setVersion或者class_getVersion進行修改、讀取
 long info                                 OBJC2_UNAVAILABLE; // 類信息,供運行時期使用的一些位標識,如CLS_CLASS (0x1L) 表示該類爲普通 class,其中包含實例方法和變量;CLS_META (0x2L) 表示該類爲 metaclass,其中包含類方法;
 long instance_size                        OBJC2_UNAVAILABLE; // 該類的實例變量大小(包括從父類繼承下來的實例變量)
 struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE; // 該類的成員變量地址列表
 struct objc_method_list **methodLists     OBJC2_UNAVAILABLE; // 方法地址列表,與 info 的一些標誌位有關,如CLS_CLASS (0x1L),則存儲實例方法,如CLS_META (0x2L),則存儲類方法;
 struct objc_cache *cache                  OBJC2_UNAVAILABLE; // 緩存最近使用的方法地址,用於提升效率;
 struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE; // 存儲該類聲明遵守的協議的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */

由以上代碼可見,類與對象的區別就是類比對象多了很多特徵成員,類也可以當做一個objc_object來對待,也就是說類和對象都是對象,分別稱作類對象(class object)和實例對象(instance object),這樣我們就可以區別對象和類了。

isa:objc_object(實例對象)中isa指針指向的類結構稱爲class(也就是該對象所屬的類)其中存放着普通成員變量與動態方法(“-”開頭的方法);此處isa指針指向的類結構稱爲metaclass,其中存放着static類型的成員變量與static類型的方法(“+”開頭的方法)。

super_class: 指向該類的父類的指針,如果該類是根類(如NSObject或NSProxy),那麼super_class就爲nil。

類與對象的繼承層次關係如圖(圖片源自網絡):

objective-runtime-1

所有的metaclass中isa指針都是指向根metaclass,而根metaclass則指向自身。根metaclass是通過繼承根類產生的,與根class結構體成員一致,不同的是根metaclass的isa指針指向自身。

4.2 SEL

SEL是selector在Objective-C中的表示類型。selector可以理解爲區別方法的ID。

typedef struct objc_selector *SEL;

objc_selector的定義如下:

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 名稱
    char *types;                      OBJC2_UNAVAILABLE;// 類型
};

name和types都是char類型。

4.3 IMP

終於到IMP了,它在objc.h中得定義如下:

typedef id (*IMP)(id, SEL, ...);

IMP是“implementation”的縮寫,它是由編譯器生成的一個函數指針。當你發起一個消息後(下文介紹),這個函數指針決定了最終執行哪段代碼。

4.4 Method

Method代表類中的某個方法的類型。

typedef struct objc_method *Method;

objc_method的定義如下:

struct objc_method {
    SEL method_name                   OBJC2_UNAVAILABLE; // 方法名
    char *method_types                OBJC2_UNAVAILABLE; // 方法類型
    IMP method_imp                    OBJC2_UNAVAILABLE; // 方法實現
}

方法名method_name類型爲SEL,上文提到過。
方法類型method_types是一個char指針,存儲着方法的參數類型和返回值類型。
方法實現method_imp的類型爲IMP,上文提到過。

4.5 Ivar

Ivar代表類中實例變量的類型

typedef struct objc_ivar *Ivar;

objc_ivar的定義如下:

struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; // 變量名
    char *ivar_type                   OBJC2_UNAVAILABLE; // 變量類型
    int ivar_offset                   OBJC2_UNAVAILABLE; // 基地址偏移字節
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE; // 佔用空間
#endif
}

4.6 objc_property_t

objc_property_t是屬性,它的定義如下:

typedef struct objc_property *objc_property_t;

objc_property是內置的類型,與之關聯的還有一個objc_property_attribute_t,它是屬性的attribute,也就是其實是對屬性的詳細描述,包括屬性名稱、屬性編碼類型、原子類型/非原子類型等。它的定義如下:

typedef struct {
    const char *name; // 名稱
    const char *value;  // 值(通常是空的)
} objc_property_attribute_t;

4.7 Cache

Catch的定義如下:

typedef struct objc_cache *Cache

objc_cache的定義如下:

struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};

mask: 指定分配cache buckets的總數。在方法查找中,Runtime使用這個字段確定數組的索引位置。
occupied: 實際佔用cache buckets的總數。
buckets: 指定Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個數組可能會隨着時間而增長。
objc_msgSend(下文講解)每調用一次方法後,就會把該方法緩存到cache列表中,下次的時候,就直接優先從cache列表中尋找,如果cache沒有,才從methodLists中查找方法。

4.8 Catagory

這個就是我們平時所說的類別了,很熟悉吧。它可以動態的爲已存在的類添加新的方法。
它的定義如下:

typedef struct objc_category *Category;

objc_category的定義如下:

struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 類別名稱
    char *class_name                              OBJC2_UNAVAILABLE; // 類名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 實例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 類方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 協議列表
}

因爲是入門,以上就列舉這些吧!

五、Objective-C的消息傳遞

5.1 基本消息傳遞

在面向對象編程中,對象調用方法叫做發送消息。在編譯時,程序的源代碼就會從對象發送消息轉換成Runtime的objc_msgSend函數調用。
例如某實例變量receiver實現某一個方法oneMethod

[receiver oneMethod];

Runtime會將其轉成類似這樣的代碼

objc_msgSend(receiver, selector);

具體會轉換成什麼代碼呢?
Runtime會根據類型自動轉換成下列某一個函數:
objc_msgSend:普通的消息都會通過該函數發送
objc_msgSend_stret:消息中有數據結構作爲返回值(不是簡單值)時,通過此函數發送和接收返回值
objc_msgSendSuper:和objc_msgSend類似,這裏把消息發送給父類的實例
objc_msgSendSuper_stret:和objc_msgSend_stret類似,這裏把消息發送給父類的實例並接收返回值
當消息被髮送到實例對象時,是如圖所示處理的(圖片源自網絡):

objective-runtime-2

objc_msgSend函數的調用過程:

  • 第一步:檢測這個selector是不是要忽略的。
  • 第二步:檢測這個target是不是nil對象。nil對象發送任何一個消息都會被忽略掉。
  • 第三步:
    1.調用實例方法時,它會首先在自身isa指針指向的類(class)methodLists中查找該方法,如果找不到則會通過class的super_class指針找到父類的類對象結構體,然後從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查找,直至根class;
    2.當我們調用某個某個類方法時,它會首先通過自己的isa指針找到metaclass,並從其中methodLists中查找該類方法,如果找不到則會通過metaclass的super_class指針找到父類的metaclass對象結構體,然後從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查找,直至根metaclass;
  • 第四部:前三部都找不到就會進入動態方法解析(看下文)。

5.2 消息動態解析

動態解析流程圖(圖片來自網絡):

objective-runtime-6

請參照圖片品味以下步驟(實例請看下文《6.6 蒼老師的唱歌篇》):

  • 第一步:通過resolveInstanceMethod:方法決定是否動態添加方法。如果返回Yes則通過class_addMethod動態添加方法,消息得到處理,結束;如果返回No,則進入下一步;
  • 第二步:這步會進入forwardingTargetForSelector:方法,用於指定備選對象響應這個selector,不能指定爲self。如果返回某個對象則會調用對象的方法,結束。如果返回nil,則進入第三部;
  • 第三部:這步我們要通過methodSignatureForSelector:方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進入下一步;
  • 第四部:這步調用forwardInvocation:方法,我們可以通過anInvocation對象做很多處理,比如修改實現方法,修改響應對象等,如果方法調用成功,則結束。如果失敗,則進入doesNotRecognizeSelector方法,若我們沒有實現這個方法,那麼就會crash。

到這裏大家可能暈乎乎的,下面看實戰篇吧!蒼老師必須讓你懂!

六、Runtime實戰

請大家放心,以下所有實戰篇,在最後都會分享Demo給大家!

6.1 蒼老師問好篇

蒼老師見到我們廣大的粉絲們,第一反應當然是:大家好!

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

// 自定義一個方法
void sayFunction(id self, SEL _cmd, id some) {
    NSLog(@"%@歲的%@說:%@", object_getIvar(self, class_getInstanceVariable([self class], "_age")),[self valueForKey:@"name"],some);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 動態創建對象 創建一個Person 繼承自 NSObject類
        Class People = objc_allocateClassPair([NSObject class], "Person", 0);

        // 爲該類添加NSString *_name成員變量
        class_addIvar(People, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
        // 爲該類添加int _age成員變量
        class_addIvar(People, "_age", sizeof(int), sizeof(int), @encode(int));

        // 註冊方法名爲say的方法
        SEL s = sel_registerName("say:");
        // 爲該類增加名爲say的方法
        class_addMethod(People, s, (IMP)sayFunction, "v@:@");

        // 註冊該類
        objc_registerClassPair(People);

        // 創建一個類的實例
        id peopleInstance = [[People alloc] init];

        // KVC 動態改變 對象peopleInstance 中的實例變量
        [peopleInstance setValue:@"蒼老師" forKey:@"name"];

        // 從類中獲取成員變量Ivar
        Ivar ageIvar = class_getInstanceVariable(People, "_age");
        // 爲peopleInstance的成員變量賦值
        object_setIvar(peopleInstance, ageIvar, @18);

        // 調用 peopleInstance 對象中的 s 方法選擇器對於的方法
        // objc_msgSend(peopleInstance, s, @"大家好!"); // 這樣寫也可以,請看我博客說明
        ((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好");

        peopleInstance = nil; //當People類或者它的子類的實例還存在,則不能調用objc_disposeClassPair這個方法;因此這裏要先銷燬實例對象後才能銷燬類;

        // 銷燬類
        objc_disposeClassPair(People);

    }
    return 0;
}

最後的結果是:18歲的蒼老師說:大家好!
在使用

objc_msgSend(peopleInstance, s, @"大家好!");

默認會出現以下錯誤:
objc_msgSend()報錯Too many arguments to function call ,expected 0,have3
直接通過objc_msgSend(self, setter, value)是報錯,說參數過多。
請這樣解決:
Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改爲 NO
當然你也可以這樣寫(推薦):

((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好");

強制轉換objc_msgSend函數類型爲帶三個參數且返回值爲void函數,然後才能傳三個參數。
此實戰內容是,動態創建一個類,並創建成員變量和方法,最後賦值成員變量併發送消息。其中成員變量的賦值使用了KVC和object_setIvar函數兩種方式,這些東西大家舉一反三就可以了。

Demo傳送門->6.1蒼老師問好篇Demo

6.2 蒼老師的特徵篇

蒼老師在大家心目中應該有很多特徵吧,下面我們通過代碼來獲取蒼老師的特徵。
People.h文件

@interface People : NSObject
{
    NSString *_occupation;
    NSString *_nationality;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;

- (NSDictionary *)allProperties;
- (NSDictionary *)allIvars;
- (NSDictionary *)allMethods;

@end

People.m文件

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People

- (NSDictionary *)allProperties
{
    unsigned int count = 0;

    // 獲取類的所有屬性,如果沒有屬性count就爲0
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    NSMutableDictionary *resultDict = [@{} mutableCopy];

    for (NSUInteger i = 0; i < count; i ++) {

        // 獲取屬性的名稱和值
        const char *propertyName = property_getName(properties[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        id propertyValue = [self valueForKey:name];

        if (propertyValue) {
            resultDict[name] = propertyValue;
        } else {
            resultDict[name] = @"字典的key對應的value不能爲nil哦!";
        }
    }

    // 這裏properties是一個數組指針,我們需要使用free函數來釋放內存。
    free(properties);

    return resultDict;
}

- (NSDictionary *)allIvars
{
    unsigned int count = 0;

    NSMutableDictionary *resultDict = [@{} mutableCopy];

    Ivar *ivars = class_copyIvarList([self class], &count);

    for (NSUInteger i = 0; i < count; i ++) {

        const char *varName = ivar_getName(ivars[i]);
        NSString *name = [NSString stringWithUTF8String:varName];
        id varValue = [self valueForKey:name];

        if (varValue) {
            resultDict[name] = varValue;
        } else {
            resultDict[name] = @"字典的key對應的value不能爲nil哦!";
        }

    }

    free(ivars);

    return resultDict;
}

- (NSDictionary *)allMethods
{
    unsigned int count = 0;

    NSMutableDictionary *resultDict = [@{} mutableCopy];

    // 獲取類的所有方法,如果沒有方法count就爲0
    Method *methods = class_copyMethodList([self class], &count);

    for (NSUInteger i = 0; i < count; i ++) {

        // 獲取方法名稱
        SEL methodSEL = method_getName(methods[i]);
        const char *methodName = sel_getName(methodSEL);
        NSString *name = [NSString stringWithUTF8String:methodName];

        // 獲取方法的參數列表
        int arguments = method_getNumberOfArguments(methods[i]);

        resultDict[name] = @(arguments-2);
    }

    free(methods);

    return resultDict;
}

@end

在main.m中運行以下代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        People *cangTeacher = [[People alloc] init];
        cangTeacher.name = @"蒼井空";
        cangTeacher.age = 18;
        [cangTeacher setValue:@"老師" forKey:@"occupation"];

        NSDictionary *propertyResultDic = [cangTeacher allProperties];
        for (NSString *propertyName in propertyResultDic.allKeys) {
            NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyResultDic[propertyName]);
        }

        NSDictionary *ivarResultDic = [cangTeacher allIvars];
        for (NSString *ivarName in ivarResultDic.allKeys) {
            NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarResultDic[ivarName]);
        }

        NSDictionary *methodResultDic = [cangTeacher allMethods];
        for (NSString *methodName in methodResultDic.allKeys) {
            NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodResultDic[methodName]);
        }

    }
    return 0;
}

最後的輸出結果如下:

objective-runtime-3

是不是有點失望,我沒有加一些特殊的技能,留給下文了。此實戰內容是通過蒼老師的一些特徵學習:如何獲取對象所有的屬性名稱和屬性值、獲取對象所有成員變量名稱和變量值、獲取對象所有的方法名和方法參數數量。

Demo傳送門->6.2蒼老師的特徵篇Demo

6.3 蒼老師增加新技能篇

蒼老師要通過Category和Associated Objects增加技能了,快看!
創建People+Associated.h文件如下:

#import "People.h"

typedef void (^CodingCallBack)();

@interface People (Associated)

@property (nonatomic, strong) NSNumber *associatedBust; // 胸圍
@property (nonatomic, copy) CodingCallBack associatedCallBack;  // 寫代碼

@end

People+Associated.m如下:

#import "People+Associated.h"

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People (Associated)

- (void)setAssociatedBust:(NSNumber *)bust
{
    // 設置關聯對象
    objc_setAssociatedObject(self, @selector(associatedBust), bust, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSNumber *)associatedBust
{
    // 得到關聯對象
    return objc_getAssociatedObject(self, @selector(associatedBust));
}

- (void)setAssociatedCallBack:(CodingCallBack)callback {
    objc_setAssociatedObject(self, @selector(associatedCallBack), callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (CodingCallBack)associatedCallBack {
    return objc_getAssociatedObject(self, @selector(associatedCallBack));
}

@end

在main.m中運行以下代碼

#import "People.h"
#import "People+Associated.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        People *cangTeacher = [[People alloc] init];
        cangTeacher.name = @"蒼井空";
        cangTeacher.age = 18;
        [cangTeacher setValue:@"老師" forKey:@"occupation"];
        cangTeacher.associatedBust = @(90);
        cangTeacher.associatedCallBack = ^(){

            NSLog(@"蒼老師要寫代碼了!");

        };
        cangTeacher.associatedCallBack();

        NSDictionary *propertyResultDic = [cangTeacher allProperties];
        for (NSString *propertyName in propertyResultDic.allKeys) {
            NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyResultDic[propertyName]);
        }

        NSDictionary *methodResultDic = [cangTeacher allMethods];
        for (NSString *methodName in methodResultDic.allKeys) {
            NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodResultDic[methodName]);
        }

    }
    return 0;
}

這次運行結果多出了一個associatedBust(胸圍)和一個associatedCallBack(寫代碼)屬性。
如圖:

objective-runtime-4

我們成功的給蒼老師添加個一個胸圍的屬性和一個寫代碼的回調,但是添加屬性沒有什麼意義,我們平時在開發過成功中用的比較多的就是添加回調了。

Demo傳送門->6.3蒼老師增加新技能篇Demo

6.4 蒼老師的資料歸檔篇

蒼老師的資料總要整理一下吧!
創建People.h

#import <Foundation/Foundation.h>

@interface People : NSObject <NSCoding>

@property (nonatomic, copy) NSString *name; // 姓名
@property (nonatomic, strong) NSNumber *age; // 年齡
@property (nonatomic, copy) NSString *occupation; // 職業
@property (nonatomic, copy) NSString *nationality; // 國籍

@end

People.m

#import "People.h"

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([People class], &count);
    for (NSUInteger i = 0; i < count; i ++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}

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

@end

Demo傳送門->6.4蒼老師的資料歸檔篇Demo

6.5 蒼老師的資料轉換篇

服務器返回了大量蒼老師的數據,手機端這邊接收後如何去轉換呢?當然是要將JSON轉換爲Model啦!
相信平時你們的項目中也用到過這些三方庫,下面我們去了解下runtime實現JSON和Model互轉。
創建People.h

#import <Foundation/Foundation.h>

@interface People : NSObject

@property (nonatomic, copy) NSString *name; // 姓名
@property (nonatomic, strong) NSNumber *age; // 年齡
@property (nonatomic, copy) NSString *occupation; // 職業
@property (nonatomic, copy) NSString *nationality; // 國籍

// 生成model
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;

// 轉換成字典
- (NSDictionary *)covertToDictionary;

@end

People.m的代碼如下:

#import "People.h"

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People

- (instancetype)initWithDictionary:(NSDictionary *)dictionary
{
    self = [super init];

    if (self) {
        for (NSString *key in dictionary.allKeys) {
            id value = dictionary[key];

            SEL setter = [self propertySetterByKey:key];
            if (setter) {
                // 這裏還可以使用NSInvocation或者method_invoke,不再繼續深究了,有興趣google。
                ((void (*)(id, SEL, id))objc_msgSend)(self, setter, value);
            }
        }
    }
    return self;
}

- (NSDictionary *)covertToDictionary
{
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &count);

    if (count != 0) {
        NSMutableDictionary *resultDict = [@{} mutableCopy];

        for (NSUInteger i = 0; i < count; i ++) {
            const void *propertyName = property_getName(properties[i]);
            NSString *name = [NSString stringWithUTF8String:propertyName];

            SEL getter = [self propertyGetterByKey:name];
            if (getter) {
                id value = ((id (*)(id, SEL))objc_msgSend)(self, getter);
                if (value) {
                    resultDict[name] = value;
                } else {
                    resultDict[name] = @"字典的key對應的value不能爲nil哦!";
                }

            }
        }

        free(properties);

        return resultDict;
    }

    free(properties);

    return nil;
}

#pragma mark - private methods

// 生成setter方法
- (SEL)propertySetterByKey:(NSString *)key
{
    // 首字母大寫,你懂得
    NSString *propertySetterName = [NSString stringWithFormat:@"set%@:", key.capitalizedString];

    SEL setter = NSSelectorFromString(propertySetterName);
    if ([self respondsToSelector:setter]) {
        return setter;
    }
    return nil;
}

// 生成getter方法
- (SEL)propertyGetterByKey:(NSString *)key
{
    SEL getter = NSSelectorFromString(key);
    if ([self respondsToSelector:getter]) {
        return getter;
    }
    return nil;
}

@end

main.m中運行以下代碼:

#import <Foundation/Foundation.h>

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

#import "People.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSDictionary *dict = @{
                               @"name" : @"蒼井空",
                               @"age"  : @18,
                               @"occupation" : @"老師",
                               @"nationality" : @"日本"
                               };

        // 字典轉模型
        People *cangTeacher = [[People alloc] initWithDictionary:dict];
        NSLog(@"熱烈歡迎,從%@遠道而來的%@歲的%@%@",cangTeacher.nationality,cangTeacher.age,cangTeacher.name,cangTeacher.occupation);

        // 模型轉字典
        NSDictionary *covertedDict = [cangTeacher covertToDictionary];
        NSLog(@"%@",covertedDict);

    }
    return 0;
}

最後輸出內容如下:

objective-runtime-5

相信通過前面的學習,這些代碼不用寫過多的註釋你也可以看懂了,我把假設是網絡返回的蒼老師的資料轉化爲了model,然後又將model轉回字典。這是一個JSON轉Model相互轉換的一個思路,大家稍後運行Demo細細品味。

Demo傳送門->6.5蒼老師的資料轉換篇Demo

6.6 蒼老師的唱歌篇

這個實例主要是驗證一下上文《5.2 消息動態解析》

第一首:

添加sing實例方法,但是不提供方法的實現。驗證當找不到方法的實現時,動態添加方法。
創建People.h

#import <Foundation/Foundation.h>

@interface People : NSObject

@property (nonatomic, copy) NSString *name;

- (void)sing;

@end

創建People.m

#import "People.h"

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 我們沒有給People類聲明sing方法,我們這裏動態添加方法
    if ([NSStringFromSelector(sel) isEqualToString:@"sing"]) {
        class_addMethod(self, sel, (IMP)otherSing, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void otherSing(id self, SEL cmd)
{
    NSLog(@"%@ 唱歌啦!",((People *)self).name);
}

在main.m中運行以下代碼:

#import <Foundation/Foundation.h>

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

#import "People.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        People *cangTeacher = [[People alloc] init];
        cangTeacher.name = @"蒼老師";
        [cangTeacher sing];

    }
    return 0;
}

結果如下:

objective-runtime-7

我們沒有提供蒼老師唱歌的方法實現,因此在調用此方法的時候,會調用resolveInstanceMethod方法,我們動態添加了方法。我們也可以返回No,繼續向下傳遞。(此處請返回《5.2 消息動態解析》第一步品味下)

Demo傳送門->6.6蒼老師唱歌篇(第一首)Demo

第二首

外面的小鳥在唱歌,但是蒼老師的歌聲蓋過了小鳥,我們只能聽到蒼老師唱歌了。
這裏我們不聲明sing方法,將調用途中動態更換調用對象。
在第一首代碼的基礎上,創建Bird的model
Bird.h

#import <Foundation/Foundation.h>

@interface Bird : NSObject

@property (nonatomic, copy) NSString *name;

@end

Bird.m

#import "Bird.h"
#import "People.h"

@implementation Bird

// 第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

// 第二部:我們不指定備選對象響應aSelector,進入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步:返回方法選擇器,然後進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

// 第四部:這步我們修改調用對象
- (void)forwardInvocation:(NSInvocation *)anInvocation 
{
    // 我們改變調用對象爲People
    People *cangTeacher = [[People alloc] init];
    cangTeacher.name = @"蒼老師";
    [anInvocation invokeWithTarget:cangTeacher];
}

@end

main.m運行代碼如下:

#import <Foundation/Foundation.h>

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

#import "People.h"
#import "Bird.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Bird *bird = [[Bird alloc] init];
        bird.name = @"小小鳥";

        ((void (*)(id, SEL))objc_msgSend)((id)bird, @selector(sing));
    }
    return 0;
}

運行結果如下:

objective-runtime-8

成功更換了對象,把對象更換爲蒼老師了。(此處請返回《5.2 消息動態解析》品味)

Demo傳送門->6.6蒼老師唱歌篇(第二首)Demo

第三首

蒼老師不想唱歌想跳舞了。
這裏我是實現不提供聲明,不修改調用對象,但是將sing方法修改爲dance方法。
創建People.h

#import <Foundation/Foundation.h>

@interface People : NSObject

@end

People.m

#import "People.h"

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

@implementation People

// 第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

// 第二部:我們不指定備選對象響應aSelector,進入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步:返回方法選擇器,然後進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

// 第四部:這步我們修改調用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation setSelector:@selector(dance)];
    // 這還要指定是哪個對象的方法
    [anInvocation invokeWithTarget:self];
}

// 若forwardInvocation沒有實現,則會調用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    NSLog(@"消息無法處理:%@", NSStringFromSelector(aSelector));
}

- (void)dance
{
    NSLog(@"跳舞!!!come on!");
}

@end

在main.m中運行如下代碼:

#import <Foundation/Foundation.h>

#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif

#import "People.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        People *cangTeacher = [[People alloc] init];

        ((void(*)(id, SEL)) objc_msgSend)((id)cangTeacher, @selector(sing));

    }
    return 0;
}

結果如圖:

objective-runtime-9

成功更換了方法,蒼老師由唱歌改爲跳舞了(此處請返回《5.2 消息動態解析》品味)


附上大神博客鏈接:https://www.ianisme.com/ios/2019.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章