美文网首页iOS
OC的runtime机制和基本使用场景

OC的runtime机制和基本使用场景

作者: 无敌大闸蟹 | 来源:发表于2018-11-29 15:06 被阅读28次

    Runtime即我们通常叫的运行时,也就是程序在运行的时候做的事情。是 Objective-C底层的一套C语言的API,是 iOS 内部的核心之一,我们平时编写的 Objective-C 代码,底层都是基于它来实现的,Objective-C代码编译后,其实都是Runtime形式的C语言代码。
    OC会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
    因此,只靠编译器是不够的,我们还需要一个运行时系统来处理编译后的代码。

    所以一些用OC不太好实现的功能 我们可以利用runtime来实现 比如:
    1:动态交换两个方法的实现(常用于交换系统方法)
    2:动态添加对象的成员变量和成员方法
    3:获得某个类的所有属性变量属性及方法
    4:实现分类Category中可以增加属性
    5:拦截系统自带的方法调用,比如拦截viewDidLoad
    67891.。。。。。。。。等等等

    先来看动态交换两个方法 用到的方法是method_exchangeImplementations
    举个例子

        [self fuck];
        [self dontFuck];
        Method method1 = class_getInstanceMethod([self class], @selector(fuck));
        Method method2 = class_getInstanceMethod([self class], @selector(dontFuck));
        //类方法用class_getClassMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)
        method_exchangeImplementations(method1, method2);
        [self fuck];
        [self dontFuck];
    
    - (void)fuck
    {
        NSLog(@"fuck");
    }
    
    - (void)dontFuck
    {
        NSLog(@"dontFuck");
    }
    

    再看系统方法的拦截交换

    比如遇到需求 iOS9 以上的版本需要使用另一套图片, 这时候需要在一个个使用的地方判断版本来加载不同的图片吗? 这样会不会太繁琐呢? 有好的解决方法吗?
    这时候就可以使用Swizzle, 来拦截UIImage的 imageName这个加载图片的系统方法, 来交换成我们自己的方法
    创建一个UIImage的分类:(UIImage+Category);
    在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断修改图片名

    //自定义方法
    + (UIImage *)new_ImageNamed:(NSString *)name {
        double version = [[UIDevice currentDevice].systemVersion doubleValue];
        if (version >= 9.0) {
            name = [name stringByAppendingString:@"_ios9"];
        }
        return [UIImage new_ImageNamed:name]; //方法交换后, 调用imageNamed方法, 让有加载图片的功能
    }
    

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

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

    在使用中, 如果iOS9以上版本使用另一版本的图片, 就可以交换系统的方法, 直接使用 imageNamed方法, 调用的是new_ImageNamed的实现

    再看分类Category中创建属性
    一般情况下在 iOS 分类中是无法直接设置属性的,如果在分类的声明中写 @property 只能为其生成 get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash
    但是利用runtime可以巧妙的实现
    比如给UIView添加一个count和一个viewName 在Category的.h中

    @property (nonatomic, assign) NSInteger count;
    
    @property (nonatomic, strong) NSString *viewName;
    

    .m中

    - (void)setCount:(NSInteger)count
    {
        //将name值和对象关联起来, 将name值存储到当前对象中
        /*参数:
         object: 给哪个对象设置属性;
         key: 一个属性对应一个key, 存储后需要通过这个key取出值, key可为double,int等任意类型, 建议用char可节省字节;
         value: 给属性设置的值;
         policy: 存储策略 (assign, copy, retain);
         */
        objc_setAssociatedObject(self, @selector(count), [NSNumber numberWithInteger:count], OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (NSInteger)count
    {
        return [objc_getAssociatedObject(self, @selector(count)) integerValue];
    }
    
    - (void)setViewName:(NSString *)viewName
    {
        objc_setAssociatedObject(self, @selector(viewName), viewName, OBJC_ASSOCIATION_COPY);
    }
    
    - (NSString *)viewName
    {
        return objc_getAssociatedObject(self, @selector(viewName));
    }
    

    这样所有的UIView及其子类都带有这两个属性了

        UIView *testView = [UIView new];
        testView.count = 5;
        testView.viewName = @"testView";
        NSLog(@"%ld,%@",(long)testView.count,testView.viewName);
    

    这里提一下 如果新增的属性和系统的一样 可能会走系统的方法也可能会走你新增的方法 所以命名时需要注意些

    再看获取类的所有属性变量和属性
    假如有个类是这样的

    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString *father;
    
    @property (nonatomic, strong) NSString *mother;
    
    @property (nonatomic, assign) NSInteger age;
    
    @property (nonatomic, strong) NSString *sister;
    
    @end
    

    你需要获取model的所有属性及值可以用class_copyPropertyList获取

        Person *person = [Person new];
        person.father = @"父亲的名字";
        person.mother = @"母亲的名字";
        person.age = 18;
        //获取所有成员变量
        /*
         参数:
         1.哪个类
         2.接收值的地址, 用于存放属性的个数
         3.返回值: 存放所有获取到的属性, 可调出名字和类型
         */
        unsigned int count;
        objc_property_t *propertyArray = class_copyPropertyList([person class], &count);
        for (int i = 0; i < count; i ++) {
            objc_property_t property = propertyArray[i];
            NSString *key = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            id value = [person valueForKey:key];
            NSLog(@"%@ = %@",key,value);
        }
    
    打印结果为 输出结果.png

    再看字典转模型 基本就是处理
    1.字典的key和模型的属性匹配不上;
    2.模型中嵌套模型(模型属性是另外一个模型对象);
    3.数组中装着模型(模型的属性是一个数组,数组中是一个个对象)
    三种情况
    第一种 runtime 是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可
    第二种 需要利用 runtime 的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归
    第三种 需要获取数组里的元素再进去第一步和第二部的判断

    再看动态添加方法 如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决
    比如Person类现在是没有实现eat方法的

        Person *person = [Person new];
        [person performSelector:@selector(eat) withObject:nil];
        NSLog(@"%@",person.father);
    

    可以在Person.m中#import "objc/message.h"

    void effect(id self, SEL _cmd) {
        [self setValue:@"父亲" forKey:@"father"];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if(sel ==NSSelectorFromString(@"eat")) {
            // class: 给哪个类添加方法
            // SEL: 添加哪个方法
            // IMP: 方法实现 => 函数 => 函数入口 => 函数名
            // type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
            class_addMethod(self, sel, (IMP)effect,"v@:@");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    打印出的person.father值为 image.png

    未完待续·····

    相关文章

      网友评论

        本文标题:OC的runtime机制和基本使用场景

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