美文网首页
iOS Method Swizzling中幺蛾子

iOS Method Swizzling中幺蛾子

作者: 言霏 | 来源:发表于2020-06-15 16:14 被阅读0次

    1.

    父类有方法A,子类没有重写方法A,在子类中进行方法A的交换,极有可能有幺蛾子。例:

    .h 中
    @interface Person : NSObject
    
    - (void)run;
    
    @end
    
    @interface Man : Person
    
    @end
    
    @interface Man(Swizzling)
    
    @end
    
    .m 中
    @implementation Person
    
    - (void)run {
        NSLog(@"Person run");
    }
    
    @end
    
    @implementation Man
    
    @end
    
    @implementation Man(Swizzling)
    
    + (void)load {
        SEL origSel = @selector(run);
        SEL swizzlingSel = @selector(swizzlingRun);
        Method fromMethod = class_getInstanceMethod([self class], origSel);
        Method toMethod = class_getInstanceMethod([self class], swizzlingSel);
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    - (void)swizzlingRun {
        
        NSLog(@"swizzlingRun");
        [self swizzlingRun];
    }
    
    @end
    

    调用:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Man *m = [[Man alloc] init];
        [m run];
        
        Person *p = [[Person alloc] init];
        [p run];
    }
    
    

    打印:

    2020-06-15 11:28:14.267950+0800 Demo[44980:8856574] swizzlingRun
    2020-06-15 11:28:14.268137+0800 Demo[44980:8856574] Person run
    2020-06-15 11:28:14.268365+0800 Demo[44980:8856574] swizzlingRun
    2020-06-15 11:28:14.268543+0800 Demo[44980:8856574] -[Person swizzlingRun]: unrecognized selector sent to instance 0x6000014881b0
    2020-06-15 11:28:14.273330+0800 Demo[44980:8856574] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person swizzlingRun]: unrecognized selector sent to instance 0x6000014881b0'
    

    可以看到,子类Man调用run还是没问题的,但是Person就直接crash了。因为

    Method fromMethod = class_getInstanceMethod([self class], origSel);
    

    子类没有实现run方法,fromMethod获取到的是父类的run IMP。也就是父类的runswizzlingRun交换了。

    [m run]会去父类中找run的IMP,由于已经交换,会调用到swizzlingRun,在swizzlingRun中调用[self swizzlingRun];,因为子类确实有swizzlingRun 的 SEL,又因为已经交互所以会调用到父类的run。也就会出现打印的情况。
    [p run]由于已经交换,会调用到swizzlingRun,在swizzlingRun中调用[self swizzlingRun];,因为父类没有有swizzlingRun 的 SEL,所以就直接报unrecognizedcrash掉了。

    2.

    对上述例1优化:

    .h 中
    @interface Person : NSObject
    
    - (void)run;
    
    @end
    
    @interface Man : Person
    
    @end
    
    @interface Man(Swizzling)
    
    @end
    
    .m 中
    @implementation Person
    
    - (void)run {
        NSLog(@"Person run");
    }
    
    @end
    
    @implementation Man
    
    @end
    
    @implementation Man(Swizzling)
    
    + (void)load {
        SEL origSel = @selector(run);
        SEL swizzlingSel = @selector(swizzlingRun);
        Method fromMethod = class_getInstanceMethod([self class], origSel);
        Method toMethod = class_getInstanceMethod([self class], swizzlingSel);
        /*
         向本类中添加方法(原始方法的SEL,交换后方法的IMP),如果didAddMethod为YES,
         说明本类没有重写继承来的原方法,此时fromMethod为父类中的Method,再将swizzlingSel的IMP指向fromMethod的IMP。
         */
        BOOL didAddMethod = class_addMethod([self class], origSel, method_getImplementation(toMethod), method_getTypeEncoding(toMethod));
        if (didAddMethod) {
            class_replaceMethod([self class], swizzlingSel, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
        } else {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    }
    
    - (void)swizzlingRun {
        
        NSLog(@"swizzlingRun");
        [self swizzlingRun];
    }
    
    @end
    

    调用:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Man *m = [[Man alloc] init];
        [m run];
    
        Person *p = [[Person alloc] init];
        [p run];
    }
    

    输出:

    2020-06-15 14:27:24.386779+0800 Demo[49010:8964879] swizzlingRun
    2020-06-15 14:27:24.387890+0800 Demo[49010:8964879] Person run
    2020-06-15 14:27:24.387973+0800 Demo[49010:8964879] Person run
    

    Man中本没有run的实现,但是向Man中添加了(run SEL:swizzlingRun IMP),所以[m run]会调用到swizzlingRun,输出NSLog(@"swizzlingRun");又因为把swizzlingRun的IMP替换到了父类的run,所以[self swizzlingRun];会调到父类run

    如果Man中重写了run

    @implementation Man
    
    - (void)run {
        [super run];
        NSLog(@"Man run");
    }
    @end
    

    同样的调用方式输出:

    2020-06-15 14:58:41.952407+0800 Demo[49749:8985021] swizzlingRun
    2020-06-15 14:58:41.952529+0800 Demo[49749:8985021] Person run
    2020-06-15 14:58:41.952599+0800 Demo[49749:8985021] Man run
    2020-06-15 14:58:41.952679+0800 Demo[49749:8985021] Person run
    

    这样可以解决crash,且同时完成hook。
    当然这只能hook Man以及Man子类中的run,不能hook到父类Person的方法。
    如果Man的子类重写了run却没有调用父类方法,那就hook不到子类的run了。
    有时只想hook Man中的run,不想hook其子类的run(调用父类run)是做不到的,子类一旦调用了父类方法就一定会被影响(这一点Aspects很好,可以只hook单个实例的方法)。
    所以一般hook的方法存在继承关系比如viewDidLoad就直接在UIViewController的分类中进行hook。

    相关文章

      网友评论

          本文标题:iOS Method Swizzling中幺蛾子

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