iOS Method Swizzling中幺蛾子

2020-06-15  本文已影响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。

上一篇 下一篇

猜你喜欢

热点阅读