从开始写博客的时候,就由一个准备把Runtime好好研究整理出来。后来发现写runtime得文章太多了,而且自己研究的也算不上有多么深入,写出来估计很多错误- —–(因为主要是给自己看得)。最近在项目中,第一次用到了runtime解决了一个实际问题,这启发我,我的文章应该从怎么利用runtime去解决实际的问题,给自己提供一个解决问题的新思路。
##runtime 关联对象
从关联对象说起,是因为在最近的项目中用到了这个方法。当时的场景是,我需要创建一个category,里面需要一个block对象。然而我们知道,我们不能在category中添加成员变量。如果我们尝试添加的话,编译器会报错。当然,使用全局变量能解决这个问题,但明显是一个很优雅的方法,也很容易造成错误。Objective-C针对这一中问题,提供了一个解决方案,就是关联对象(Associated Object)。
我们可以想象,关联对象就是一个key - value的模型,把对象通过特定的Key链接到相关的实例上。不过由于使用的是C的借口,所以key是一个void指针。同时,我们要需要为这个关联对象设定内存管理策略。相关的内存的管理策略如下:
1 | OBJC_ASSOCIATION_ASSIGN |
对OBJC_ASSOCIATION_ASSIGN
的,当关联对象所关联的实例被释放掉后,对象不会被释放掉。而OBJC_ASSOCIATION_RETAIN
和OBJC_ASSOCIATION_COPY
两个的话,在实例被释放掉同时,关联对象也会被release掉。当我们用同一个key去关联另外的实例对象的时候,也会自动释放掉之前关联的对象。这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。
关联对象的的runtime函数如下
1 | // 设置关联对象 |
下面就给出在项目中实际遇到的问题给做一个例子。
首先在头文件中定义了以下内容。
可以看到,这个category是为了扩展vc,让vc具有一个弹出系统通讯录的功能。
在.m文件中,如下实现block关联对象
然后就像正常的对象一样操作就行了
在上面看到,我对关联对象使用的key是用了@selector()
。那为什么可以使用SEL来代替设定一个固定的key呢?
方法与消息
- SEL
又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector结构体的详细定义没有在
- IMP
通过SEL找到函数后,就可以获取到该函数的IMP。IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下
1 | id (*IMP)(id, SEL, ...) |
通过取得IMP,我们可以跳过runtime的消息传递机制,直接通过IMP执行指向的函数实现。这样一来可以提高效率,省去了消息传递中的一系列过程
1 | 回答上面的问题 |
##下面是看的一些文章的摘录和自己的一项心得,主要是给自己做一个备忘。
选择器相关的操作函数包括:
1 | // 返回给定选择器指定的方法的名称 |
方法操作的函数如下
1 | // 调用指定方法的实现 |
##方法调用流程
在Objective-C中,消息会一直到运行时绑定的方法实现上。我们常用的[receiver message]会被runtime转换为一个函数调用objc_msgSend
。如下
objc_msgSend(receiver, selector)
如果消息中还有其它参数,则该方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
这个函数在执行的过程中完成了所有动态绑定的过程。
- 找到SEL对应的方法实现。由于一个方法可能在不同的类有不同的实现,所有我们要依赖于接受者的类来确定确切的实现。
- 调用方法的实现,将参数传入。
- 将方法的返回这作为的自己的返回值。
一个基本消息的框架
注意到,在objc_msgSend
中有两个隐藏参数:
- 消息接受对象
- 方法的selecor
隐藏是以为内在定义的方法中没有申明,是在编译期被插入实现代码的。
##消息转发
当一个对象能接受一个消息的时候,那么一切都会按照我们想得去做,但是如果不能接受消息的时候,会发生什么?一般情况下,如果不能相应[object message],编译期会报错。如果是以perform来调用的,那么在运行时才会确定object是否会接收消息,如果不行的话,就会造成程序崩溃。
当然,我们可以通过respondsToSelector:
来判断一下是否会响应这个消息,但是我们要用runtime来搞一搞,所以我们应该从消息转发机制
来考虑。
消息转发机制基本分为三个步骤
- 动态方法解析
- 备用接收者
- 完成转发
###动态方法解析
当当对象接收到位置的消息时,首先会调用类的+resolveInstanceMethod:(实例方法)
或者+resolveClassMethod:(类方法)
。因此,我们可以去增加一个处理方法。在消息不能被对象接收的时候就运行这个方法,前提是这个方法已经被实现了。
1 | void functionForMethod1(id self, SEL _cmd) { |
备用接收者
当我们的对象不能处理消息的时候,我们将消息转发到一个备用的对象,只要这个对象的类实现了这个方法- (id)forwardingTargetForSelector:(SEL)aSelector
如果返回的不是一个nil的话,那么这个对象就会被作为消息的心的接收者。而在外部看来,这个消息仍然被我们的发送的对象处理了。注意,这个方法返回的对象不是self自身,不然就是个无限循环了。
1 | @interface SUTRuntimeMethodHelper : NSObject |
###完成的消息转发
如果上述的行为都不能解决的话,runtime在这时候会给对象最后一次机会:1
- (void)forwardInvocation:(NSInvocation *)anInvocation
这个时候,消息的所有未处理的内容都被封装到了anInvocation中,包括SEL,target,参数。我们在这里可以选着将消息转发给其他的对象。
forwardInvocation:方法的实现有两个任务:
定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
对于这一块,还没有具体的实践,毕竟到这一步,用runtime来解决不如好好回去check以下代码吧。
##Method Swizze
Method Swizzling是改变一个selector的实际实现的技术.这样,我们可以在runtime的时候修改类分发的队形的函数,修改对应的实现。
例如,我们想在每一个viewDidAppear
增加一个跟踪代码,观察哪个vc被调用了,那么我们就可以通过Method Swizzling
###Swizzling 注意事项
Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。
+load在父类,子类,分类的实现都会分别调用,所以+load更适合
###Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。
###特别的地方
>
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看
头文件以了解事件是如何发生的。 - 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
##总结
runtime到这里,大概讲了下两个问题,方法转发和关联对象。觉得还是不够,因为很多东西写起来发现自己也不是很了解,暂且做一个笔记。
这里感谢南峰子,写得runtime教程非常详细,给了我很多帮助。
本文参考了大量下面三篇文章的内容。