美文网首页
关于Method swizzling的两件事

关于Method swizzling的两件事

作者: Beta是条好狗 | 来源:发表于2018-03-17 22:02 被阅读59次

    1. 关于Method swizzling的两种写法。

    简单实现:

    void simple_swizzle(Class class, SEL original, SEL swizzle) {
        Method originalMethod = class_getInstanceMethod(class, original);
        Method swizzleMethod = class_getInstanceMethod(class, swizzle);
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
    

    需要注意的是,class_getInstanceMethod如果没在当前类中查找到对应方法那么就会在整个继承体系中查找。如果明确知道originalMethod是当前类实现的方法,这种方式可以正常工作。当originalMethod是父类的方法时,swizzle后父类调用此方法会因为找不到子类的实现然后报错。
    如果originalMethod==nil说明没有找到这个方法,会导致swizzle失败。

    最佳实践:

    void best_swizzle(Class class, SEL original, SEL swizzle) {
        Method originalMethod = class_getInstanceMethod(class, original);
        Method swizzleMethod = class_getInstanceMethod(class, swizzle);
        //  尝试向子类添加original方法,对应的方法实现为swizzle后的实现。
        //  didAddMethod == NO时,说明当前类存在originalMethod,处理方式和simple_swizzle一样。
          // didAddMethod == YES时,说明当前类没有实现originalMethod,已成功添加
        BOOL didAddMethod = class_addMethod(class, original, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        if (didAddMethod) {
    //  这里将swizzle方法的实现replace成父类的实现
            class_replaceMethod(class, swizzle, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzleMethod);
        }
    //  这里处理 originalMethod==nil 的情况,说明之前在didAddMethod == YES的分支里repalce失败。在这里直接将swizzleMethod的实现用一个block替换
        if (!originalMethod) {
            method_setImplementation(swizzleMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
                NSLog(@"original method not implemented");
            }));
        }
    }
    

    一般情况下,如果明确需要swizzle的方法是当前类的方法,那么直接用第一种方式即可,这种方式建议用在一些通用的,需要满都高度容错度的场景,比如作为SDK的一部分发布出去的时候。

    2. 关于多次swizzle后所有swizzle的方法是否都会被执行的问题。

    一直有一个困惑,对同一个方法多次methods swizzing之后到底会发生什么,执行的顺序又是怎么样的。趁着周末时间来测试验证一下。

    创建Coder类,提供一个叫做coding的方法。

    @interface Coder : NSObject
    - (void)coding;
    @end
    
    @implementation Coder
    - (void)coding {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
    }
    @end
    

    创建用于swizzle的category,在category的load方法中连续swizzle5次。

    @interface Coder (language)
    @end
    @implementation Coder (language)
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            coder_bestSwizzle(self, @selector(coding), @selector(coding_objc));
            coder_bestSwizzle(self, @selector(coding), @selector(coding_kotlin));
            coder_bestSwizzle(self, @selector(coding), @selector(coding_cpp));
            coder_bestSwizzle(self, @selector(coding), @selector(coding_java));
            coder_bestSwizzle(self, @selector(coding), @selector(coding_swift));
        });
    }
    - (void)coding_objc {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
        [self coding_objc];
    }
    - (void)coding_kotlin {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
        [self coding_kotlin];
    }
    - (void)coding_cpp {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
        [self coding_cpp];
    }
    - (void)coding_java {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
        [self coding_java];
    }
    - (void)coding_swift {
        NSLog(@"\n %s", __PRETTY_FUNCTION__);
        [self coding_swift];
    }
    @end
    

    新建coder对象,调用coding方法

    - (void)coderTest {
        Coder *coder = Coder.new;
        [coder coding];
    }
    

    打印如下:

    2018-03-17 20:17:30.093302+0800 MyOCDemoProject[36165:5131009] 
     -[Coder(objc) coding_swift]
    2018-03-17 20:17:30.093449+0800 MyOCDemoProject[36165:5131009] 
     -[Coder(objc) coding_java]
    2018-03-17 20:17:30.093568+0800 MyOCDemoProject[36165:5131009] 
     -[Coder(objc) coding_cpp]
    2018-03-17 20:17:30.093675+0800 MyOCDemoProject[36165:5131009] 
     -[Coder(objc) coding_kotlin]
    2018-03-17 20:17:30.093805+0800 MyOCDemoProject[36165:5131009] 
     -[Coder(objc) coding_objc]
    2018-03-17 20:17:30.093915+0800 MyOCDemoProject[36165:5131009] 
     -[Coder coding]
    

    如预期的所有方法都hook成功,并且按照FILO(first in last out)的顺序调用了一遍。
    那么这个顺序为什么是这样的呢?看下面的示意图就知道了

    1. 开始时的SEL和IMP的对应关系


      FFC0929C-7071-458A-8998-F57481129BB7.png
    2. swizzle coding_objc & coding之后的对应关系,IMP指针互换

    1. swizzle coding_kotlin & coding之后的对应关系
      因为之前coding的IMP指针已经指向了coding_objc的实现,所以在method_exchangeImplementations时实际上交换的是coding方法的当前IMP指针而不是原始方法实现对应的IMP指针

      384BCB75-6289-4720-97B9-E3B9D833BB2C.png
    2. 后续的swizzle动作也是如此,method_exchangeImplementations实际上互换的对象是前一次swizzle后coding方法对应IMP指针而不是coding方法原始实现的IMP指针。
      最终SEL和IMP对应关系如下图所示,并且[coder coding]的整个执行的路径也在图中用白线标明。

      48DCBC28-73A7-4510-809D-A142072033EA.png

    看到这里也就不难理解为什么最后会形成FILO的调用顺序了。

    需要注意的是:

    1. 正常来说swizzle不会如本文那样集中在同一个category的load方法中。方法的实际执行顺序就要具体类加载的顺序。
    2. 在使用runtime替换系统的方法的实现的目的大多是为了hook系统方法方法,插入自己的实现。如果你的目的是hook,那么务必要记住在你替换的实现里,调用自己一次。否则就会导致上图执行路径中,执行到你的实现时从IMP连到SEL的线断开,系统方法得不到调用。

    参考文章:
    Runtime中Swizzle时你可能没注意到的问题 - 简书
    Objective-C Method Swizzling 的最佳实践

    相关文章

      网友评论

          本文标题:关于Method swizzling的两件事

          本文链接:https://www.haomeiwen.com/subject/gqcsqftx.html