美文网首页
iOS开发 Runtime机制

iOS开发 Runtime机制

作者: 抄无止境 | 来源:发表于2018-08-24 15:38 被阅读9次

    Runtime简介

    • Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。
    • runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。
      对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
      OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
      事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错。

    runtime 消息机制

    • 我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现通过 runtime 实现)。
    • 消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
      每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。
      OC-->runtime
      简单示例:
      验证:方法调用,是否真的是转换为消息机制?
      必须要导入头文件 #import
      注解1:我们导入系统的头文件,一般用尖括号。
      注解2:OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】
      注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)
      注解4:这里一般不会直接导入
      message.h
      示例代码:OC 方法-->runtime 方法
    说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
    // Person *p = [Person alloc];
    // 底层的实际写法
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    // p = [p init];
    p = objc_msgSend(p, sel_registerName("init"));
    // 调用对象方法(本质:让对象发送消息)
    //[p eat];
    // 本质:让类对象发送消息
    objc_msgSend(p, @selector(eat));
    objc_msgSend([Person class], @selector(run:),20);
    // 也许下面这种好理解一点
    // id objc = [NSObject alloc];
    id objc = objc_msgSend([NSObject class], @selector(alloc));
    // objc = [objc init];
    objc = objc_msgSend(objc, @selector(init));
    

    Runtime 方法调用流程「消息机制」

    面试:消息机制方法调用流程

    • 怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
      1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。
      2.注册方法编号(这里用方法编号的好处,可以快速查找)。
      3.根据方法编号去查找对应方法。
      4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
    • 补充:一个objc 对象的 isa 的指针指向什么?有什么作用?
      每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

    Runtime应用场景

    • 动态交换两个方法的实现
    • 动态添加属性
    • 实现字典转模型的自动转换
    • 发送消息
    • 动态添加方法
    • 拦截并替换方法
    • 实现 NSCoding 的自动归档和解档

    Runtime常用开发应用场景「工作掌握」

    runtime 交换方法

    应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
    需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
    方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
    方案二:使用 runtime,交换方法.
    实现步骤:
    1.给系统的方法添加分类
    2.自己实现一个带有扩展功能的方法
    3.交换方法,只需要交换一次,
    案例代码:方法+调用+打印输出

    - (void)viewDidLoad {
        [super viewDidLoad];
        // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
        // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
        UIImage *image = [UIImage imageNamed:@"123"];
    }
    #import <objc/message.h>
    @implementation UIImage (Image)
    /**
     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
     所以第二步,我们要 自己实现一个带有扩展功能的方法.
     + (UIImage *)imageNamed:(NSString *)name {
    
     }
     */
    @end
    // 打印输出
    2016-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功
    

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

    • runtime 给分类动态添加属性
      原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
      应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
      注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
      需求:给系统 NSObject 类动态添加属性 name 字符串。
      案例代码:方法+调用+打印
    @interface NSObject (Property)
    
    // @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
    @property NSString *name;
    @property NSString *height;
    @end
    
    @implementation NSObject (Property)
    
    - (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);
    
    // 打印输出
    2016-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123
    

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

    • Runtime 字典转模型
      字典转模型的方式:
    1. 一个一个的给模型属性赋值(初学者)。
    2. 字典转模型KVC实现
      KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。
      如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
      分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
      解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
      3.字典转模型 Runtime 实现
      思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。
      考虑情况:
      .当字典的key和模型的属性匹配不上。
      .模型中嵌套模型(模型属性是另外一个模型对象)。
      .数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
      注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;
      步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
    • MJExtension 字典转模型实现
      底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已_.)
      这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,那就好好学习
      字典转模型 Runtime 方式实现:
      说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解
      图片.png
      1、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:
    // Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
    // 思路:遍历模型中所有属性->使用运行时
    + (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;
    }
    

    注解:这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
    原因:Ivar:成员变量,以下划线开头,Property 属性
    获取类里面属性 class_copyPropertyList
    获取类中的所有成员变量 class_copyIvarList

    {
        int _a; // 成员变量
    }
    @property (nonatomic, assign) NSInteger attitudes_count; // 属性
    这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
    

    使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和getter方法的。
    2、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;
    }
    

    3、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;
    }
    
    图片.png

    总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
    这里提到的你如果不是很清楚,建议参考我的Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。

    • 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];
    }
    @end
    
    // 打印输出
    2016-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米
    
    • 动态变量控制
      现在有一个Person类,创建 xiaoming对象
      动态获取 XiaoMing 类中的所有属性 [当然包括私有]
    Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
    

    遍历属性找到对应name字段

    const char *varName = ivar_getName(var);
    

    修改对应的字段值成20

    object_setIvar(self.xiaoMing, var, @"20");
    

    代码参考

    -(void)answer{
       unsigned int count = 0;
       Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
       for (int i = 0; i<count; i++) {
           Ivar var = ivar[i];
           const char *varName = ivar_getName(var);
           NSString *name = [NSString stringWithUTF8String:varName];
           if ([name isEqualToString:@"_age"]) {
               object_setIvar(self.xiaoMing, var, @"20");
               break;
           }
       }
       NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
    }
    
    • 实现NSCoding的自动归档和解档
      如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
      假设现在有一个Movie类,有3个属性。先看下 .h文件
    // Movie.h文件
    //1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
    @interface Movie : NSObject<NSCoding>
    
    @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后,这里就有更简便的方法,如下。

    #import "Movie.h"
    #import <objc/runtime.h>
    @implementation Movie
    
    - (void)encodeWithCoder:(NSCoder *)encoder {
    
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Movie class], &count);
    
        for (int i = 0; i<count; 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<count; 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
    

    这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,好说,下面看看更加简便的方法:两句代码搞定。

    #import "Movie.h"
    #import <objc/runtime.h>
    
    #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;\
    \
    
    @implementation Movie
    
    - (void)encodeWithCoder:(NSCoder *)encoder
    
    {
        encodeRuntime(Movie)
    }
    
    - (id)initWithCoder:(NSCoder *)decoder
    {
        initCoderRuntime(Movie)
    }
    @end
    

    优化:上面是encodeWithCoder 和 initWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
       const char *propertyName = property_getName(propertyList[i]);
       NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    }
    

    获取方法列表

    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i; i<count; i++) {
       Method method = methodList[i];
       NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
    }
    

    获取成员变量列表

    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
    }
    

    获取协议列表

    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
    }
    

    现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
    获得类方法

    Class PersonClass = object_getClass([Person class]);
    SEL oriSEL = @selector(test1);
    Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
    

    获得实例方法

    Class PersonClass = object_getClass([xiaoming class]);
    SEL oriSEL = @selector(test2);
    Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
    

    添加方法

    BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
    

    替换原方法实现

    class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    

    交换两个方法的实现

    method_exchangeImplementations(oriMethod, cusMethod);
    

    常用方法

    附:上面有提到写常用示例,这里再总结下 ~
      // 得到类的所有方法  
    Method allMethods = class_copyMethodList([Person class], &count);  
    // 得到所有成员变量  
    Ivar allVariables = class_copyIvarList([Person class], &count);  
    // 得到所有属性  
    objc_property_t properties = class_copyPropertyList([Person class], &count);  
    // 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义
    Ivar oneCVIvar = class_getClassVariable([Person class], name);  
    // 根据名字得到实例变量的Ivar指针 
     Ivar oneIVIvar = class_getInstanceVariable([Person class], name);  
    // 找到后可以直接对私有变量赋值  
    object_setIvar(_per, oneIVIvar, @"Mike");
    //强制修改name属性  / 动态添加方法:   
    //第一个参数表示Class cls 类型;  
    //第二个参数表示待调用的方法名称;   
    //第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;   
    //第四个参数表方法的参数,0代表没有参数;   */  
    class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);  // 交换两个方法  
    method_exchangeImplementations(method1, method2);
    // 关联两个对象
    objc_setAssociatedObject(id object, const void key, id value, objc_AssociationPolicy policy)/ id object                     :
    表示关联者,是一个对象,变量名理所当然也是object const void key               :获取被关联者的索引key id value                      :被关联者,这里是一个block objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
    /
    // 获得某个类的类方法
    Method class_getClassMethod(Class cls , SEL name)
    // 获得某个类的实例对象方法
    Method class_getInstanceMethod(Class cls , SEL name)
    // 交换两个方法的实现
    void method_exchangeImplementations(Method m1 , Method m2)
    // 将某个值跟某个对象关联起来,将某个值存储到某个对象中
    void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
    // 利用参数key 将对象object中存储的对应值取出来id objc_getAssociatedObject(id object , const void *key)
    // 获得某个类的所有成员变量(outCount 会返回成员变量的总数)Ivar class_copyIvarList(Class cls , unsigned int outCount)
    // 获得成员变量的名字const char *ivar_getName(Ivar v)
    // 获得成员变量的类型const char *ivar_getTypeEndcoding(Ivar v)
    // 获取类里面所有方法class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)// 本质:创建谁的对象
    // 获取类里面属性class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)
    
    
    
    
    ##### runtime 几个参数概念
    
    以上的几种方法应该算是`runtime`在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。
    这里在对 `runtime` 几个参数概念,做一简单说明
    
    **1、objc_msgSend**
    这是个最基本的用于发送消息的函数。
    其实编译器会根据情况在`objc_msgSend`, `objc_msgSend_stret`,,`objc_msgSendSuper`, 或 `objc_msgSendSuper_stret` 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 `Super` 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有`stret`的函数。
    
    **2、SEL**
    `objc_msgSend`函数第二个参数类型为`SEL`,它是`selector`在Objc中的表示类型(Swift中是Selector类)。`selector`是方法选择器,可以理解为区分方法的 `ID`,而这个 `ID` 的数据结构是`SEL`:
    `typedef struct objc_selector *SEL;`
    其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令`@selector()``或者 Runtime` 系统的`sel_registerName`函数来获得一个`SEL`类型的方法选择器。
    
    **3、id**
    `objc_msgSend`第一个参数类型为`id`,大家对它都不陌生,它是一个指向类实例的指针:
    `typedef struct objc_object *id;`
    那`objc_object`又是啥呢:
    `struct objc_object { Class isa; };`
    `objc_object`结构体包含一个`isa`指针,根据`isa`指针就可以顺藤摸瓜找到对象所属的类。
    
    **4、runtime.h里Class的定义**
    ```objc
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针
    
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;//父类
        const char *name                                         OBJC2_UNAVAILABLE;//类名
        long version                                             OBJC2_UNAVAILABLE;//类版本
        long info                                                OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
        long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//缓存
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议
    #endif
    
    } OBJC2_UNAVAILABLE;
    

    可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理。

    https://juejin.im/post/593f77085c497d006ba389f0

    相关文章

      网友评论

          本文标题:iOS开发 Runtime机制

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