美文网首页
我学习中的 Runtime

我学习中的 Runtime

作者: 看看外面的世界 | 来源:发表于2018-04-25 10:00 被阅读11次

    runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。

    OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

    我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现通过 runtime 实现)。

    消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

    每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

    首先举个例子

    OC    Person *p = [Person alloc];  [p  eat];

    底层调用    runtime

    Person *p = objc_msgSend([Person class], @selector(alloc));

        p = objc_msgSend(p, @selector(init));

        objc_msgSend(p, @selector(eat));无参数

        objc_msgSend(p, @selector(eat1:),@"20");有参数

    消息机制方法调用流:

    1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。

    2.注册方法编号(这里用方法编号的好处,可以快速查找)。

    3.根据方法编号去查找对应方法。

    4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

    一、动态交换实现

    需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。

    方法 -> 交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现

    首先建个UIImage分类

    /**

     load方法: 把类加载进内存的时候调用,只会调用一次

     方法应先交换,再去调用

     */

    + (void)load {
        // 1.获取 imageNamed方法地址
        // class_getClassMethod(获取某个类的方法)
        Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
            // 2.获取 ln_imageNamed方法地址
        Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
        // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
        method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);  
    }

    /** 看清楚下面是不会有死循环的 调用 imageNamed => ln_imageNamed 调用 ln_imageNamed => imageNamed */

    + (UIImage *)ln_imageNamed:(NSString *)name {
        UIImage *image = [UIImage ln_imageNamed:name];
        if (image) {
            NSLog(@"runtime添加额外功能--加载成功");
        } else {
            NSLog(@"runtime添加额外功能--加载失败");
        }
        return image;
    }

    不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super

     所以第二步,我们要 自己实现一个带有扩展功能的方法.

    总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。

    二、runtime 给分类动态添加属性

    原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

    应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。

    注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

    需求:给系统 NSObject 类动态添加属性 name 字符串。

    - (void)setName:(NSString *)name {

        // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)

        // object:给哪个对象添加属性

        // key:属性名称

        // value:属性值

        // policy:保存策略

        objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    - (NSString *)name {

        return objc_getAssociatedObject(self, @"name");

    }

    // 调用

    NSObject *objc = [[NSObject alloc] init];

    objc.name = @"123";

    NSLog(@"runtime动态添加属性name==%@",objc.name); 

    总结:其实,给属性赋值的本质,就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让name和NSObject产生关联,而runtime可以做到这一点。

    三、runtime 字典转模型

    思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。

    考虑情况:

    1.当字典的key和模型的属性匹配不上。

    2.模型中嵌套模型(模型属性是另外一个模型对象)。

    3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。

    注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;

    步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

    MJExtension 字典转模型实现

    底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已^_^.)。

    ①.Runtime 字典转模型

    + (instancetype)modelWithDict:(NSDictionary *)dict

    {

        // 1.创建对应的对象

        id objc = [[self alloc] init];

        // 2.利用runtime给对象中的属性赋值

        /**

        class_copyIvarList: 获取类中的所有成员变量

        Ivar:成员变量

        第一个参数:表示获取哪个类中的成员变量

        第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值

        返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。

        count: 成员变量个数

        */

        unsigned int count = 0;

        // 获取类中的所有成员变量

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

        // 遍历所有成员变量

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

            // 根据角标,从数组取出对应的成员变量

            Ivar ivar = ivarList[i];

            // 获取成员变量名字

            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)

            NSString *key = [ivarName substringFromIndex:1];

            // 根据成员属性名去字典中查找对应的value

            id value = dict[key];

            // 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】

            // 而报错 (could not set nil as the value for the key age.)

            if (value) {

                // 给模型中属性赋值

                [objc setValue:value forKey:key];

            }

        }

        return objc;

    }

    ②.runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下

    + (instancetype)modelWithDict2:(NSDictionary *)dict

    {

        // 1.创建对应的对象

        id objc = [[self alloc] init];

        // 2.利用runtime给对象中的属性赋值

        unsigned int count = 0;

        // 获取类中的所有成员变量

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

        // 遍历所有成员变量

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

            // 根据角标,从数组取出对应的成员变量

            Ivar ivar = ivarList[i];

            // 获取成员变量名字

            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 获取成员变量类型

            NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

            // 替换: @\"User\" -> User

            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];

            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

            // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)

            NSString *key = [ivarName substringFromIndex:1];

            // 根据成员属性名去字典中查找对应的value

            id value = dict[key];

            //--------------------------- <#我是分割线#> ------------------------------//

            //

            // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型

            // 判断下value是否是字典,并且是自定义对象才需要转换

            if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {

                // 字典转换成模型 userDict => User模型, 转换成哪个模型

                // 根据字符串类名生成类对象

                Class modelClass = NSClassFromString(ivarType);

                if (modelClass) { // 有对应的模型才需要转

                    // 把字典转模型

                    value = [modelClass modelWithDict2:value];

                }

            }

            // 给模型中属性赋值

            if (value) {

                [objc setValue:value forKey:key];

            }

        }

        return objc;

    }

    ③.runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:

    // Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值

    // 思路:遍历模型中所有属性->使用运行时

    + (instancetype)modelWithDict3:(NSDictionary *)dict

    {

        // 1.创建对应的对象

        id objc = [[self alloc] init];

        // 2.利用runtime给对象中的属性赋值

        unsigned int count = 0;

        // 获取类中的所有成员变量

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

        // 遍历所有成员变量

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

            // 根据角标,从数组取出对应的成员变量

            Ivar ivar = ivarList[i];

            // 获取成员变量名字

            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)

            NSString *key = [ivarName substringFromIndex:1];

            // 根据成员属性名去字典中查找对应的value

            id value = dict[key];

            //--------------------------- <#我是分割线#> ------------------------------//

            //

            // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.

            // 判断值是否是数组

            if ([value isKindOfClass:[NSArray class]]) {

                // 判断对应类有没有实现字典数组转模型数组的协议

                // arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型

                if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                    // 转换成id类型,就能调用任何对象的方法

                    id idSelf = self;

                    // 获取数组中字典对应的模型

                    NSString *type =  [idSelf arrayContainModelClass][key];

                    // 生成模型

                    Class classModel = NSClassFromString(type);

                    NSMutableArray *arrM = [NSMutableArray array];

                    // 遍历字典数组,生成模型数组

                    for (NSDictionary *dict in value) {

                        // 字典转模型

                        id model =  [classModel modelWithDict3:dict];

                        [arrM addObject:model];

                    }

                    // 把模型数组赋值给value

                    value = arrM;

                }

            }

            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错

            if (value) {

                // 给模型中属性赋值

                [objc setValue:value forKey:key];

            }

        }

        return objc;

    }

    runtime字典转模型-->数组中装着模型 打印输出

    总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

    四、动态添加方法

    应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

    注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

    需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

    - (void)viewDidLoad {

        [super viewDidLoad];   

        Person *p = [[Person alloc] init];

        // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。

        // 动态添加方法就不会报错

        [p performSelector:@selector(run:) withObject:@10];

    }

    @implementation Person

    // 没有返回值,1个参数

    // void,(id,SEL)

    void aaa(id self, SEL _cmd, NSNumber *meter) {

        NSLog(@"跑了%@米", meter);

    }

    // 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)

    // 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理

    // 作用:动态添加方法,处理未实现

    + (BOOL)resolveInstanceMethod:(SEL)sel

    {

        // [NSStringFromSelector(sel) isEqualToString:@"run"];

        if (sel == NSSelectorFromString(@"run:")) {

            // 动态添加run方法

            // class: 给哪个类添加方法

            // SEL: 添加哪个方法,即添加方法的方法编号

            // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))

            // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

            class_addMethod(self, sel, (IMP)aaa, "v@:@");

            return YES;

        }

        return [super resolveInstanceMethod:sel];

    }

    五、实现NSCoding的自动归档和解档

    如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。

    假设现在有一个Movie类,有3个属性。先看下 .h文件

    // Movie.h文件

    //1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding

    @interface Movie : NSObject

    @property (nonatomic, copy) NSString *movieId;

    @property (nonatomic, copy) NSString *movieName;

    @property (nonatomic, copy) NSString *pic_url;

    @end

    如果是正常写法, .m 文件应该是这样的:

    // Movie.m文件

    @implementation Movie

    - (void)encodeWithCoder:(NSCoder *)aCoder

    {

        [aCoder encodeObject:_movieId forKey:@"id"];

        [aCoder encodeObject:_movieName forKey:@"name"];

        [aCoder encodeObject:_pic_url forKey:@"url"];

    }

    - (id)initWithCoder:(NSCoder *)aDecoder

    {

        if (self = [super init]) {

            self.movieId = [aDecoder decodeObjectForKey:@"id"];

            self.movieName = [aDecoder decodeObjectForKey:@"name"];

            self.pic_url = [aDecoder decodeObjectForKey:@"url"];

        }

        return self;

    }

    @end

    如果这里有100个属性,那么我们也只能把100个属性都给写一遍吗。

    不过你会使用runtime后,这里就有更简便的方法,如下。

    - (void)encodeWithCoder:(NSCoder *)encoder

    {

        unsigned int count = 0;

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

    for (int i = 0; i

            // 取出i位置对应的成员变量

            Ivar ivar = ivars[i];

            // 查看成员变量

            const char *name = ivar_getName(ivar);

            // 归档

            NSString *key = [NSString stringWithUTF8String:name];

            id value = [self valueForKey:key];

            [encoder encodeObject:value forKey:key];

        }

        free(ivars);

    }

    - (id)initWithCoder:(NSCoder *)decoder

    {

        if (self = [super init]) {

            unsigned int count = 0;

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

    for (int i = 0; i

            // 取出i位置对应的成员变量

            Ivar ivar = ivars[i];

            // 查看成员变量

            const char *name = ivar_getName(ivar);

           // 归档

           NSString *key = [NSString stringWithUTF8String:name];

          id value = [decoder decodeObjectForKey:key];

           // 设置到成员变量身上

            [self setValue:value forKey:key];

            }

            free(ivars);

        } 

        return self;

    }

    @end

    这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,

    好说下面看看更加简便的方法:两句代码搞定。

    #define encodeRuntime(A)  unsigned int count = 0;Ivar *ivars = class_copyIvarList([A class], &count);for (int 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];[encoder encodeObject:value forKey:key];    }free(ivars);


    #define initCoderRuntime(A) if (self = [super init]) {unsigned int count = 0;Ivar *ivars = class_copyIvarList([A class], &count);for (int i = 0;  i < count; i++){Ivar ivar = ivars[i];const char *name = ivar_getName(ivar);NSString *key = [NSString stringWithUTF8String:name];id value = [decoder decodeObjectForKey:key];[self setValue:value forKey:key];}free(ivars);}return self;

    - (void)encodeWithCoder:(NSCoder *)encoder

    {

        encodeRuntime(Movie)

    }

    - (id)initWithCoder:(NSCoder *)decoder

    {

        initCoderRuntime(Movie)

    }

    简单粗暴!!!

    其实我也是从这里学习到的iOS模式详解runtime面试工作 - CocoaChina_让移动开发更简单。

    还有很多具体方式都在这个资料里。

    相关文章

      网友评论

          本文标题:我学习中的 Runtime

          本文链接:https://www.haomeiwen.com/subject/geejlftx.html