Method Swizzling替换子类中未重写的父类方法实现

当用Method Swizzling替换子类中未重写的父类方法实现时,class_getInstanceMethod会获取到子类没有重写的父类方法的Method,这个Method对应的是父类中的方法,用这个Method调用method_exchangeImplementations实现Method Swizzling之后就会交换父类Method和子类Method的实现。然后在基类对象上调用被交换的方法时,如果该方法调用了自身就会引起unrecognized selector异常。


提问后追加:1. 我知道unrecognized selector异常出现的原因;2.“这种情况”指的是子类没有重写父类的方法,但是代码中又需要针对这个特定子类及其派生类重写方法实现的情况。

具体情景可以参照:需要在UITableViewCell类及其子类上重写NSObject实现的方法setValue:forKeyPath:,而又不影响UITableViewCell的基类的方法调用结果。


问:
在这种情况下,如何“更优美”地达到重写不能修改源码的特定方法的目的呢?

示例代码如下:

#类声明

@interface BaseClass : NSObject

- (void)baseVersionMethod;

@end

@interface SubClass : BaseClass

@end

@interface SubClass (MySubClass)

- (void)myMethod;

@end

#类定义

@implementation BaseClass

- (void)baseVersionMethod {
    NSLog(@"baseVersionMethod has been called.");
}

@end

@implementation SubClass

@end

@implementation SubClass (MySubClass)

- (void)myMethod {
    NSLog(@"myMethod");
    [self myMethod];
}

+ (void)load {
    Method oriM = class_getInstanceMethod(SubClass.class, @selector(baseVersionMethod));
    Method newM = class_getInstanceMethod(SubClass.class, @selector(myMethod));

    method_exchangeImplementations(oriM, newM);
}

@end


#调用交换后的方法

int main(int argc, char * argv[]) {
    BaseClass *obj = [[BaseClass alloc] init];
    [obj baseVersionMethod];//抛出unrecognized selector异常
}
阅读 8.3k
3 个回答

父类没有实现子类的方法,当然会抛异常啊。

可以在父类的类别里实现

@interface BaseClass (Swizzle)

- (void)myMethod;

@end

@implementation BaseClass (Swizzle)

- (void)myMethod {
    NSLog(@"myMethod");
    [self myMethod];
}

+ (void)load {
    Method oriM = class_getInstanceMethod(BaseClass.class, @selector(baseVersionMethod));
    Method newM = class_getInstanceMethod(BaseClass.class, @selector(myMethod));

    method_exchangeImplementations(oriM, newM);
}

@end

Method Swizzling的原理

Method swizzling的原理是改变在方法映射表中SEL与IMP的对应关系,所以,写method应该在改变对应关系上下手。

贴出代码的问题所在

class_getClassMethod这个方法是返回指向方法的指针,而不是方法返回方法的implementation。你应该用method_getImplementation这个方法。

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

另一种实现

SEL selector = @selector(printHello);
IMP selectorImplementation = imp_implementationWithBlock(^{
    NSLog(@"HI");
});
Method transformedValueClassMethod = class_getClassMethod([testClass class], selector);
class_replaceMethod([testClass class], selector, selectorImplementation, method_getTypeEncoding(transformedValueClassMethod));

补充修改===========================================

之前没有理解题主的意思,以为题主是问method swizzling的相关问题,就匆匆写了上面的答案,现在细看题目之后回答:

出现unrecognized selector的原因

题主交换了myMethodbaseVersionMethod的IMP,因此,在调用baseVersionMethod的时候调用的是

- (void)myMethod {
    NSLog(@"myMethod");
    [self myMethod];
}

的implementation。此时的baseVersionMethod是这样的:

- (void)baseVersionMethod
{
    NSLog(@"myMethod");
    [self myMethod];
}

注意,代码块中[self myMethod]一句中在baseClass实例里面调用的话是没有myMethod的selector的,所以报错。unrecognized selector是指myMethod而不是baseVersionMethod。

method swizzling最好用在同一个类中的方法,因为如果跨类替换的话容易出现implementation中的实例变量,或者调用方法和实例内容不符的情况,就像你代码中出现的问题一样。这非常容易导致crash,最惨的是如果没有crash,后面会产生各种稀奇古怪的bug,很难调试。

如何达到重写不修改源码特定方法的目的

这句话的意思是如何在原方法被swizzling之后再去调用原方法,返回结果不变吗?没看明白你这个问题,现在姑且猜测你就是这个意思吧。

在同一个类中:

- (void)swizzlingMethod {
    [self swizzlingMethod];
}
- (void)swizzledMethod {
    NSLog(@"swizzled");
}

虽然swizzling和swizzled方法内容被交换,但是在调用swizzled方法的时候implementation又调了一遍swizzled的implementation,效果和直接调用交换前的swizzled方法等效。

该不该用method swizzling?

method swizzling就像一把快刀,有人觉得这刀太锋利了,容易伤人,但也有人就喜欢用快刀。对于runtime的世界,没有不能触碰的禁区。

贴一篇帖子,供你参考:
What are the Dangers of Method Swizzling in Objective C?

我是题主。

目前我自己想到的一个办法是在exchange之前用class_addMethod方法先为特定类添加一次仅调用super方法的实现,然后再使用exchange。addMethod在类中已有实现的情况下会失败,返回NO。

经人提醒,过于依赖runtime的方法去修改方法实现是一个比较危险的行为,确实是用共同基类的方式来重写方法比较安全。只应当在确实需要的情况使用这种hack技巧。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题