• iOS 开发-- Runtime 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把程序转为可令机器读懂 的机器语言。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 消息动态解析》品味)

    Demo传送门->6.6苍老师唱歌篇(第三首)Demo

    总结

    好吧,我承认我骗了你,当你读到这里你肯定花了不止1小时。都是我的错,不是因为你笨,之所以说1小时是为了让你有信心,有耐心继续下去。读到这里 恭喜你已经在iOS开发的道路上又向前了一步!同时我也要感谢以下参考文献以及文章,是他们让我更好的理解了runtime,再次表示感谢!这篇文章断断 续续写了将近一周的时间,您可以读到这里就是对我最大的鼓励,谢谢!

    Demo传送门->所有的Demo打包下载

    本文参考文献以及文章:

    Objective-C Runtime Reference
    Object Model
    初识Objective-C Runtime
    Objective-C Runtime
    Objective-C Runtime 运行时之一:类与对象
    Runtime Message Forwarding
    runtime模型与字典互转
    iOS开发之深入探讨runtime机制
    Objective-C runtime之消息(二)
    Objective-C runtime之消息转发机制(三)
    《编写高质量代码:改善Objective-C程序的61个建议》
    《正则表达式30分钟入门教程》(参考写作方式)

    原文

  • 相关阅读:
    Python List comprehension列表推导式
    Git
    Python 默认参数 关键词参数 位置参数
    Blog List
    【论文笔记】DeepOrigin: End-to-End Deep Learning for Detection of New Malware Families
    【论文笔记】Malware Detection with Deep Neural Network Using Process Behavior
    Scala入门 【1】
    RocketMQ与Kafka对比(18项差异)
    Kafka简介
    Spark存储管理(读书笔记)
  • 原文地址:https://www.cnblogs.com/wanghuaijun/p/5407845.html
Copyright © 2020-2023  润新知