美文网首页iOS专题资源__系统知识点iOS技术资料iOS进阶之runtime
12- Runtime基础使用场景-拦截替换方法(class_a

12- Runtime基础使用场景-拦截替换方法(class_a

作者: 春田花花幼儿园 | 来源:发表于2016-05-21 21:37 被阅读7035次

前话

这几天在系统的学习 runtime,在学习 runtime 的基础使用案例中,"方法替换"这种使用情况下,发现有两种写法. 其实也不是两种写法,准确的来说一种是比较严谨的,另一种则没有那么严谨.

发现这两种写法的差异后,我主要集中在下列:

  • class_addMethod
  • class_replaceMethod
  • method_exchangeImplementations

哪个方法的具体作用.

下面,这篇文章就这两种写法和上述三种方法的区别.

第一种写法

《OC最实用的runtime总结,面试、工作你看我就足够了!》的时候,它里边的写法是简单的获取到被替换和替换方法的Method.然后直接使用method_exchangeImplementations进行方法的替换. 最开始使用的时候,因为测试范例比较简单,所以并没有发现这样写的弊端.但是确实能够实现方法替换的效果. 代码如下:

+(void)load{
    //获取两个类的方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(ll_imageName:));
    //开始交换方法实现
    method_exchangeImplementations(m1, m2);
}

在后来看到《runtime详解》的时候,发现作者的写法并不是这样,虽然作者添加少量注释,但是愚钝的我还没有想清楚,这也是这篇文章的初衷,也是下一小结的由来.

第二种写法

上一节的这种情况虽然能够实现我们想要的效果.但是我们有没有想过这种情况:

" 周全起见,有两种情况要考虑一下。第一种情况是要复写的方法(overridden)并没有在目标类中实现(notimplemented),而是在其父类中实现了。第二种情况是这个方法已经存在于目标类中(does existin the class itself)。这两种情况要区别对待。 (译注: 这个地方有点要明确一下,它的目的是为了使用一个重写的方法替换掉原来的方法。但重写的方法可能是在父类中重写的,也可能是在子类中重写的。) 对于第一种情况,应当先在目标类增加一个新的实现方法(override),然后将复写的方法替换为原先(的实现(original one)。 对于第二情况(在目标类重写的方法)。这时可以通过method_exchangeImplementations来完成交换."

---- 以上来自:《Objective-C的方法替换》

+(void)load{
    NSString *className = NSStringFromClass(self.class);
    NSLog(@"classname %@", className);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //要特别注意你替换的方法到底是哪个性质的方法
        // When swizzling a Instance method, use the following:
        //        Class class = [self class];

        // When swizzling a class method, use the following:
        Class class = object_getClass((id)self);

        SEL originalSelector = @selector(systemMethod_PrintLog);
        SEL swizzledSelector = @selector(ll_imageName);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

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

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

解析:

上面提到的:

dispatch_once这里不是“单例”,是保证方法替换只执行一次.

说明:

systemMethod_PrintLog:被替换方法ll_imageName:替换方法

class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现

1.如果返回成功:则说明被替换方法没有存在.也就是被替换的方法没有被实现,我们需要先把这个方法实现,然后再执行我们想要的效果,用我们自定义的方法去替换被替换的方法. 这里使用到的是class_replaceMethod这个方法. class_replaceMethod本身会尝试调用class_addMethodmethod_setImplementation,所以直接调用class_replaceMethod就可以了)

2.如果返回失败:则说明被替换方法已经存在.直接将两个方法的实现交换即

另外:

  • 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP
  • 我们可以利用 class_replaceMethod 来修改类
  • 我们可以利用 method_setImplementation 来直接设置某个方法的IMP

其实我们如果 研究过 AFN 代码的话,会发现, AFN 就是第二种写法.在AFURLSessionManager.m的第296行:

static inline void af_swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    if (class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }

}

详尽的代码请查看 Demo.

下载地址

具体的 Demo 代码可以在我的 GitHub 上找到 Demo地址

其它

关于 load 的调用次数问题,大家可以查看这两篇文章.+(void)load和+(void)initialize可当做普通类方法(Class Method)调用的.《NSObject的load和initialize方法!》《Objective C类方法load和initialize的区别》

参考文章

  1. 《OC最实用的runtime总结,面试、工作你看我就足够了!》
  2. 《Objective-C的方法替换》

交流

希望能和大家交流技术

我的博客地址: http://www.lilongcnc.cc/


相关文章

网友评论

  • zhangPeng丶:class_addMethod:如果发现方法已经存在,会失败返回,这个应该是方法已经实现吧?对不?
    凤栖林:理解了,这个方法不一定存在,如
    void dynamicMethodIMP(id sel,SEL _cmdd){
    NSLog(@"resolveInstanceMethod调用成功");
    }

    + (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @Selector(method)) {
    // class_addMethod([self class],sel,(IMP));
    class_addMethod([self class],sel,(IMP)dynamicMethodIMP, "v@:");
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }
    凤栖林:是啊,originalSelector应该是有对应的imp指针的,所以if是永远不会走
  • Latte_Bear:楼主,对于你这一段“第一种情况是要复写的方法(overridden)并没有在目标类中实现(notimplemented),而是在其父类中实现了。第二种情况是这个方法已经存在于目标类中(does existin the class itself)。这两种情况要区别对待。 ”中提到的父类和目标类指的是什么?
  • Pusswzy:十分感谢
  • wsxiaoluob:AFN的3.0版本是第一种写法,没找到你说的第二种写法的pull request
    wsxiaoluob:@春田花花幼儿园 https://github.com/AFNetworking/AFNetworking/pull/2702 2年前的这个pull把class_addMethod的判断去掉了,原因我还在看,你也可以一起看看
    春田花花幼儿园:@wsxiaoluob 具体替换到3.0改变了没有,我这边没有查看,你可以比对一下
    春田花花幼儿园:@wsxiaoluob 不是基于3.0的那段代码
  • 1c780c09e324:博主,有个地方想不通请教一下
    当class_addMethod为该类添加一个方法的实现,该实现是swizz方法的实现
    然后调用class_replaceMethod替换swizz方法的实现为original方法的实现,
    问题:如果子类没有重写,那么originalMethod拿到的是父类的方法实现,调用class_replaceMethod不是把swizz方法的实现替换了父类的实现,那跟调用直接method_exchangeImplementations有什么区别吗? 感谢,望指教
    atom2ueki:gist sample:
    https://gist.github.com/atom2ueki/79dbf4270b39beda67ae3774a6e99661
    atom2ueki:如果subclass没有override(重名或者重写,在swift里面重复名字不算override) parent class的func,确实没什么区别,原因是`METHOD`这个struct只看名字不看param,所以在class对象是subclass的时候,如果发现original SEL在subclass和superclass有一样名字的func,`class_getInstanceMethod`拿到的是subclass override的`METHOD`, 所以subclass override了,直接exchange,换的是subclass的俩method,不影响parent的,如果subclass没有override,class_getInstanceMethod 会去拿parent class的method,那么 `class_addMethod ` 会success, `class_addMethod` 做了什么事情呢?就是把original SEL的IMP换成了swizzle的IMP,然后`class_replaceMethod`又把swizzle SEL的IMP用原本original的IMP替换了,其实就是两个步骤做了exchange的事情,但是因为有override的特殊情况,就这样写了
    6a34cf1c060b:在子类没有重写的情况下,作者文章中调用
    class_replaceMethod(class,
    swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod))
    应该是拿original对应父类方法的实现替换swizzle方法的实现,也就是说class_replaceMethod方法仅仅是替换掉了swizzle方法的实现,但没替换original对应父类方法的实现;而method_exchangeImplementations是交换了swizzle方法和original方法的实现。
    另外补充下,就像上面分析的,class_replaceMethod本身会尝试调用class_addMethod和method_setImplementation,如果swizzledSelector没有方法实现则调用class_addMethod,有实现则调用method_setImplementation覆盖掉原来的实现。
  • 无敌毛毛: // When swizzling a class method, use the following:
    Class class = object_getClass((id)self);

    SEL originalSelector = @selector(systemMethod_PrintLog);
    SEL swizzledSelector = @selector(ll_imageName);

    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    博主你好,看你的示例代码里边,交换的是类方法(Class class = object_getClass((id)self);),是不是应该使用class_getClassMethod这个函数?
    水清_木秀:Method class_getClassMethod(Class cls , SEL name) 是获取某个类的类方法。Method class_getInstanceMethod(Class cls , SEL name) 是获取某个累的实力方法。object_getClass 是获取每个类的class
  • ifelseboyxx:博主 我这种写法为什么没有交换成功啊?

    - (void)viewDidLoad {
    [super viewDidLoad];

    [ViewController go];
    [ViewController stop];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    SEL s1 = @selector(go);
    SEL s2 = @selector(stop);
    Class class = [self class];
    Method m1 = class_getClassMethod(class, s1);
    Method m2 = class_getClassMethod(class, s2);
    // Method m1 = class_getInstanceMethod(self.class, s1);
    // Method m2 = class_getInstanceMethod(self.class, s2);
    BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
    if (success){
    class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
    }else{
    method_exchangeImplementations(m1, m2);
    }
    });
    NSLog(@"--------------------------------------------");
    [ViewController go];
    [ViewController stop];
    }

    + (void)go {
    NSLog(@"Go!");
    }

    + (void)stop {
    NSLog(@"Stop!");
    }


    打印出来:

    2017-01-22 17:43:00.012 RuntimeDemo[3216:1376638] Go!
    2017-01-22 17:43:00.012 RuntimeDemo[3216:1376638] Stop!
    2017-01-22 17:43:00.012 RuntimeDemo[3216:1376638] --------------------------------------------
    2017-01-22 17:43:00.012 RuntimeDemo[3216:1376638] Go!
    2017-01-22 17:43:00.012 RuntimeDemo[3216:1376638] Stop!
    Jamesholy:@ifelseboyxx
    给类添加类方法,要在类的元类上添加。下面这样写就可以。因为类也是对象,它的类方法是在它的元类上的。
    Class metaClass = object_getClass(class);// <------修改
    BOOL didAddMethod = class_addMethod(metaClass,origSel,
    method_getImplementation(altMethod),
    method_getTypeEncoding(altMethod));

    if (didAddMethod) {
    class_replaceMethod(metaClass,altSel,
    method_getImplementation(origMethod),
    method_getTypeEncoding(origMethod));
    春田花花幼儿园:@_littleboy
    我简单看了下,你的代码加上:- (void)go {
    NSLog(@" - Go!");
    }

    - (void)stop {
    NSLog(@"- Stop!");
    }

    类方法就可以替换了. 具提原因我显现在也不是很清楚.之前测试还真没有遇到这个问题.
    初步猜测,可能可能是判断的方法有问题.
    ifelseboyxx:而我去掉判断 直接 method_exchangeImplementations(m1, m2); 是可以交换成功的!
    打印:

    2017-01-22 17:44:20.051 RuntimeDemo[3238:1384694] Go!
    2017-01-22 17:44:20.051 RuntimeDemo[3238:1384694] Stop!
    2017-01-22 17:44:20.052 RuntimeDemo[3238:1384694] --------------------------------------------
    2017-01-22 17:44:20.052 RuntimeDemo[3238:1384694] Stop!
    2017-01-22 17:44:20.052 RuntimeDemo[3238:1384694] Go!
  • b8e6682d009f:我看现在的库里都是用exchange了直接,afnetworking也是
    Pusswzy:@FreeDani :+1:
    FreeDani:AFURLSessionManager.m exchange那里用了af_swizzleSelector(theClass, oriSelector, swiSelector)进行封装,在调用af_swizzleSelector时仍然先判断了af_addMethod
  • b523aa780226:不错 浅显易懂 一目了然
  • 皮特儿:工程里面有2个模块都对imageNamed进行了hook,怎么办?
    水清_木秀:取用不同方法名
  • 闭眼_聆听世界:class_addMethod 已经替换好了, 为什么还要class_replaceMethod
    闭眼_聆听世界:@春田花花幼儿园 add 之后实现了replace 可以在你自定义方法里 调用自己, 实现[super ...],和Exchange 差不多
    闭眼_聆听世界:@春田花花幼儿园 class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
    1. 子类方法里没有这个方法,
    2. 分类里写了这个方法的实现,
    3. add, 增加方法成功, 上面的写法, 已经重写, originalSelector的名字, swizzledMethod的实现和类型, Demo试了一下
    春田花花幼儿园:@闭眼_聆听世界 "class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现". class_addMethod 是判断是我们要替换的和被替换的两个方法我们有没有写出实现. 应该不具备替换的功能
  • Double_kay:dispatch_once有点多余了吧,load方法不是只会调用一次吗?
    cb2bcb507166:防止手动调用load方法吧
    春田花花幼儿园:@奔跑的蚂蚁00 load 方法是可以当做类方法主动调用的,为的是避免这种情况. 你看看http://www.cnblogs.com/ider/archive/2012/09/29/objective_c_load_vs_initialize.html

本文标题:12- Runtime基础使用场景-拦截替换方法(class_a

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