iOS Runtime基础学习

作者: SunshineBrother | 来源:发表于2017-03-16 15:43 被阅读273次

    Runtime简介

    因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

    Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

    Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

    运行时在开发中的主要应用场景

    • 字典转模型
    • 给分类增加关联对象,开发框架时解耦
    • 交换方法,在无法修改系统或者第三方框架的方式时
      • 利用交换方法,先执行自己的方法
      • 再执行系统或第三方框架的方法
      • 黑魔法,对系统/框架版本有很强的依赖性

    1、动态获取类的属性

    常用与字典转模型的时候使用

    我这里就用一个NSObject的分类给大家详细的讲解一下
    大概思路:
    1、class_copyPropertyList 获取属性的数组
    2、遍历数组,property_getName 获取每一个属性的名称
    3、添加到数组中
    4、free 释放数组

    首先我先创建了一个继承自NSObject的类,里面有两个成员变量,然后在创建一个分类,我们要在分类中获取person类中的成员变量

    DB39A491-125F-4977-89DE-09572415982D.png

    在写之前我们肯定要让分类中添加#import <objc/runtime.h>

    在分类方法中
    • 第一步:调用运行时方法,获取类的属性列表
      调用的是class_copy...方法
    屏幕快照 2017-03-16 上午11.55.19.png

    我们可以看到一共联想出了4中方法,Ivar 成员变量,Method 方法,Property 属性,Protocol 协议,通过这四种方法,我们可以获取所有我们想要知道的。
    这里我们想要获取Person类的属性,所以我们就调用了class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)这个方法。

    方法解析

    48CD87E0-FBCF-4451-AD55-36EA01AA064B.png

    我们可以看到这个方法里面一共有两个参数,我们点住该方法,按住control键,可以看到该方法的一些具体内容,内容如下

    1F0BF0E5-1A91-49E7-81DB-894ED7E39EB4.png
     /**
         参数
         1. 要获取的类
         2. 类属性的个数指针
         
         返回值
         所有属性的`数组`,C 语言中,数组的名字,就是指向第一个元素的地址
         
         retain/create/copy 需要 release,最好 option + click
         */
        unsigned int count = 0;
        objc_property_t *proList = class_copyPropertyList([self class], &count);
        
        NSLog(@"属性的数量 %d", count);
    

    释放数组的方法free(proList);

    这个时候,我们打印count,会发现count=2;有图有真相,请看下面打印

    3EE4DEAB-9577-4D12-B2FE-79BA6791AB17.png

    这个时候,我们虽然获取到了2,但是我们是想要具体的内容,而不是这个,所以还需要继续向下走

    • 第二步:遍历数组,拿到我们想要的东西
      这里面有两个主要的方法
      • objc_property_t pty = proList[i];
      • const char *cName = property_getName(pty);
     // 遍历所有的属性
        for (unsigned int i = 0; i < count; i++) {
            
            // 1. 从数组中取得属性
            /**
             C 语言的结构体指针,通常不需要 `*`
             */
            objc_property_t pty = proList[i];
            
            // 2. 从 pty 中获得属性的名称
            const char *cName = property_getName(pty);
            
            NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
            
            NSLog(@"%@", name);
        }
    
    

    打印结果


    66C705D1-5904-4B91-840F-133D439D1978.png
    • 第三步:获取关联对象,动态添加属性,当Person对象属性已经获取的时候,就直接返回,防止多次调用运行时方法提高效率
      需要用到的两个方法

    • objc_getAssociatedObject

    • objc_setAssociatedObject

    const char * kPropertiesListKey = "CZPropertiesListKey";
     // --- 1. 从`关联对象`中获取对象属性,如果有,直接返回!
        /**
         获取关联对象 - 动态添加的属性
         
         参数:
         1. 对象 self
         2. 动态属性的 key
         
         返回值
         动态添加的`属性值`
         */
        NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
        if (ptyList != nil) {
            return ptyList;
        }
    
    
    // --- 2. 到此为止,对象的属性数组已经获取完毕,利用关联对象,动态添加属性
        /**
         参数
         
         1. 对象 self [OC 中 class 也是一个特殊的对象]
         2. 动态添加属性的 key,获取值的时候使用
         3. 动态添加的属性值
         4. 对象的引用关系
         */
        objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    动态获取类的属性,方法讲解完毕,大家感觉如何,下面是完整的代码
    {
        const char * kPropertiesListKey = "CZPropertiesListKey";
        // --- 1. 从`关联对象`中获取对象属性,如果有,直接返回!
        /**
         获取关联对象 - 动态添加的属性
         
         参数:
         1. 对象 self
         2. 动态属性的 key
         
         返回值
         动态添加的`属性值`
         */
        NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
        if (ptyList != nil) {
            return ptyList;
        }
        
        // 调用运行时方法,取得类的属性列表
        // Ivar 成员变量
        // Method 方法
        // Property 属性
        // Protocol 协议
        /**
         参数
         1. 要获取的类
         2. 类属性的个数指针
         
         返回值
         所有属性的`数组`,C 语言中,数组的名字,就是指向第一个元素的地址
         
         retain/create/copy 需要 release,最好 option + click
         */
        unsigned int count = 0;
        objc_property_t *proList = class_copyPropertyList([self class], &count);
        
        NSLog(@"属性的数量 %d", count);
        // 创建数组
        NSMutableArray *arrayM = [NSMutableArray array];
        
        // 遍历所有的属性
        for (unsigned int i = 0; i < count; i++) {
            
            // 1. 从数组中取得属性
            /**
             C 语言的结构体指针,通常不需要 `*`
             */
            objc_property_t pty = proList[i];
            
            // 2. 从 pty 中获得属性的名称
            const char *cName = property_getName(pty);
            
            NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
            
    //        NSLog(@"%@", name);
            // 3. 属性名称添加到数组
            [arrayM addObject:name];
        }
        
        // 释放数组
        free(proList);
        
        // --- 2. 到此为止,对象的属性数组已经获取完毕,利用关联对象,动态添加属性
        /**
         参数
         
         1. 对象 self [OC 中 class 也是一个特殊的对象]
         2. 动态添加属性的 key,获取值的时候使用
         3. 动态添加的属性值
         4. 对象的引用关系
         */
        objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
        return arrayM.copy;
    }
    
    

    2、字典转模型

    上面那一个方法可以使我们获取Person对象的所有属性,那么字典转模型使用一个KVC就可以了。
    具体方法

    // 所有字典转模型框架,核心算法!
    + (instancetype)objWithDict:(NSDictionary *)dict {
        // 实例化对象
        id object = [[self alloc] init];
        
        // 使用字典,设置对象信息
        // 1> 获得 self 的属性列表
        NSArray *proList = [self getPersonArray];
        
        // 2> 遍历字典
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            
            NSLog(@"key %@ --- value %@", key, obj);
            // 3> 判断 key 是否在 proList 中
            if ([proList containsObject:key]) {
                //  说明属性存在,可以使用 `KVC` 设置数值
                [object setValue:obj forKey:key];
            }
        }];
        
        return object;
    }
    
    

    3、交叉方法

    进行时之所以那么被广大的iOS程序员所敬仰,很大的一部分原因就是因为这个交叉方法,我们可以用这个方法更改任意代码,交叉方法也被称为黑魔法。但是,我们平时不到万不得已最好不要用这个黑魔法,就像斗地主一样,你上来就王炸,那么以后的路肯定就不会好走了。
    援引一段AFNetWorking作者的一段话

    最后,请记住仅在不得已的情况下使用 Objective-C runtime。随便修改基础框架或所使用的三方代码是毁掉你的应用的绝佳方法哦。请务必要小心哦。

    原文链接

    交叉方法一般使用的地方
    • 我们使用第三方框架时,我们发现了一些错误,我们不用修改第三方的代码
    • 第三方框架或者系统原生方法十分不够我们的使用,我们强烈希望增加一些功能
    这里面有三个常用的方法
    • 获取类方法
    Class PersonClass = object_getClass([Person class]);
    SEL oriSEL = @selector(test1);
    Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
    
    • 替换原方法实现
    class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    
    • 交换两个方法
    method_exchangeImplementations(oriMethod, cusMethod);
    

    这里我给imageView做了一个交换方法,调整图像尺寸
    代码如下

    // 在类被加载到运行时的时候,就会执行
    + (void)load {
       
       // 1. 获取 UIImageView 类的 实例方法 `setImage:`
       Method originalMethod = class_getInstanceMethod([self class], @selector(setImage:));
       // 2. 获取 UIImageView 类的 实例方法 `cz_setImage:`,本身定义在分类中
       Method swizzledMethod = class_getInstanceMethod([self class], @selector(cz_setImage:));
    
       // 3. 交换方法 setImage 和 cz_setImage,交换完成之后
       // 1> 调用 setImage 相当于调用 cz_setImage
       // 2> 调用 cz_setImage 相当于调用 setImage
       method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
    ///  1. 当在其他位置调用 `setImage` 方法时,`自动`调用 cz_setImage: 方法
    - (void)cz_setImage:(UIImage *)image {
       NSLog(@"%s %@", __FUNCTION__, image);
       
       // 1. 根据 imageView 的大小,重新调整 image 的大小
       // 使用 `CG` 重新生成一张和目标尺寸相同的图片
       UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0);
       
       // 绘制图像
       [image drawInRect:self.bounds];
       
       // 取得结果
       UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
       
       // 关闭上下文
       UIGraphicsEndImageContext();
       
       // 调用系统默认的 setImage 方法
       [self cz_setImage:result];
    }
    
    

    4、给分类添加属性

    给分类添加属性其实就是获取关联对象,然后添加
    内容比较简单,直接上代码

    .h文件
    
    //分类的头文件
    @interface ClassName (CategoryName)
    @property (nonatomic, strong) NSString *str;
    @end
    .m文件
    
    //实现文件
    #import "ClassName + CategoryName.h"
    #import <objc/runtime.h>
    
    static void *strKey = &strKey;
    
    @implementation ClassName (CategoryName) 
    -(void)setStr:(NSString *)str  
    {  
        objc_setAssociatedObject(self, & strKey, str, OBJC_ASSOCIATION_COPY);  
    }  
    
    -(NSString *)str  
    {  
        return objc_getAssociatedObject(self, &strKey);  
    }
    @end
    

    5、NSClassFromString(根据一个字符串生成一个类)

    使用NSClassFromString 使用NSClassFromString可以直接从字符串初始化出对象出来,即使不引用头文件也没关系。
    这个方法判断类是否存在,如果存在就动态加载的,不存为就返回一个空对象;
    简单使用方法

     id myObj = [[NSClassFromString(@"MySpecialClass") alloc] init];
    
      正常情况下等价于:
    
    id myObj = [[MySpecialClass alloc] init];
    
          但是,如果你的程序中并不存在MySpecialClass这个类,下面的写法会出错,而上面的写法只是返回一个空对象而已。
    

    其中对这个方法有一个比较经典的用法,iOS 万能跳转界面方法 (runtime实用篇一)
    想要了解的小伙伴们可以点进去看一看作者的思路。

    最后在给大家推荐几篇比较好的文章,有兴趣的同学可以看一看

    runtime详解
    OC最实用的runtime总结,面试、工作你看我就足够了!
    Runtime 10种用法(没有比这更全的了)
    iOS 万能跳转界面方法 (runtime实用篇一)
    button防止被重复点击的相关方法(详细版)

    讲解的东西都是超级基础的东西,小伙伴们要是对进行时不太了解的可以看一看,看过以后,在看一看大神们都是怎么灵活运用的。想来那样应该就可以应付工作上的一些需求了。

    相关文章

      网友评论

        本文标题:iOS Runtime基础学习

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