OC里的runtime已经被大家说的很多了,有很多很好的文章。这里自己再整理一下。
runtime的概念
runtime概念的描述取自这篇博客:
OC是一门动态语言,它将很多静态语言在编译和链接时做的事情放到了运行时来处理。
这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
Runtime库主要做下面几件事:
1.封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
2.找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。
那么我们可以利用runtime来干什么呢?这么多牛人写过runtime的利用之后,基本上大家都是用来method swizzled的。也就是在运行时用另一个method替换原先的method。
method swizzled的原理从后往前讲,顺序是:
- 替换指向method的指针,从而实现了替换method。
- 某个类有一个地方是用来存储指向所有method的指针的。
- OC是C语言的扩展,是怎样实现面向对象特性的。(其中包括类是怎样的结构或者说类是怎样实现的。上一点中讲到的存储method指针的东西是在类中是怎样的存在?@_@)
类
看一下Class在OC里面是怎么定义的
原来Class是一个指向objc_class
结构体的指针。
看一下这个结构体
看到里面有一个struct objc_method_list ** methodLists
,这就是存储该类所有方法的地方了。根据这篇博客所说,查找方法的时候并不是每次都去遍历methodList的,而是先去cache中查,cache中存储了最近常用的方法。
至于objc_class
结构体里面的其他的元素如isa
等,下回再说。
看一下objc_method_list
这个结构体
它有一个指向存储废弃方法列表的指针struct objc_method_list *obsolete
,还有方法的个数int method_count
,还有一个用于存储方法的数组struct objc_method method_list[1]
。其中数组的长度是可变的。
看一下objc_method
这个结构体,
SEL method_name
表示方法名,char *method_types
表示参数及返回值类型,IMP method_imp
表示指向方法实现的指针。
看一下SEL
的定义,SEL是objc_selector指针,表示运行时方法的名字,根据方法名来找到方法。它不具备函数重载的特性,当两个SEL一样即使参数不一样时,会发生错误。
看一下IMP
,它是一个函数指针的别名。一个函数必须包括,id
即消息的接收者,SEL
即方法选择器,以及一些参数。可以通过获取IMP而直接调用某个方法,跳过消息传递机制。
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
method swizzled
method swizzled
也被大家用得很多了,其原理就是替换IMP。
借用一下这篇博客里的图片。每个Method对象里面都维持着一个SEL与IMP的对应关系,而实现method swizzled其实就是改变对应关系。
用一个例子来说比较好,某个有名的SDK有自动统计页面的功能,估计也是使用了mthod swizzled的原理,这就减少了开发者需要在每个页面的viewDidAppear:
,viewDidDisappear:
里面进行打点的工作量。就拿viewDidAppear:
来说,加入我要实现自动打点,需要先执行一下我自己的打点方法,然后再去执行开发者的该方法。
runtmie.h
里面有很多方法,看方法名猜测主要是以下几个方法可以很方便的实现method swizzled:
class_getInstanceMethod
获取实例方法。Method class_getInstanceMethod(Class cls, SEL name)
。
class_getClassMethod
获取类方法。Method class_getClassMethod(Class cls, SEL name)
.
class_getMethodImplementation
获取实例的某个方法的实现函数的指针。IMP class_getMethodImplementation(Class cls, SEL name)
.
class_copyMethodList
获取所有实例方法。Method *class_copyMethodList(Class cls, unsigned int *outCount)
.
class_addMethod
向类中添加一个方法。BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
.
class_replaceMethod
替换类中某个方法的实现。IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
.
method_getImplementation
获取某个方法的实现函数的指针。IMP method_getImplementation(Method m)
.
method_getTypeEncoding
获取某个方法的参数和返回值类型。const char *method_getTypeEncoding(Method m)
.
method_setImplementation
设置某个方法的实现函数指针。IMP method_setImplementation(Method m, IMP imp)
.
method_exchangeImplementations
交换两个方法的实现函数指针。void method_exchangeImplementations(Method m1, Method m2)
.
要实现在开发者viewDidAppear里面自动打点的功能,首先要先把自己的打点方法添加到UIViewController类里面。而**category
**可以实现给一个类添加方法的功能。
category
官方文档里面是这样说category的:
A category can be declared for any class, even if you don’t have the original implementation source code (such as for standard Cocoa or Cocoa Touch classes). Any methods that you declare in a category will be available to all instances of the original class, as well as any subclasses of the original class. At runtime, there’s no difference between a method added by a category and one that is implemented by the original class.
Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class
在category里面声明的要给某个类添加的方法,可以在该类实例以及该类的子类实例中都能够访问。在运行时看来,从category里添加的方法与原有的方法其实是没有什么区别的。
category可以添加实例方法或者类方法,但是不能用来添加属性。
Avoid Category Method Name Clashes
也就是说在使用category添加新方法的时候,尽量避免起名冲突。最好的方法是加上你自己的前缀。例如xyz_isOccludedByView
等这种命名方法
一般是在@interface之后使用(categoryName)
来声明一个category。一般是放在original+categoryName.h/.m
里面。
好,现在就声明一个UIViewController的category,往里面添加我自己的方法
@interface UIViewController (MYSDK)
- (void)myViewDidAppear:(BOOL)animated;
@end
@implementation UIViewController (MYSDK)
- (void)myViewDidAppear:(BOOL)animated {
[self myViewDidAppear:animated];
NSLog(@"-----my view Did appear----,vc name = %@",[self class]); // 此处打印出UIViewController的名字
}
@end
这里面我在myViewDidAppear:
里面看上去是又调用了自己[self myViewDidAppear:animated];
,但这恰恰是下面要说明的,进行了method swizzled之后,就不会出现死循环。
那么什么时候进行method swizzled合适呢?此处涉及到+load
和+initialize
这两个方法。具体可以见这篇博客
+load vs +initialize
Swizzling should always be done in +load.
There are two methods that are automatically invoked by the Objective-C runtime for each class. +load is sent when the class is initially loaded, while +initialize is called just before the application calls its first method on that class or an instance of that class. Both are optional, and are executed only if the method is implemented.
Because method swizzling affects global state, it is important to minimize the possibility of race conditions. +load is guaranteed to be loaded during class initialization, which provides a modicum of consistency for changing system-wide behavior. By contrast, +initialize provides no such guarantee of when it will be executed—in fact, it may never be called, if that class is never messaged directly by the app.
+load
方法是当类或分类被添加到oc runtime时被调用的。子类的+load方法会在它所有父类的+load方法之后执行,而分类的+load方法会在它的主类的+load方法之后执行。不同类之间的+load方法的调用顺序是不确定的。
+initialize
方法是在类或者它的子类都到第一条消息(包括类方法和实例方法)之前被调用的。及+initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或者他的子类发送消息,那么它的+initialize方法是永远不会被调用的。
因为method swizzling是一个影响全局的操作,理应把可能的竞争降到最低。+load能够确保在类初始化的时候被加载,从而实现method swizzling系统级的影响。
Swizzling should always be done in a dispatch_once.
Again, because swizzling changes global state, we need to take every precaution available to us in the runtime. Atomicity is one such precaution, as is a guarantee that code will be executed exactly once, even across different threads. Grand Central Dispatch’s dispatch_once provides both of these desirable behaviors, and should be considered as much a standard practice for swizzling as they are for initializing singletons.
method swizzling应该放在dispatch_once里面。
OK,实现在viewDidAppear里面自动打点的代码如下:
#import <objc/runtime.h>
@implementation UIViewController (MYSDK)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
Class class = [self class];
SEL originalSelector = @selector(viewDidAppear:);
SEL mySwizzledSelector = @selector(myViewDidAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method mySwizzledMethod = class_getInstanceMethod(class, mySwizzledSelector);
BOOL addResult = class_addMethod(class, originalSelector, method_getImplementation(mySwizzledMethod), method_getTypeEncoding(mySwizzledMethod));
if (addResult){
class_replaceMethod(class, mySwizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, mySwizzledMethod);
}
});
}
- (void)myViewDidAppear:(BOOL)animated
{
[self myViewDidAppear:animated];
NSLog(@"-----my view Did appear----,vc name = %@",[self class]);
}
@end
根据官方对class_addMethod
的描述,class_addMethod
用于向类中添加一个新方法。class_addMethod可以对父类的该方法进行覆盖,但是不会替换子类该方法的实现。所以说,如果UIViewController
的子类覆盖了viewDidAppear:
该方法,那么在上放的代码里面addResult就是NO,即class_addMethod不成功,这时候就要使用method_exchangeImplementations
替换方法的实现。
会造成循环吗?
看到在myViewDidAppear:
里面又调用了myViewDidAppear:
。如果没有使用method swizzling,那么肯定是造成死循环。但使用了method swizzling就不会。但是如果是在方法里面继续调用[self viewDidAppear:animated]
,就会造成无限循环,因为这个方法的实现已经在运行时被指定为为myViewDidAppear:
了。
如果开发者本身就对该方法进行了method siwzzling,那么会覆盖他的操作吗?
只要在我的代码里面遵循总是调用方法的原始实现这个规则,那么就不会有问题。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分
如果是想要对按钮的点击事件进行打点
一般对UIButton的点击事件进行响应,要么是在xib里面牵出一个action,要么是在代码里面[myButton addTarget:self action:xxx forControlEvents:UIControlEventTouchUpInside]
,发现这其实是UIControl里的一个方法。查看UIControl
里面,发现有下面这两个方法
OK,那就对第一个方法进行打点。
使用swizzling时的注意事项
Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
- 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
官方文档上有关于category的相关问答:
Finding and Fixing Category Method Name Clashes
官方文档上说,在编译器的环境参数中将OBJC_PRINT_REPLACED_METHODS
设置为YES,就能够检测方法方法名是否有冲突。具体做法如下:
1.Product -> Scheme -> Edit Scheme
2.在Run下面的Arguments的环境变量中设置OBJC_PRINT_REPLACED_METHODS
的值为YES。
3.然后运行程序,就能看到Xcode打印出来的东西。里面还能看到系统framework里的IMP。
其他
现在已经有很多工具是基于method swizzling的了,其中比较有名的是**JSPatch
**。它是利用了JavaScriptCore.framework,来实现方法的替换,本质上其实是method swizzling。具体相关可以查看其作者的博客。
参考链接
1.http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/
2.http://nshipster.com/method-swizzling/
3.https://developer.apple.com/library/ios/qa/qa1908/_index.html#//apple_ref/doc/uid/DTS40016829