runtime之method swizzling

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的原理从后往前讲,顺序是:

  1. 替换指向method的指针,从而实现了替换method。
  2. 某个类有一个地方是用来存储指向所有method的指针的。
  3. OC是C语言的扩展,是怎样实现面向对象特性的。(其中包括类是怎样的结构或者说类是怎样实现的。上一点中讲到的存储method指针的东西是在类中是怎样的存在?@_@)

看一下Class在OC里面是怎么定义的

class

原来Class是一个指向objc_class结构体的指针。

看一下这个结构体

struct objc_class

看到里面有一个struct objc_method_list ** methodLists,这就是存储该类所有方法的地方了。根据这篇博客所说,查找方法的时候并不是每次都去遍历methodList的,而是先去cache中查,cache中存储了最近常用的方法。

至于objc_class结构体里面的其他的元素如isa等,下回再说。

看一下objc_method_list这个结构体

struct objc_method_lsit

它有一个指向存储废弃方法列表的指针struct objc_method_list *obsolete,还有方法的个数int method_count,还有一个用于存储方法的数组struct objc_method method_list[1]。其中数组的长度是可变的。

看一下objc_method这个结构体,

Method
struct objc_method

SEL method_name表示方法名,char *method_types表示参数及返回值类型,IMP method_imp表示指向方法实现的指针。

看一下SEL的定义,SEL是objc_selector指针,表示运行时方法的名字,根据方法名来找到方法。它不具备函数重载的特性,当两个SEL一样即使参数不一样时,会发生错误。
objc_selector

看一下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其实就是改变对应关系。

method and implementation

method and implementation2

用一个例子来说比较好,某个有名的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一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分

selector c
selector c2

如果是想要对按钮的点击事件进行打点

一般对UIButton的点击事件进行响应,要么是在xib里面牵出一个action,要么是在代码里面[myButton addTarget:self action:xxx forControlEvents:UIControlEventTouchUpInside],发现这其实是UIControl里的一个方法。查看UIControl里面,发现有下面这两个方法

UIControl sendevent

OK,那就对第一个方法进行打点。

使用swizzling时的注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对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。

objc print replace method

3.然后运行程序,就能看到Xcode打印出来的东西。里面还能看到系统framework里的IMP。

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

Show Comments