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[1]                                        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类的一个继承体系了,如下图所示:


对于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 = 0; 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:1];
            NSString * firstChar = [key substringToIndex:1].uppercaseString;
            NSString *setSelName = [NSString stringWithFormat:@"set%@%@:",firstChar,[key substringFromIndex:1]];
            if (0) {//纯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 = 0; 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:1];
        if (0) {
            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 = 0; 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:1];

        [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中不能直接调用,若想调用只能设置选项为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 = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员属性
        Ivar ivar = ivarList[i];

        // 获取成员属性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 处理成员属性名->字典中的key
        // 从第一个角标开始截取
        NSString *key = [name substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的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赋值,下图是区别:

-方法+方法
注释的已经很清晰了,这里不再赘述了。

demo地址-github

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值