什么是runtime
说到runtime,根据字面意思就是运行期间。
但我觉得首先应该说一下oc到底是个什么东西。首先,它是一门编程语言;其次,它是基于C语言,在其基础上面增加了面向对象的特性。最后重点的是,oc使用的是“消息结构”,而并非“函数调用”。
关键性的区别就是:基于消息结构的的语言,运行时所执行的代码是由运行环境来决定的,而不是实在编译期间决定的,这一点就衍生出了runtime机制。
简单来说runtime就是在运行的期间去选择到底是去使用哪一个方法,而不是在编译期间决定死了。打个通俗的比方,一个人从同一个起点到同一个目的地,如果乘公交车的话,线路是死的,公交只会按照既定的公交线路来走。如果是自己开车的话,可以随便选择自己随性所欲的路线,最终到达目的地就ok了。乘公交的方式,就好比编译期间决定;自己开车的方式就好比运行期间决定。
预备知识
oc中对象的定义
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
oc中id对象的定义
struct objc_object {
Class isa ;
} *id;
oc中Class的定义
// 类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指向metaClass(元类)
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 => 序列化支持
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存;优化methodLists查找
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
typedef struct objc_class *Class;
关于元类(metaClass):
class结构体中存放的是类的元数据(metadata),其结构体中的首个变量也是isa指针,很显然说明了Class本身也是一个OC对象。
同时调用类方法就是向该类发送消息,会在该方法的方法列表中寻找此方法。
函数调用原型
void objc_msgSend(id self, SEL cmd, ...)
上面函数调用原型是一个参数可变的函数,可以接受两个或者两个以上的参数。
第一个参数代表接收者,第二个参数代表SEL类型,后面的参数就是消息中的附带的参数,按照原顺序依次在后。
下面正式开始
1.理解objc_msgSend的作用
知道了上面的信息后,第一步要明白oc调用机制,也就是上面所说的消息调用机制,至于调用原型,参考上面的函数调用原型。
oc调用对象的方法就是如此,用oc的术语来说,叫做“传递消息”。所以在整个oc开发中,这一点一定要铭记于心。
比如我们在oc中调用方法是这样写的
[someObj doSomeThing:parameter];
但编译器在看到此条消息后,会将其转化成标准的C语言函数调用。如下:
objc_msgSend(someObj, @selector(doSomeThing:), parameter);
当oc开始调用方法的时候,objc_msgSend方法会根据后面的参数来匹配具体的方法。匹配的时候,回去该接收者所属类中的方法列表(methodLists)中去查找。如果查找到了,则跳转到方法中去实现。如果找不到的话,就进行消息转发操作(后面会讲消息转发)。
如果每次调用都来实现类似的操作的话,那么性能肯定会浪费。所以,objc_msgSend会将匹配的结果缓存起来,下次会从缓存里面取。
2.消息转发机制
上面讲了消息的传递机制,但是如果碰到无法解读的消息之后呢?此时oc就会开启消息转发。
先看看转发的几个方法:
1.当前类处理
+ (Bool)resolveInstanceMethod:(SEL)selector
+ (Bool)resolveClassMethod:(SEL)selector
2.转发到其它类来处理
- (id)forwardingTargetForSelector:(SEL)selector
3.转发到其它类来处理
- (void)forwardingInvocation:(NSInvocation*)invocation
关于消息转发的话,总体上就是三步。
首先由resolveInstanceMethod来处理,如果成功则继续处理找到的方法。如果返回false,则继续向下转发。
接下来是第二次机会,由forwardingTargetForSelector:来处理,同样的,成功则返回找到的方法。如果返回为nil的话,则继续向下转发。
还无法处理,就到forwardingInvocation:。到了这一步只能启用完整的消息转发了。会首先创建一个NSInvacation对象,将那条未处理的相关消息(sel,target,参数)全部封装到里面。触发了NSInvacation对象时,就由oc的消息派发系统来调用forwardingInvocation:。当发现某个调用操作不应该由此类来处理的时候,那么就调用超类的同名方法。这样的话继承体系中的每个类都有机会处理该调用请求,直到NSObject。如果最后调用了NSObject的方法,那么该方法会调用“doesNotRecognizeSelector:”来抛出异常信息,结果显示此方法未能得到处理。
消息转发的流程图如下
runtime使用
当我们了解了oc的消息机制后,肯定会疑惑,runtime有何用处?
下面会说明runtime使用的两个方法,但是一定要慎用,因为可能造成未知的错误。
1.method swizzling,俗称“黑魔法”。
2.关联对象(Associated Object)
1.method swizzling
正式因为oc的动态绑定,所以我们才可以使用method swizzling黑魔法。
类方法会将相应的sel名称映射到相关的方法实现上,这些方法均已函数指针的形式来表示,该指针叫做IMP。原型如下:
id (*IMP)(id, SEL, ...)
根据下面这张图,我们来看看映射对应关系:
这种映射关系是在运行期间选择的,所以我们可以通过一些方法来新增,交换其中映射关系,使本该调用imp1的lowercaseString方法从而实现调用了uppercaseString方法。调用后的映射关系图如下:
方法实现的获取函数如下:
Method class_getInstanceMethod(Class aClass, SEL aSelector)
方法实现的交换函数如下:
void method_exchangeImplementations(Method m1, Method m2);
新增一个方法的函数如下:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
通过上面3个方法我们就可以来实现运行期间方法的改变了。
代码示例,就直接上Mattt大神的代码了,如下:
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
//load方法会在类第一次加载的时候被调用
//调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load{
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
SEL systemSel = @selector(viewWillAppear:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(swiz_viewWillAppear:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else{
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}
- (void)swiz_viewWillAppear:(BOOL)animated{
//这时候调用自己,看起来像是死循环
//但是其实自己的实现已经被替换了
[self swiz_viewWillAppear:animated];
NSLog(@"swizzle");
}
@end
在一个自己定义的viewController中重写viewWillAppear
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(@"viewWillAppear");
}
想看效果的自己跑起来试试。
2.关联对象(Associated Object)
一把我们都是直接在程序里面直接添加对象,但万一那段代码不是我们自己写的,或者我们接触不到具体代码怎么办,但是我们又想新增一个对象?这个时候,就可以使用关联对象了。
我们先来了解关联对象的基本知识。
1.对象关联类型
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
2.用给定的键和策略为某对象设置关联对象值
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
3.用给定的键从某对象中获取对应的关联对象值
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
4.移除指定对象的全部关联对象
//移除关联的对象
void objc_removeAssociatedObjects(id object)
要注意的是,设置关联对象的时候,如果想让两个建匹配到同一个值,那么意味着两者的指针必须完全相同。简单来说,就是设置关联对象的建的时候,用静态全局变量做建就行了。
具体使用的示例参考别人写的这篇文章,讲得很清楚了。iOS runtime实战应用:关联对象
参考资料
《effective Objective-C 2.0》
网友评论