iOS Runtime基础学习

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防止被重复点击的相关方法(详细版)

讲解的东西都是超级基础的东西,小伙伴们要是对进行时不太了解的可以看一看,看过以后,在看一看大神们都是怎么灵活运用的。想来那样应该就可以应付工作上的一些需求了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,692评论 7 64
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,521评论 33 466
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,125评论 0 9
  • 若同时追两只兔子........ ..................你一只也抓不到。(-俄罗斯谚语) 连续两周不...
    夏花de解忧杂货铺阅读 322评论 14 22