本篇将会总结Rutime的具体应用实例,结合其动态特性,Runtime在开发中的应用大致分为以下几个方面:
一、动态方法交换:Method Swizzling
实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能。使用到关键方法如下:
↵
下面的图展示了自定义方法交换的过程:
1, 实例代码如下:在控制器中自定义两个方法methodA和methodB,然后在类的预加载函数+ (void)load中分别获取方法A和方法B,然后调用method_exchangeImplementations交换方法A和B的实现,当我们实际去调用方法A时,打印methodB,说明方法A的IMP指向了方法B的实现,方法B同理
2,拦截并替换系统方法
Runtime动态方法交换更多的是应用于系统类库和第三方框架的方法替换。在不可见源码的情况下,我们可以借助Rutime交换方法实现,为原有方法添加额外功能,这在实际开发中具有十分重要的意义。
下面将展示一个拦截并替换系统方法的示例:为了实现不同机型上的字体都按照比例适配,我们可以拦截系统UIFont的systemFontOfSize方法,load方法不需要手动调用,iOS会在应用程序启动的时候自动调起load方法,而且执行时间较早,所以在此方法中执行交换操作比较合适。具体操作如下:
注意: 这里有一个坑,注意避坑,自定义方法 adjust_systemFontOfSize里面最后一句不能写 return [UIFont systemFontOfSize:fontSize * scale];否则会造成死循环,原因:定义一个UILabel对象label,设置字体大小,调用label.font = [UIFont systemFontOfSize:fontSize];然后systemFontOfSize实现的时候会去调用adjust_systemFontOfSize:方法,然后在adjust_systemFontOfSize:方法最后又调用systemFontOfSize:因为systemFontOfSize:指向adjust_systemFontOfSize:的实现,所以又再一次调systemFontOfSize:如此循环调用就成了死循环
二,类别(Category)添加新属性
我们在开发中常常使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承会造新了类的属性和方法过多的继承父类,最后类越来越庞大,添加不必要的继承关系无疑增加了代码的复杂度。
遗憾的是,OC的类目并不支持直接添加属性,如果我们直接在分类的声明中写入Property属性,那么只能为其生成set与get方法声明,却不能生成成员变量,直接调用这些属性还会造成崩溃。
所以为了实现给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:
注意:key与关联属性一一对应,我们必须确保其全局唯一性,一般可以用static关键字定义一个静态字符串作为key,但我们常常使用@selector(methodName)作为key,可以减少key的定义。
现在演示一个代码示例:为UITableView增加一个分类:UITableView+datas,并为其设置关联属性datas(数据源属性),相关代码如下:
使用@selector(methodName)作为key
使用static关键字定义一个静态字符作为key
测试一下
三、获取类的详细信息
1,获取类的属性(只能获取由property声明的属性,包括.m中的,获取的属性名称不带下划线)列表
2,获取类的成员变量(能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外))列表
3.获取类的所有方法
4.获取当前遵循的所有协议
可以打印到协议protocolA
注意:C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏
四、解决同一方法高频率调用的效率问题
五、方法动态解析与消息转发
Runtime能够让我们在运行时动态添加一个未实现的方法,主要有两个应用场景: 场景1:动态添加未实现方法,解决代码中因为方法未找到而崩溃的问题; 场景2:利用懒加载思路,当一个类中的方法非常多且有些方法不常用的时候如果直接写了方法,那么这些方法会直接加载到内存,于是内存就很大了,所以我们使用runtime的动态添加方法就不会出现这个问题,只有在运行时才会添加到内存,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:
1.动态添加未实现方法
对象在接收到未知的消息时,首先会调用所属类的方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。
2.解决方法无响应崩溃问题
如果在上一步无法处理消息,则Runtime会继续调以下方法:- (id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:
这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。
执行OC方法其实就是一个发送消息的过程,若方法未实现,我们可以利用方法动态解析与消息转发来避免程序崩溃,也就是找一个方法的备用接收者,这主要涉及下面一个处理未实现消息的过程:
3.快速消息转发在项目中的使用
自定义容器 view 转发给自己的 subview
在日常开发中, 我们往往会遇到一个场景, 一个同样的自定义 view 用于 cell 中和不用于 cell 中, 这时候我们会自定义一个 view 暴露如下图所示接口, 供外界调用。
传统做法.m文件里面要写一堆的setter和getter方法:
但这里如果使用快速消息转发, 会是代码更加简洁, 我们仅需要在 - (id)forwardingTargetForSelector:(SEL)aSelector 方法中 return 加在自己中的自定义 view 对象即可, 当然这里要配合 @dynamic 关键字使用。因为 @property 关键字会生成对应的 getter 、setter 方法, 这样的话 cell 相当于实现了 getter 、setter 方法, 自然不会走到 - (id)forwardingTargetForSelector:(SEL)aSelector 中。
而 @dynamic 关键字意味着编译器不会帮助我们自动生成 getter 、setter 方法。这样一来, 相当于我们只申明了这些方法而并没有实现这些方法, 当调用时便会遵循消息转发机制的步骤, 从而调用 - (id)forwardingTargetForSelector:(SEL)aSelector。
六、动态操作属性
1.动态修改属性变量
现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性name,这样的操作我们就可以使用Runtime可以动态修改对象属性。
基本思路:首先使用Runtime获取Peson对象的所有属性,找到name,然后使用ivar的方法修改其值。具体的代码示例如下:
setIvar()和valueForKey利用了KVC键值观察的原理
2.实现 NSCoding 的自动归档和解档
归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐,而且一旦增加几个属性,还得在这两个方法里面加代码,一旦忙起来很容易遗漏。 归档操作主要涉及两个方法:encodeObject 和 decodeObjectForKey,原始的归档方法代码如下:
现在,我们可以利用Runtime来改进它们,关键的代码示例如下:
3.实现字典与模型的转换
字典数据转模型的操作在项目开发中很常见,通常我们会选择第三方如YYModel、JsonModel;其实我们也可以自己来实现这一功能,主要的思路有两种:KVC、Runtime,总结字典转化模型过程中需要解决的问题如下:
现在,我们使用Runtime来实现字典转模型的操作,大致的思路是这样: 借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。 代码如下: