程序员iOS Developer

Objective-C Runtime(三)Method Swi

2017-12-23  本文已影响71人  liuyanhongwl

Runtime 3 Method Swizzling

Objective-C Runtime(一)

Objective-C Runtime(二)

Objective-C Runtime(三)

Objective-C Runtime(四)

持续更新中...

Method Swizzling

其实runtime的概念、特性已经讲完了。现在来说一下runtime很强大的一个黑色技能:Method Swizzling。

Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。

消息传递 中有讲到,每个类里都有一个 dispatch table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。swizzle 一个方法其实就是在程序运行时在 dispatch table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。

转换前,一一对应:

method_swizzling_1.png

转换后,交换一一对应:

method_swizzling_2.png

objc/runtime.h中,OC提供了以下API来动态替换方法的实现:

这些方法归根结底,都是偷换了method的IMP。

class_replaceMethod

class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 

在文档中详细说明了,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用 class_addMethod 来为该类增加一个新方法。也因此它需要在调用时传入types参数,而method_exchangeImplementationsmethod_setImplementation却不需要。

method_setImplementation

最简单,仅仅是给一个方法设置其实现方式。

method_exchangeImplementations

顾名思义,是交换两个方法的实现,等同于调用两次method_setImplementation

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

Method Swizzling 的应用

有时候我们想对已有类的已有实现增加一些额外处理,这时候我们可以在已有类的分类中做Method Swizzling。

下面我们在UIControl的分类里,将-setTag:和自定义的-xxx_setTag:方法交换:

- (void)xxx_setTag:(NSInteger)tag {
    NSLog(@"%s  %@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self xxx_setTag:tag];
}

上面是自定义-xxx_setTag:方法的实现,乍一看像是递归。但是别忘了我们是要调换方法的IMP的,在runtime的时候,函数实现已经被交换了。调用-setTag:会调用你实现的-xxx_setTag:,而在 -xxx_setTag:里调用-xxx_setTag:实际上调用的是原来的-setTag:

//UIControl+XXXExtension.m
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        //1
        SEL originalSelector = @selector(setTag:);
        SEL swizzledSelector = @selector(xxx_setTag:);
        //2
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        //3
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
        //4
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
        //5
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
  1. 拿到要交换的两个方法名SEL
  2. 通过方法名SEL,拿到方法对象Method
  3. 在交换方法前,先调用了class_addMethod。是因为要保证所交换的原方法是本类的方法,不是父类的方法。class_addMethod会覆盖父类的方法实现,但是不会替换本类已经有的方法实现。所以先做一层class_addMethod保证了本类本身有原方法的实现。
  4. 如果本类没有相应的原方法实现,class_addMethod会成功添加一个原方法,实现IMP设置为新的实现。然后通过class_replaceMethod在新方法名义下设置原方法的实现。
  5. 如果本类有相应的原方法实现,method_exchangeImplementations交换两个方法的实现。

通常我们方法混写是想要在应用程序的整个生命周期中有效,所以把method swizzling的代码放在+load的dispatch once中,是为了保证它的执行是线程安全的,并且只执行一次。了解更多关于+load

然后我们来调用一下混写后的方法:

 UIControl *ctrl = [[UIControl alloc] init];
 ctrl.tag = 10;
    
 UIView *view = [[UIView alloc] init];
 view.tag = 100;

控制台输出:

-[UIControl(XXXExtension) xxx_setTag:]  tag=10

从输出中,可以知道新方法只应用于UIControl的对象,并不影响其父类UIView的对象。这就因为我们只交换了UIControl类的方法,明显UIControl本身没有-setTag:方法,所以会通过class_addMethod添加一个,所以不影响其父类UIView。

Method Swizzling 注意事项

- Method swizzling is not atomic

很明显方法混写的代码要完整的执行,程序才会正常执行,正如我们在+load方法中执行dispatch once。

- Changes behavior of un-owned code

混写的方法不止对一个实例有效,是对目标类的所有实例。我们改变了目标类,所以swizzling是很重要的事,要十分小心。

- Possible naming conflicts

命名冲突贯穿整个Cocoa的问题,我们常常在类名和类别方法名前加上前缀,所以我们也在新方法的前面加前缀,就像前面代码里的-xxx_setTag:。但如果-xxx_setTag:在别处也定义了怎么办?这个问题不仅仅存在于swizzling,这我们可以用别的变通的方法:

直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。

static void (*gOriginalSetTagIMP)(id self, SEL _cmd, NSInteger tag);

static void xxxSetTag(id self, SEL _cmd, NSInteger tag) {
    // do custom work
    NSLog(@"%s  tag=%ld", __FUNCTION__, tag);
    gOriginalSetTagIMP(self, _cmd, tag);
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{    
     
        Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
        gOriginalSetTagIMP = (void *)method_getImplementation(originalMethod);
        
        if(!class_addMethod(self, @selector(setTag:), (IMP)xxxSetTag, method_getTypeEncoding(originalMethod))) {
            method_setImplementation(originalMethod, (IMP)xxxSetTag);
        }
    });
}

注意这里的自定义实现xxxSetTag里不能再调用xxxSetTag本身了,不然就真的递归了。因为这里调用的是函数,直接调用,不走runtime的消息传递了。

或者用OC提供的imp_implementationWithBlock,直接用block生成对应的实现IMP:

Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
IMP originalImp = method_getImplementation(originalMethod);
SEL originalSel = method_getName(originalMethod);
    
IMP swizzledImp = imp_implementationWithBlock(^(id target, NSInteger tag){
    NSLog(@"%s   tag=%ld", __FUNCTION__, tag);
    void (*func)(id, SEL, NSInteger) = (void(*)(id, SEL, NSInteger))originalImp;
    func(target, originalSel, tag);
});
    
if(!class_addMethod(self, @selector(setTag:), (IMP)swizzledImp, method_getTypeEncoding(originalMethod))) {
    method_setImplementation(originalMethod, (IMP)swizzledImp);
}

- Swizzling changes the method's arguments

method swizzling 后的方法,想正常调用的话,将是个问题。

比如如果想直接调用xxx_setTag:方法:

[self xxx_setTag:12];

runtime 的做法是:

objc_msgSend(self, @selector(xxx_setTag:), 12);  

runtime去寻找xxx_setTag:的方法实现, _cmd参数为xxx_setTag: ,但是事实上runtime找到的方法实现是原始的setTag:的。

解决方法:使用全局的函数指针IMP。

- The order of swizzles matters

多个swizzle方法的执行顺序也需要注意。那么应该是什么顺序呢,从父类->子类的顺序交换,还是从父类->子类的顺序?

举个例子,在UIView、UIControl、UIButton的分类里面分别自定义如下代码,准备与原来的-setTag:方法进行交换:

//  UIView+LYHExtension.m
- (void)viewSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self viewSetTag:tag];
}

//  UIControl+LYHExtension.m
- (void)controlSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self controlSetTag:tag];
}

//  UIButton+LYHExtension.m
- (void)buttonSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self buttonSetTag:tag];
}

从父类->子类的顺序进行method swizzle:

[UIView swizzle:@selector(setTag:) withSelector:@selector(viewSetTag:)];
[UIControl swizzle:@selector(setTag:) withSelector:@selector(controlSetTag:)];
[UIButton swizzle:@selector(setTag:) withSelector:@selector(buttonSetTag:)];

这里新写了一个方法-swizzle: withSelector:,实现跟上面+load方法里的方法交换一样。

交换前:

交换前.png

按父类->子类的顺序交换后:

交换后_父类子类顺序.png

创建一个UIButton的实例,然后调用一下-setTag:,因为在自定义的方法里的实现是用NSLog打印当前方法信息,以及调用原来的方法。那就看一下控制台:

-[UIButton(XXXExtension) buttonSetTag:]   cmd=setTag:  tag=100
-[UIControl(XXXExtension) controlSetTag:]   cmd=buttonSetTag:  tag=100
-[UIView(XXXExtension) viewSetTag:]   cmd=controlSetTag:  tag=100

用图表示更清晰,按父类->子类的顺序交换后,调用子类的-setTag:方法:

交换后_父类子类顺序_调用.png

很明显,这种从父类->子类的交换顺序,能正常实现我们想要的流程,也就是子类中拿到的方法,是父类已经swizzle后的代码。

反过来,子类->父类的顺序进行method swizzle:

按子类->父类的顺序交换后:

交换后_子类父类顺序.png

很明显,每层的子类(UIButton、UIControl)都是直接跟拥有原始方法的父类(UIView)直接进行交换,不管是否跨层级。

依然创建一个UIButton的实例,调用一下-setTag:,控制台:

-[UIButton(XXXExtension) buttonSetTag:]   cmd=setTag:  tag=100

控制台只打印了UIButton的自定义方法,没有继承父类UIControl和祖父类UIView的自定义方法。

按子类->父类的顺序交换后,调用子类的-setTag:方法:

交换后_子类父类顺序_调用.png

多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。

- Difficult to understand (looks recursive)

新方法的实现看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂。

解决方法:使用全局的函数指针IMP。

- Difficult to debug

使用NSStringFromSelector(_cmd)打印出的方法名调用的方法名,__FUNCTION__打印出的方法名是真正实现的方法名。这块可能会混乱,毕竟交换了方法,就像A的实现是B,B的实现是A,不是平常看到的代码那么直接,这块需要思考,一不小心就会忘了。

解决方法:充分的文档,即使只有你一个人开发。

上一篇下一篇

猜你喜欢

热点阅读