iOS開發(fā)- runtime方法交換的坑

class_replaceMethod與method_exchangeImplementations區(qū)別

方法交換在開發(fā)中還是挺常見的,比如hook調(diào)viewDidLoad方法,想在每個(gè)viewDidLoad里面打印出當(dāng)前類名,可以寫個(gè)jm_ viewDidLoad方法,在用runtime交換倆方法的實(shí)現(xiàn)(也叫IMP)。


viewDidLoad方法交換示意圖->網(wǎng)上找的

看不少開源庫(kù)都用到方法交換,基本有倆種實(shí)現(xiàn)方式:
第一種實(shí)現(xiàn):

void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
  Method originalMethod = class_getInstanceMethod(cls, origSelector);
  Method swizzledMethod = class_getInstanceMethod(cls, newSelector);

  IMP previousIMP = class_replaceMethod(cls, origSelector, method_getImplementation(swizzledMethod),
                                                 method_getTypeEncoding(swizzledMethod));
  class_replaceMethod(cls, newSelector, previousIMP,method_getTypeEncoding(originalMethod));
}

第二種實(shí)現(xiàn):

void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
  Method originalMethod = class_getInstanceMethod(cls, origSelector);
  Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
  method_exchangeImplementations(originalMethod, swizzledMethod);
}

在某些情況下,這倆種確實(shí)都能達(dá)到交換IMP的效果,但是其中又有一絲區(qū)別。

看看class_replaceMethod的文檔解釋:

方法:IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char * types)

  • Replaces the implementation of a method for a given class.
  • @param cls The class you want to modify.
  • @param name A selector that identifies the method whose implementation you want to replace.
  • @param imp The new implementation for the method identified by name for the class identified by cls.
  • @param types An array of characters that describe the types of the arguments to the method.
  • Since the function must take at least two arguments-self and _cmd, the second and third characters
  • must be “@:” (the first character is the return type).
  • @return The previous implementation of the method identified by name for the class identified by \e cls.
  • @note This function behaves in two different ways:
  • If the method identified by name does not yet exist, it is added as if class_addMethod were called.
  • If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called.
    重點(diǎn)是最后note的描述:當(dāng)name描述的方法不存在的時(shí)候,將調(diào)用class_addMethod方法;當(dāng)這個(gè)方法存在,將調(diào)用method_setImplementation方法。

再看看method_exchangeImplementations的文檔解釋:

方法:void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

  • @param m1 Method to exchange with second method.
  • @param m2 Method to exchange with first method.
  • @note This is an atomic version of the following:
  • IMP imp1 = method_getImplementation(m1);
  • IMP imp2 = method_getImplementation(m2);
  • method_setImplementation(m1, imp2);
  • method_setImplementation(m2, imp1);
    可以看出這個(gè)方法完全是個(gè)二愣子,直接交換IMP,啥都不管。
倆中方法都用到了class_getInstanceMethod(cls, selector)方法,這個(gè)方法有個(gè)特點(diǎn):如果這個(gè)類中沒有實(shí)現(xiàn)selector這個(gè)方法,它返回的是它某父類的 Method 對(duì)象(沿著繼承鏈找到為止)。

重點(diǎn)來了:
如果這個(gè)類沒實(shí)現(xiàn)這個(gè)方法,但是它父類實(shí)現(xiàn)了,直接拿這方法來交換,真沒問題么??
先說結(jié)果:第二種實(shí)現(xiàn)有問題,第一種實(shí)現(xiàn)沒問題。

看代碼上代碼:

@interface NSObject(test)
@end

@implementation NSObject(test)

- (void)oneSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{    Class cls = [self class];
    Method originalMethod = class_getInstanceMethod(cls, origSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
    
    IMP previousIMP = class_replaceMethod(cls,
                                          origSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));
    
    class_replaceMethod(cls,
                        newSelector,
                        previousIMP,
                        method_getTypeEncoding(originalMethod));
}
- (void)twoSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Class cls = [self class];
    Method originalMethod = class_getInstanceMethod(cls, origSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

@end
@interface Base : NSObject
@end

@implementation Base
- (void)print:(NSString*)msg
{
    NSLog(@"print-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
- (void)hookPrint:(NSString*)msg {
    NSLog(@"hookPrin-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
@end

//A只實(shí)現(xiàn)了print方法,沒有實(shí)現(xiàn)hookPrint方法。
@interface A : Base
@end
@implementation A
- (void)print:(NSString*)msg {
    NSLog(@"A obj print say:%@", msg);
}
@end

//B啥都沒實(shí)現(xiàn)。
@interface B : Base
@end
@implementation B
@end

現(xiàn)在我們測(cè)試第一種實(shí)現(xiàn):分別交換A、B的print和hookPrint的方法實(shí)現(xiàn)。

[A oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
[B oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//測(cè)試代碼是這樣的:
 A* a = [A new]; [a print:@"hello1"];
 B* b = [B new]; [b print:@"hello2"];
//結(jié)果:
2019-07-02 14:55:19.294580+0800 xctest[3533:667117] hookPrin-->obj A print say:hello1
2019-07-02 14:55:19.294871+0800 xctest[3533:667117] hookPrin-->obj B print say:hello2

說明A、B確實(shí)交換了方法實(shí)現(xiàn)。

現(xiàn)在我們測(cè)試第二種實(shí)現(xiàn):分別交換A、B的print和hookPrint的方法實(shí)現(xiàn)。

 [A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//測(cè)試代碼是這樣的:
 A* a = [A new]; [a print:@"hello1"];
 B* b = [B new]; [b print:@"hello2"];
//結(jié)果:
2019-07-02 14:58:48.692403+0800 xctest[3585:681651] hookPrin-->obj A print say:hello1
2019-07-02 14:58:48.692697+0800 xctest[3585:681651] A obj print say:hello2

嗯?有沒不對(duì)勁?B的交換方法失敗了。不是期望中hookPrin-->obj B print say:hello2。
why?分析下:
第一種方法,用class_replaceMethod()實(shí)現(xiàn)的。由于A中不存在hookPrint方法,class_replaceMethod會(huì)調(diào)用class_addMethod方法,而class_addMethod會(huì)把Base的hookPrint實(shí)現(xiàn)添加到當(dāng)前類,print的實(shí)現(xiàn)最終會(huì)和hookPrint的實(shí)現(xiàn)交換。B中倆方法都不存在,也會(huì)添加倆方法,最終交換倆方法的實(shí)現(xiàn)。最終打印的時(shí)候,會(huì)調(diào)用Base的hookPrint方法。
如果調(diào)用[a hookPrint:@"hello_k"];那么最終實(shí)現(xiàn)的應(yīng)該是A類中print的實(shí)現(xiàn)。結(jié)果是:A obj print say:hello_k。
第二種方法,用method_exchangeImplementations實(shí)現(xiàn)。由于A沒有實(shí)現(xiàn)hookPrint方法,在調(diào)用
[A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的時(shí)候,將A的print實(shí)現(xiàn)與Base的hookPrint實(shí)現(xiàn)交換了。
接著調(diào)用 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的時(shí)候,由于B類啥都沒實(shí)現(xiàn),它只能將Base的print實(shí)現(xiàn)與base的hookPrint交換了。最終,調(diào)用[b print:@"hello2"]的時(shí)候,調(diào)用的是代碼中A類的print方法的實(shí)現(xiàn)。

巨丑的示意圖

這里加不加class_addMethod的判斷,結(jié)果都一樣??梢宰约涸囋?。

測(cè)試代碼地址:github鏈接
注意:整篇這是沒考慮交換方法不存在的情況下考量的。

結(jié)論:當(dāng)我們寫的類沒有繼承的關(guān)系的時(shí)候,倆種方法都沒什么問題。當(dāng)有繼承關(guān)系又不確定方法實(shí)現(xiàn)沒實(shí)現(xiàn),最好用class_replaceMethod方法。當(dāng)啥都不確定的時(shí)候,老老實(shí)實(shí)地用class_replaceMethod 吧,安全無(wú)痛苦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容