美文网首页
runtime学习(四):runtime的实际应用

runtime学习(四):runtime的实际应用

作者: 杰克道长 | 来源:发表于2018-08-07 17:01 被阅读0次

    注:本文不是原创,只是在学习中做的整理和笔记,以便自己以后更好的复习。原文来自runtime从入门到精通系列

    有关runtime的理论知识介绍就先告于段落,小伙伴们,真正的干货来了,runtime在实际的开发中到底有何牛X的作用?我们该怎么使用这么牛X的工具呢?

    想使用runtime,首先在写运行时代码之前,要先加上头文件:

    #import <objc/objc-runtime.h> // 模拟器
    或者
    #import <objc/runtime.h> // 真机
    #import <objc/message.h> // 真机
    

    runtime在实际的开发中的使用:

    1. 动态添加一个类
      “KVO”的实现是利用了runtime能够动态添加类
    2. 通过runtime获取一个类的所有属性,可用用来:
      1、打印一个类的所有ivar, property 和 method(简单直接的使用)
      2、动态变量控制
      3、在NSObject的分类中增加方法来避免使用KVC赋值的时候出现崩溃
      4、自动的归档和解档
      5、字典转模型
    3. 利用runtime的动态交换方法实现,可以用来:
      1、方法简单的交换
      2、拦截系统方法(Swizzle 黑魔法),也可以说成对系统的方法进行替换
      3、运行时实现多继承的效果
    4. 动态添加方法
    5. 利用运行时关联Associated Object,可以让类别可以添加属性
    6. 万能界面跳转
    7. 插件开发
    8. Jspath 热更新 也是使用运行时,但是现在不可用了。

    下面详细介绍各个用法:

    1. 动态添加一个类

    • “KVO”的实现是利用了runtime能够动态添加类:

    当你对一个对象进行观察时, 系统会自动新建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. 然后把原对象的isa指针指向这个新类, 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.

    就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.

    • 如何动态添加一个类:
    // 创建一个类(size_t extraBytes该参数通常指定为0, 该参数是分配给类和元类对象尾部的索引ivars的字节数。)
    Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
    
    // 添加ivar
    // @encode(aType) : 返回该类型的C字符串
    class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
    
    // 注册该类
    objc_registerClassPair(clazz);
    // 创建实例对象
    id object = [[clazz alloc] init];
    
    // 设置ivar
    [object setValue:@"Tracy" forKey:@"name"];
    
    Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
    object_setIvar(object, ageIvar, @18);
    
    // 打印对象的类和内存地址
    NSLog(@"%@", object);
    // 打印对象的属性值
    NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
    
    // 当类或者它的子类的实例还存在,则不能调用objc_disposeClassPair方法
    object = nil;
    // 销毁类
    objc_disposeClassPair(clazz);
    

    运行结果为:

    2016-09-04 17:04:08.328 Runtime-实践篇[13699:1043458] <GoodPerson: 0x1002039b0>
    2016-09-04 17:04:08.329 Runtime-实践篇[13699:1043458] name = Tracy, age = 18
    

    这样, 我们就在程序运行时动态添加了一个继承自NSObject的GoodPerson类, 并为该类添加了name和age成员变量.

    2. 通过runtime获取一个类的所有属性,可以做些什么?

    1、 打印一个类的所有ivar, property 和 method(简单直接的使用)

    Person *p = [[Person alloc] init];
    [p setValue:@"Kobe" forKey:@"name"];
    [p setValue:@18 forKey:@"age"];
    //    p.address = @"广州大学城";
    p.weight = 110.0f;
    
    // 1.打印所有ivars
    unsigned int ivarCount = 0;
    // 用一个字典装ivarName和value
    NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
    Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
    for(int i = 0; i < ivarCount; i++){
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
        id value = [p valueForKey:ivarName];
    
        if (value) {
            ivarDict[ivarName] = value;
        } else {
            ivarDict[ivarName] = @"值为nil";
        }
    }
    // 打印ivar
    for (NSString *ivarName in ivarDict.allKeys) {
        NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
    }
    
    // 2.打印所有properties
    unsigned int propertyCount = 0;
    // 用一个字典装propertyName和value
    NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
    objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
    for(int j = 0; j < propertyCount; j++){
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
        id value = [p valueForKey:propertyName];
    
        if (value) {
            propertyDict[propertyName] = value;
        } else {
            propertyDict[propertyName] = @"值为nil";
        }
    }
    // 打印property
    for (NSString *propertyName in propertyDict.allKeys) {
        NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
    }
    
    // 3.打印所有methods
    unsigned int methodCount = 0;
    // 用一个字典装methodName和arguments
    NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
    Method *methodList = class_copyMethodList([p class], &methodCount);
    for(int k = 0; k < methodCount; k++){
        SEL methodSel = method_getName(methodList[k]);
        NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
    
        unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
    
        methodDict[methodName] = @(argumentNums - 2); // -2的原因是每个方法内部都有self 和 selector 两个参数
    }
    // 打印method
    for (NSString *methodName in methodDict.allKeys) {
        NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
    }
    
    

    打印结果为 :

    2016-09-04 17:06:49.070 Runtime-实践篇[13723:1044813] ivarName:_name, ivarValue:Kobe
    2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_age, ivarValue:18
    2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_weight, ivarValue:110
    2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] ivarName:_address, ivarValue:值为nil
    
    2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:address, propertyValue:值为nil
    2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:weight, propertyValue:110
    
    2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:setWeight:, argumentsCount:1
    2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:weight, argumentsCount:0
    2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:setAddress:, argumentsCount:1
    2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:address, argumentsCount:0
    2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:.cxx_destruct, argumentsCount
    

    2、动态变量控制

    在程序中,XiaoMing的age是10,后来被runtime变成了20,来看看runtime是怎么做到的:

    -(void)changeAge{
         unsigned int count = 0;
         //动态获取XiaoMing类中的所有属性[当然包括私有] 
         Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
         //遍历属性找到对应age字段 
         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"]) {
                 //修改对应的字段值成20
                 object_setIvar(self.xiaoMing, var, @"20");
                 break;
             }
         }
         NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
     }
    

    3、在NSObject的分类中增加方法来避免使用KVC赋值的时候出现崩溃

    在有些时候我们需要通过KVC去修改某个类的私有变量,但是又不知道该属性是否存在,如果类中不存在该属性,那么通过KVC赋值就会crash,这时也可以通过运行时进行判断。同样我们在NSObject的分类中增加如下方法。

    /**
     *  判断类中是否有该属性
     *  @param property 属性名称
     *  @return 判断结果
     */
    -(BOOL)hasProperty:(NSString *)property {
        BOOL flag = NO;
        u_int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            const char *propertyName = ivar_getName(ivars[i]);
            NSString *propertyString = [NSString stringWithUTF8String:propertyName];
            if ([propertyString isEqualToString:property]){
                flag = YES;
            }
        }
        return flag;
    }
    

    4、自动的归档和解档

    详情请看自动的归档和解档

    5、字典转模型

    详情请看字典转模型

    3. 利用runtime的动态交换方法实现,可以做什么?

    1、方法简单的交换

    创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

    + (void)run {
        NSLog(@"跑");
    }
    
    + (void)study {
        NSLog(@"学习");
    }
    

    控制器中调用,则先打印跑,后打印学习

    [Person run];
    [Person study];
    

    下面通过runtime 实现方法交换,类方法用class_getClassMethod ,对象方法用class_getInstanceMethod

    // 获取两个类的类方法
    Method m1 = class_getClassMethod([Person class], @selector(run));
    Method m2 = class_getClassMethod([Person class], @selector(study));
    // 开始交换方法实现
    method_exchangeImplementations(m1, m2);
    // 交换后,先打印学习,再打印跑!
    [Person run];
    [Person study];
    

    2、拦截系统方法(Swizzle 黑魔法),也可以说成对系统的方法进行替换

    由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(系统的方法或者一些开源库出现问题的时候),这个时候runtime就派上用场了。

    需求:比如iOS6 升级 iOS7 后需要版本适配,根据不同系统使用不同样式图片(拟物化和扁平化),如何通过不去手动一个个修改每个UIImage的imageNamed:方法就可以实现为该方法中加入版本判断语句?

    步骤:
    1、为UIImage建一个分类(UIImage+Category)
    2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断

    + (UIImage *)xh_imageNamed:(NSString *)name {
        double version = [[UIDevice currentDevice].systemVersion doubleValue];
        if (version >= 7.0) {
            // 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片
            name = [name stringByAppendingString:@"_os7"];
        }
        return [UIImage xh_imageNamed:name];
    }
    

    3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

    + (void)load {
        // 获取两个类的类方法
        Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
        Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
        // 开始交换方法实现
        method_exchangeImplementations(m1, m2);
    }
    

    注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(有点绕,就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!

    利用以上思路,我们还可以给 NSObject 添加分类,统计创建了多少个对象给控制器添加分类,统计有创建了多少个控制器,特别是公司需求总变的时候,在一些原有控件或模块上添加一个功能,建议使用该方法!

    交换原理:

    • 交换前: 交换前.png
    • 交换后: 交换后.png

    3、运行时实现多继承的效果

    当向某个对象Object发送某消息,但runtime system在当前类和父类中都找不到对应方法的实现时,runtime system并不会立即报错使程序崩溃,而是依次执行消息转发

    1. 动态方法解析(Method Resolution)
    2. 后备接收者(Fast Forwarding)
    3. 完整的消息转发(Normal Forwarding)

    所以,我们可以利用消息转发过程中的2、3两步,将消息转发给其他对象处理,间接实现多继承的效果。

    4. 动态添加方法

    开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
    经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

    简单使用:

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        Person *p = [[Person alloc] init];
        // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
        // 动态添加方法就不会报错
        [p performSelector:@selector(eat)];
    }
    
    @end
    
    
    // Person.m文件
    @implementation Person
    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if (sel == @selector(eat)) {
            // 动态添加eat方法
    
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(eat), eat, "v@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    @end
    

    5. 利用运行时关联Associated Object,可以让类别可以添加属性

    1、创建一个类别,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)。
    2、先在.h 中@property 声明出get 和 set 方法,方便点语法调用

    @property(nonatomic,copy)NSString *name;
    

    3、在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值

    char nameKey;
    
    - (void)setName:(NSString *)name {
        // 将某个值跟某个对象关联起来,将某个值存储到某个对象中
        objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name {
        return objc_getAssociatedObject(self, &nameKey);
    }
    

    6. 万能界面跳转

    利用runtime动态生成对象、属性、方法这特性,先跟服务端商量好,定义跳转规则,比如要跳转到A控制器,需要传属性id、type,那么服务端返回字典给我,里面有控制器名,两个属性名跟属性值,客户端就可以根据控制器名生成对象,再用kvc给对象赋值,这样就搞定了。详情请看runtime实现万能界面跳转

    7. 插件开发

    XCode 有个很坑爹的地方,就是它并不官方支持插件开发,官方没有文档,XCode 也没有开源,但由于 XCode 是 Objective-C 写的,OC 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:

    1. dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。
    2. 通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。
    3. 通过 NSNotificationCenter 监听各种事件的发生。

    8. Jspath 热更新 也是使用运行时,jspatch 基本上算是黑科技,在线修复版本bug,微信都使用了这个技术。但是现在热更App好像是不能过审的。

    相关文章

      网友评论

          本文标题:runtime学习(四):runtime的实际应用

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