天天看點

runtime總結(上)runtime總結

runtime總結

看了很多關于runtime的文章,在此做一個記錄與總結,不足之處歡迎指正。

本文參考了南峰子的技術部落格

關于runtime将分為三篇文章講述

runtime總結(上)

基礎知識及周遊屬性方法的使用

runtime總結(中)

關聯對象及方法交換的使用

runtime總結(下)

消息發送及方法攔截,消息轉發

什麼是runtime

我們寫的代碼在程式運作過程中都會被轉化成runtime的C代碼執行,例如[target doSomething];會被轉化成objc_msgSend(target, @selector(doSomething));。

OC中一切都被設計成了對象,我們都知道一個類被初始化成一個執行個體,這個執行個體是一個對象。實際上一個類本質上也是一個對象,在runtime中用結構體表示。

先來看看一些定義

  • Class

    Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。它的定義如下:

typedef struct objc_class *Class;
           

檢視objc/runtime.h中objc_class結構體的定義如下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;



#if !__OBJC2__

    Class super_class                       OBJC2_UNAVAILABLE;  // 父類

    const char *name                        OBJC2_UNAVAILABLE;  // 類名

    long version                            OBJC2_UNAVAILABLE;  // 類的版本資訊,預設為0

    long info                               OBJC2_UNAVAILABLE;  // 類資訊,供運作期使用的一些位辨別

    long instance_size                      OBJC2_UNAVAILABLE;  // 該類的執行個體變量大小

    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 該類的成員變量連結清單

    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定義的連結清單

    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法緩存

    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 協定連結清單

#endif



} OBJC2_UNAVAILABLE;
           
  1. isa:需要注意的是在Objective-C中,所有的類自身也是一個對象,這個對象的Class裡面也有一個isa指針,它指向metaClass(元類),我們會在後面介紹它。
  2. super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
  3. cache:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象隻有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中周遊一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法後,這個方法就會被緩存到cache清單中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。
  4. version:我們可以使用這個字段來提供類的版本資訊。這對于對象的序列化非常有用,它可是讓我們識别出不同類定義版本中執行個體變量布局的改變。

    針對cache,我們用下面例子來說明其執行過程:

NSArray *array = [[NSArray alloc] init];
           

其流程是:

  1. [NSArray alloc]先被執行。因為NSArray沒有+alloc方法,于是去父類NSObject去查找。
  2. 檢測NSObject是否響應+alloc方法,發現響應,于是檢測NSArray類,并根據其所需的記憶體空間大小開始配置設定記憶體空間,然後把isa指針指向NSArray類。同時,+alloc也被加進cache清單裡面.
  3. 接着,執行-init方法,如果NSArray響應該方法,則直接将其加入cache;如果不響應,則去父類查找。
  • objc_object與id

    objc_object是表示一個類的執行個體的結構體,它的定義如下(objc/objc.h):

struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};

typedef struct objc_object *id;
           

可以看到,這個結構體隻有一個字型,即指向其類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運作時庫會根據執行個體對象的isa指針找到這個執行個體對象所屬的類。Runtime庫會在類的方法清單及父類的方法清單中去尋找與消息對應的selector指向的方法。找到後即運作這個方法。

當建立一個特定類的執行個體對象時,配置設定的記憶體包含一個objc_object資料結構,然後是類的執行個體變量的資料。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來建立objc_object資料結構。

另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實作類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void *指針類型的作用。

  • objc_cache

    上面提到了objc_class結構體中的cache字段,它用于緩存調用過的方法。這個字段是一個指向objc_cache結構體的指針,其定義如下:

struct objc_cache {

    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;

    unsigned int occupied                                    OBJC2_UNAVAILABLE;

    Method buckets[]                                        OBJC2_UNAVAILABLE;

};
           
  1. mask:一個整數,指定配置設定的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來确定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash雜湊演算法。
  2. occupied:一個整數,指定實際占用的緩存bucket的總數。
  3. buckets:指向Method資料結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數組可能會随着時間而增長。
  • 元類(Meta Class)

在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。如:

NSArray *array = [NSArray array];
           

這個例子中,+array消息發送給了NSArray類,而這個NSArray也是一個對象。既然是對象,那麼它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那麼這些就有一個問題了,這個isa指針指向什麼呢?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念

meta-class是一個類對象的類。

當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法清單中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法清單中查找。

meta-class之是以重要,是因為它存儲着一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。

再深入一下,meta-class也是一個類,也可以向它發送一個消息,那麼它的isa又是指向什麼呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。這樣就形成了一個完美的閉環。

通過上面的描述,再加上對objc_class結構體中super_class指針的分析,我們就可以描繪出類及相應meta-class類的一個繼承體系了,如下圖所示:

runtime總結(上)runtime總結

對于NSObject繼承體系來說,其執行個體方法對體系中的所有執行個體、類和meta-class都是有效的;而類方法對于體系内的所有類和meta-class都是有效的。

這裡需要注意的是:我們在一個類對象調用class方法是無法擷取meta-class,它隻是傳回類而已。

如果對meta-class感興趣可以看看這篇文章iOS runtime 之 Class 和 MetaClass

  • 類與對象操作函數

    runtime提供了大量的函數來操作類與對象。類的操作方法大部分是以class為字首的,而對象的操作方法大部分是以objc或object_為字首。下面我們将根據這些方法的用途來分類讨論這些方法的使用。

  • 類名(name)

    類名操作的函數主要有:

// 擷取類的類名
const char * class_getName ( Class cls );
           

對于class_getName函數,如果傳入的cls為Nil,則傳回一個字字元串。

  • 父類(super_class)和元類(meta-class)

父類和元類操作的函數主要有:

// 擷取類的父類

Class class_getSuperclass ( Class cls );



// 判斷給定的Class是否是一個元類

BOOL class_isMetaClass ( Class cls );
           

class_getSuperclass函數,當cls為Nil或者cls為根類時,傳回Nil。不過通常我們可以使用NSObject類的superclass方法來達到同樣的目的。

class_isMetaClass函數,如果是cls是元類,則傳回YES;如果否或者傳入的cls為Nil,則傳回NO。

  • 執行個體變量大小(instance_size)
// 擷取執行個體大小

size_t class_getInstanceSize ( Class cls );
           
  • 成員變量(ivars)及屬性

    在objc_class中,所有的成員變量、屬性的資訊是放在連結清單ivars中的。ivars是一個數組,數組中每個元素是指向Ivar(變量資訊)的指針。runtime提供了豐富的函數來操作這一字段。大體上可以分為以下幾類:

成員變量操作函數,主要包含以下函數:
// 擷取類中指定名稱執行個體成員變量的資訊

Ivar class_getInstanceVariable ( Class cls, const char *name );

// 擷取類成員變量的資訊

Ivar class_getClassVariable ( Class cls, const char *name );



// 添加成員變量

BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );



// 擷取整個成員變量清單

Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
           

class_getInstanceVariable函數,它傳回一個指向包含name指定的成員變量資訊的objc_ivar結構體的指針(Ivar)。

class_getClassVariable函數,目前沒有找到關于Objective-C中類變量的資訊,一般認為Objective-C不支援類變量。注意,傳回的清單不包含父類的成員變量和屬性。

Objective-C不支援往已存在的類中添加執行個體變量,是以不管是系統庫提供的提供的類,還是我們自定義的類,都無法動态添加成員變量。但如果我們通過運作時來建立一個類的話,又應該如何給它添加成員變量呢?這時我們就可以使用class_addIvar函數了。不過需要注意的是,這個方法隻能在objc_allocateClassPair函數與objc_registerClassPair之間調用。另外,這個類也不能是元類。成員變量的按位元組最小對齊量是1<

// 擷取指定的屬性

objc_property_t class_getProperty ( Class cls, const char *name );



// 擷取屬性清單

objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );



// 為類添加屬性

BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );



// 替換類的屬性

void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
           

這一種方法也是針對ivars來操作,不過隻操作那些是屬性的值。

  • 方法(methodLists)

    方法操作主要有以下函數:

// 添加方法

BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );



// 擷取執行個體方法

Method class_getInstanceMethod ( Class cls, SEL name );



// 擷取類方法

Method class_getClassMethod ( Class cls, SEL name );



// 擷取所有方法的數組

Method * class_copyMethodList ( Class cls, unsigned int *outCount );



// 替代方法的實作

IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );



// 傳回方法的具體實作

IMP class_getMethodImplementation ( Class cls, SEL name );

IMP class_getMethodImplementation_stret ( Class cls, SEL name );



// 類執行個體是否響應指定的selector

BOOL class_respondsToSelector ( Class cls, SEL sel );
           

class_addMethod的實作會覆寫父類的方法實作,但不會取代本類中已存在的實作,如果本類中包含一個同名的實作,則函數會傳回NO。如果要修改已存在實作,可以使用method_setImplementation。一個Objective-C方法是一個簡單的C函數,它至少包含兩個參數—self和_cmd。是以,我們的實作函數(IMP參數指向的函數)至少需要兩個參數,如下所示:

void myMethodIMP(id self, SEL _cmd)

{

    // implementation ....

}
           

與成員變量不同的是,我們可以為類動态添加方法,不管這個類是否已存在。

另外,參數types是一個描述傳遞給方法的參數類型的字元數組,這就涉及到類型編碼,我們将在後面介紹。

● class_getInstanceMethod、class_getClassMethod函數,與class_copyMethodList不同的是,這兩個函數都會去搜尋父類的實作。

● class_copyMethodList函數,傳回包含所有執行個體方法的數組,如果需要擷取類方法,則可以使用class_copyMethodList(object_getClass(cls), &count)(一個類的執行個體方法是定義在元類裡面)。該清單不包含父類實作的方法。outCount參數傳回方法的個數。在擷取到清單後,我們需要使用free()方法來釋放它。

● class_replaceMethod函數,該函數的行為可以分為兩種:如果類中不存在name指定的方法,則類似于class_addMethod函數一樣會添加方法;如果類中已存在name指定的方法,則類似于method_setImplementation一樣替代原方法的實作。

● class_getMethodImplementation函數,該函數在向類執行個體發送消息時會被調用,并傳回一個指向方法實作函數的指針。這個函數會比method_getImplementation(class_getInstanceMethod(cls, name))更快。傳回的函數指針可能是一個指向runtime内部的函數,而不一定是方法的實際實作。例如,如果類執行個體無法響應selector,則傳回的函數指針将是運作時消息轉發機制的一部分。

● class_respondsToSelector函數,我們通常使用NSObject類的respondsToSelector:或instancesRespondToSelector:方法來達到相同目的。

  • 協定(objc_protocol_list)

    協定相關的操作包含以下函數:

// 添加協定

BOOL class_addProtocol ( Class cls, Protocol *protocol );



// 傳回類是否實作指定的協定

BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );



// 傳回類實作的協定清單

Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
           

● class_conformsToProtocol函數可以使用NSObject類的conformsToProtocol:方法來替代。

● class_copyProtocolList函數傳回的是一個數組,在使用後我們需要使用free()手動釋放。

  • 版本(version)
// 擷取版本号

int class_getVersion ( Class cls );



// 設定版本号

void class_setVersion ( Class cls, int version );
           
  • 動态建立類和對象
  • 動态建立類

    動态建立類涉及到以下幾個函數:

// 建立一個新類和元類

Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );



// 銷毀一個類及其相關聯的類

void objc_disposeClassPair ( Class cls );



// 在應用中注冊由objc_allocateClassPair建立的類

void objc_registerClassPair ( Class cls );
           

● objc_allocateClassPair函數:如果我們要建立一個根類,則superclass指定為Nil。extraBytes通常指定為0,該參數是配置設定給類和元類對象尾部的索引ivars的位元組數。

為了建立一個新類,我們需要調用objc_allocateClassPair。然後使用諸如class_addMethod,class_addIvar等函數來為新建立的類添加方法、執行個體變量和屬性等。完成這些後,我們需要調用objc_registerClassPair函數來注冊類,之後這個新類就可以在程式中使用了。

執行個體方法和執行個體變量應該添加到類自身上,而類方法應該添加到類的元類上。

● objc_disposeClassPair函數用于銷毀一個類,不過需要注意的是,如果程式運作中還存在類或其子類的執行個體,則不能調用針對類調用該方法。

  • 動态建立對象
// 建立類執行個體

id class_createInstance ( Class cls, size_t extraBytes );



// 在指定位置建立類執行個體

id objc_constructInstance ( Class cls, void *bytes );



// 銷毀類執行個體

void * objc_destructInstance ( id obj );
           

● class_createInstance函數:建立執行個體時,會在預設的記憶體區域為類配置設定記憶體。extraBytes參數表示配置設定的額外位元組數。這些額外的位元組可用于存儲在類定義中所定義的執行個體變量之外的執行個體變量。該函數在ARC環境下無法使用。

調用class_createInstance的效果與+alloc方法類似。不過在使用class_createInstance時,我們需要确切的知道我們要用它來做什麼。在下面的例子中,我們用NSString來測試一下該函數的實際效果:

id theObject = class_createInstance(NSString.class, sizeof(unsigned));

id str1 = [theObject init];

NSLog(@"%@", [str1 class]);

id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);
           

輸出結果是:

NSString

__NSCFConstantString
           

可以看到,使用class_createInstance函數擷取的是NSString執行個體,而不是類簇中的預設占位符類__NSCFConstantString。

● objc_constructInstance函數:在指定的位置(bytes)建立類執行個體。

● objc_destructInstance函數:銷毀一個類的執行個體,但不會釋放并移除任何與其相關的引用。

  • 執行個體操作函數
// 傳回指定對象的一份拷貝

id object_copy ( id obj, size_t size );



// 釋放指定對象占用的記憶體

id object_dispose ( id obj );
           

針對對象執行個體變量進行操作的函數,這類函數包含:

// 修改類執行個體的執行個體變量的值

Ivar object_setInstanceVariable ( id obj, const char *name, void *value );



// 擷取對象執行個體變量的值

Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );



// 傳回指向給定對象配置設定的任何額外位元組的指針

void * object_getIndexedIvars ( id obj );



// 傳回對象中執行個體變量的值

id object_getIvar ( id obj, Ivar ivar );



// 設定對象中執行個體變量的值

void object_setIvar ( id obj, Ivar ivar, id value );
           

針對對象的類進行操作的函數,這類函數包含

// 傳回給定對象的類名

const char * object_getClassName ( id obj );



// 傳回對象的類

Class object_getClass ( id obj );



// 設定對象的類

Class object_setClass ( id obj, Class cls );
           
  • 執行個體(Example)及用法

    說了那麼多都是紙上談兵,實際開發中我們怎麼去用呢?我在這裡列出了兩種runtime的周遊屬性用法:

  • 歸檔解擋

    參考連結runtime自動歸檔/解檔

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];
    if (self) {
        //最笨的方法
//        self.name = [coder decodeObjectForKey:@"name"];
//        self.age  = [coder decodeFloatForKey:@"age"];

        u_int count;
        Ivar *ivarList = class_copyIvarList(self.class, &count);

        for (int i = ; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            const void *typeEncoding = ivar_getTypeEncoding(ivar);
            NSString *type = [NSString stringWithUTF8String:typeEncoding];

            NSString *key = [ivarName substringFromIndex:];
            NSString * firstChar = [key substringToIndex:].uppercaseString;
            NSString *setSelName = [NSString stringWithFormat:@"set%@%@:",firstChar,[key substringFromIndex:]];
            if () {//純runtime方法
                //這裡去判斷各種type類型 這裡隻列出了float類型的
                if ([self respondsToSelector:NSSelectorFromString(setSelName)]) {
                    if ([type isEqualToString:@"f"]) {
                        float(*action)(id,SEL,float) = (float(*)(id,SEL,float))objc_msgSend;
                        NSNumber *value = [coder decodeObjectForKey:key];
                        action(self,NSSelectorFromString(setSelName),value.floatValue);
                    }else{
                        float(*action)(id,SEL,id) = (float(*)(id,SEL,id))objc_msgSend;
                        id value = [coder decodeObjectForKey:key];
                        action(self,NSSelectorFromString(setSelName),value);

                    }
                }
            }else {//second method KVC

                //如果用kvc的方式可以省去類型判斷
                //[self valueForKey:key];
                id value = [coder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }




        }
        free(ivarList);


    }
    return self;
}

 - (void)encodeWithCoder:(NSCoder *)coder
{
//    [coder encodeObject:self.name forKey:@"name"];
//    [coder encodeFloat:self.age forKey:@"age"];


    u_int count;
    Ivar *ivarList = class_copyIvarList(self.class, &count);

    for (int i = ; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        const void *typeEncoding = ivar_getTypeEncoding(ivar);
        NSString *type = [NSString stringWithUTF8String:typeEncoding];
        NSLog(@"type = %@",type);
        NSString *key = [ivarName substringFromIndex:];
        if () {
            if ([self respondsToSelector:NSSelectorFromString(key)]) {

                if ([type isEqualToString:@"f"]) {

                    //performSelector 傳回類型為id 實際age為float類型,是以這裡會崩
                    //                id value = [self performSelector:NSSelectorFromString(key)];
                    //                void(*action)(id,SEL) = (void(*)(id,SEL))objc_msgSend;
                    float(*action)(id,SEL) = (float(*)(id,SEL))objc_msgSend;
                    float value = action(self,NSSelectorFromString(key));
                    [coder encodeObject:@(value) forKey:key];
                }else {
                    //                id value = [self performSelector:NSSelectorFromString(key)];
                    id(*action)(id,SEL) = (id(*)(id,SEL))objc_msgSend;
                    id value = action(self,NSSelectorFromString(key));

                    [coder encodeObject:value forKey:key];
                }
            }
        }
        else{
        //kvc
            id value = [self valueForKey:key];

            [coder encodeObject:value forKey:key];
        }

        }

         free(ivarList);

}





 - (NSArray *)propertyList {
    u_int count;
    Ivar *ivarList = class_copyIvarList(self.class, &count);

    NSMutableArray *properNames = [[NSMutableArray alloc] init];

    for (int i = ; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        const void *typeEncoding = ivar_getTypeEncoding(ivar);
        NSString *type = [NSString stringWithUTF8String:typeEncoding];

        NSString *key = [ivarName substringFromIndex:];

        [properNames addObject:key];
    }
    free(ivarList);
    return [properNames copy];
}
           

demo列舉了三種解擋歸檔的方法:

- 沒有用runtime的方法,代碼如下

- 純runtime 實作

- runtime+kvc實作

從代碼及邏輯來看還是第三種方法比較簡單好了解,推薦大家使用

  • 談談第二種方法遇到的坑

坑1: 在歸檔的時候我們通過屬性的getter方法來擷取值,然後歸檔。但是,成員變量是是有類型的,并不是所有類型都可以歸檔,比如const void 就不支援歸檔,那麼我們需要根據類型轉換成支援歸檔的類型再存儲。另外,我們可以通過ivar_getTypeEncoding函數擷取成員變量的類型,但是這個類型不一定是我們常見的NSString之類的,可能會出現r^v這樣代表const void 。這些都是runtime系統所定義的,是以它是固定的,我們隻要去按照蘋果給出的類型編碼表就可以知道哪些字元代表什麼類型了。關于類型編碼可以參考:

關于Objective-C Runtime看我就夠了

Objective-C Runtime Programming Guide1

Objective-C Runtime Programming Guide2

坑2: 一開始我使用id value = [self performSelector:NSSelectorFromString(key)];來調用getter方法擷取value,在沒有age的時候一切正常,當有了float類型的age的時候情況就完全變了樣,performSelector 傳回類型為id 而- (float)age:(float)age;的傳回值為float類型,這裡如果這樣用的話就會挂掉,是以采用了一個巧妙地方法直接調用objc_msgSend函數獲得float類型的傳回值,objc_magSend函數在ARC中不能直接調用,若想調用隻能設定

runtime總結(上)runtime總結

選項為NO,而蘋果預設為YES,是以我們最好用下邊這種方法來調用

float(*action)(id,SEL) = (float(*)(id,SEL))objc_msgSend;
float value = action(self,NSSelectorFromString(key));
           

float可以換為其它類型的如:

id(*action)(id,SEL) = (id(*)(id,SEL))objc_msgSend;
 id value = action(self,NSSelectorFromString(key));
           
  1. 字典轉模型
+ (instancetype)runtime_modelWithDict:(NSDictionary *)dict {
    // 思路:周遊模型中所有屬性-》使用運作時

    // 0.建立對應的對象
    id objc = [[self alloc] init];

    // 1.利用runtime給對象中的成員屬性指派

    // class_copyIvarList:擷取類中的所有成員屬性
    // Ivar:成員屬性的意思
    // 第一個參數:表示擷取哪個類中的成員屬性
    // 第二個參數:表示這個類有多少成員屬性,傳入一個Int變量位址,會自動給這個變量指派
    // 傳回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過傳回的數組就能全部擷取到。
    /* 類似下面這種寫法

     Ivar ivar;
     Ivar ivar1;
     Ivar ivar2;
     // 定義一個ivar的數組a
     Ivar a[] = {ivar,ivar1,ivar2};

     // 用一個Ivar *指針指向數組第一個元素
     Ivar *ivarList = a;

     // 根據指針通路數組第一個元素
     ivarList[0];

     */
    unsigned int count;

    // 擷取類中的所有成員屬性
    Ivar *ivarList = class_copyIvarList(self, &count);

    for (int i = ; i < count; i++) {
        // 根據角标,從數組取出對應的成員屬性
        Ivar ivar = ivarList[i];

        // 擷取成員屬性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員屬性名->字典中的key
        // 從第一個角标開始截取
        NSString *key = [name substringFromIndex:];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        // 二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型
        // 判斷下value是否是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            // 字典轉模型
            // 擷取模型的類對象,調用modelWithDict
            // 模型的類名已知,就是成員屬性的類型

            // 擷取成員屬性類型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            // 生成的是這種@"@\"User\"" 類型 -》 @"User"  在OC字元串中 \" -> ",\是轉義的意思,不占用字元
            // 裁剪類型字元串
            NSRange range = [type rangeOfString:@"\""];

            type = [type substringFromIndex:range.location + range.length];

            range = [type rangeOfString:@"\""];

            // 裁剪到哪個角标,不包括目前角标
            type = [type substringToIndex:range.location];


            // 根據字元串類名生成類對象
            Class modelClass = NSClassFromString(type);


            if (modelClass) { // 有對應的模型才需要轉

                // 把字典轉模型
                value  =  [modelClass runtime_modelWithDict:value];
            }


        }

                // 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型.
                // 判斷值是否是數組
                if ([value isKindOfClass:[NSArray class]]) {
                    // 判斷對應類有沒有實作字典數組轉模型數組的協定
                    if ([self respondsToSelector:@selector(objectClassInArray)]) {

                        // 轉換成id類型,就能調用任何對象的方法
//                        id idSelf = self;

                        // 擷取數組中字典對應的模型
                        NSString *type = [self performSelector:@selector(objectClassInArray)][key];
                        if (type == nil) {
                            continue;
                        }
                        // 生成模型
                        Class classModel = NSClassFromString(type);
                        NSMutableArray *arrM = [NSMutableArray array];
                        // 周遊字典數組,生成模型數組
                        for (NSDictionary *dict in value) {
                            // 字典轉模型
                            id model =  [classModel runtime_modelWithDict:dict];
                            [arrM addObject:model];
                        }

                        // 把模型數組指派給value
                        value = arrM;

                    }
                }


        if (value) { // 有值,才需要給模型的屬性指派
            // 利用KVC給模型中的屬性指派
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}
           

參考連結:

跟着MJExtension實作簡單的字典轉模型架構

讓你快速上手Runtime

demo寫了正常kvc指派和runtime字典轉model方法,也是MJExtension-IOS的核心方法,需要注意的是根model(demo中的DDModel)必須實作這個方法:

+ (NSDictionary *)objectClassInArray
{
    return @{@"thirdModel":@"DDThirdModel"};
}
           

注意這裡的+不能改為- ,因為

[self respondsToSelector:@selector(objectClassInArray)]

中self是從類方法裡去周遊尋找method,如果改為-執行個體方法就會造成找不到這個方法大括号裡的代碼就沒法執行,thirdModel會直接以array的形式kvc指派,下圖是差別:

runtime總結(上)runtime總結
runtime總結(上)runtime總結

注釋的已經很清晰了,這裡不再贅述了。

demo位址-github