姬長信(Redy)

Method Swizzling的各种姿势-ios学习从入门到精通尽在姬长信

分享最热门的资讯

因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章Method Swizzling, 我们就先以这篇文章中的示例开始说起吧:

#import @implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

简要说明一下以上代码的几个重点:

其实以上的代码也可以简写为以下:


+ (void)load {
    Class class = [self class];
    
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {
        return;
    }
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    // 这儿的先后顺序是有讲究的,如果先执行后一句,那么在执行完瞬间方法被调用容易引发死循环
    class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
}

这是因为class_replaceMethod方法其实能够覆盖到class_addMethodmethod_setImplementation两种场景, 对于第一个class_replaceMethod来说, 如果viewWillAppear:实现在父类, 则执行class_addMethod, 否则就执行method_setImplementation将原方法的IMP指定新的代码块; 而第二个class_replaceMethod完成的工作便只是将新方法的IMP指向原来的代码.

但此处需要特别注意交换的顺序,应该优化把新的方法指定原IMP,再修改原有的方法的IMP.


除了以上的场景之外,其它场景下我们如何使用Method Swizzling呢?


1.在不同类之间实现Method Swizzling

上面示例是通过Category来新增一个方法然后实现Method Swizzling的, 但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明), 此时我们应该如何来做Method Swizzling呢?

例如已知一个className为Car的类中有一个实例方法- (void)run:(double)speed, 目前需要Hook该方法对速度小于120才执行run的代码, 按照方法交换的流程, 代码应该是这样的:


#import @interface MyCar : NSObject
@end

@implementation MyCar

+ (void)load {
    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    // 向Car类中新添加一个xxx_run:的方法
    BOOL registerMethod = class_addMethod(originalClass,
                                          swizzledSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));
    if (!registerMethod) {
        return;
    }
    
    // 需要更新swizzledMethod变量,获取当前Car类中xxx_run:的Method指针
    swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
    if (!swizzledMethod) {
        return;
    }
    
    // 后续流程与之前的一致
    BOOL didAddMethod = class_addMethod(originalClass,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(originalClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)xxx_run:(double)speed {
    if (speed 

与之前的流程相比,在前面添加了两个逻辑:

以上所有的逻辑也可以合并简化为以下:

+ (void)load {
    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
        
    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}


简化后的代码便与之前使用Category的方式并没有什么差异, 这样代码就很容易覆盖到这两种场景了, 但我们需要明确此时class_replaceMethod所完成的工作却是不一样的.

2.如何实现类方法的Method Swizzling

以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:


@interface NSDictionary (Test)
@end

@implementation NSDictionary (Test)

+ (void)load {
    Class cls = [self class];
    SEL originalSelector = @selector(dictionary);
    SEL swizzledSelector = @selector(xxx_dictionary);
    
    // 使用class_getClassMethod来获取类方法的Method
    Method originalMethod = class_getClassMethod(cls, originalSelector);
    Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {
        return;
    }
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    // 类方法添加,需要将方法添加到MetaClass中
    Class metaClass = objc_getMetaClass(class_getName(cls));
    class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
}

+ (id)xxx_dictionary {
    id result = [self xxx_dictionary];
    return result;
}

@end

相比实例方法的Method Swizzling,流程有两点差异:

3.在类簇中如何实现Method Swizzling

在上面的代码中我们实现了对NSDictionary中的+ (id)dictionary方法的交换,但如果我们用类似代码尝试对- (id)objectForKey:(id)key方法进行交换后, 你便会发现这似乎并没有什么用.

这是为什么呢? 平常我们在Xcode调试时,在下方Debug区域左侧的Variables View中,常常会发现如__NSArrayI或是__NSCFConstantString这样的Class类型, 这便是在Foundation框架中被广泛使用的类簇, 详情请参看Apple文档class cluster的内容.

所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling, 在上面介绍的不同类之间做Method Swizzling便已经能解决该问题, 下面一个简单的示例通过交换NSMutableDictionarysetObject:forKey:方法,让调用这个方法时当参数object或key为空的不会抛出异常:

@interface MySafeDictionary : NSObject
@end

@implementation MySafeDictionary

+ (void)load {
    Class originalClass = NSClassFromString(@"__NSDictionaryM");
    Class swizzledClass = [self class];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL swizzledSelector = @selector(safe_setObject:forKey:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {
        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}

@end

4.在Method Swizzling之后如何恢复

使用了Method Swizzling的各种姿势之后, 是否有考虑如何恢复到交换之前的现场呢?

一种方案就是通过一个开关标识符, 如果需要从逻辑上面恢复到交换之前, 就设置一下这个标识符, 在实现中判定如果设定了该标识符, 逻辑就直接调用原方法的实现, 其它什么事儿也不干, 这是目前大多数代码的实现方法, 当然也是非常安全的方式, 只不过当交换方法过多时, 每一个交换的方法体中都需要增加这样的逻辑, 并且也需要维护大量这些标识符变量, 只是会觉得不够优雅, 所以此处也就不展开详细讨论了.

那下面来讨论一下有没有更好的方案, 以上描述的Method Swizzling各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:

那我们来分别看一下这两件事情是否都还能恢复:

#import @interface MySafeDictionary : NSObject
@end

static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;

@implementation MySafeDictionary

+ (void)swizzlling {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        kMySafeLock = [[NSLock alloc] init];
    });
    
    [kMySafeLock lock];
    
    do {
        if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Class swizzledClass = [self class];
        SEL originalSelector = @selector(setObject:forKey:);
        SEL swizzledSelector = @selector(safe_setObject:forKey:);
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
        if (!originalMethod || !swizzledMethod) break;
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char *originalType = method_getTypeEncoding(originalMethod);
        const char *swizzledType = method_getTypeEncoding(swizzledMethod);
        
        kMySafeOriginalIMP = originalIMP;
        kMySafeSwizzledIMP = swizzledIMP;
        
        class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
        class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
    } while (NO);
    
    [kMySafeLock unlock];
}

+ (void)restore {
    [kMySafeLock lock];
    
    do {
        if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Method originalMethod = NULL;
        Method swizzledMethod = NULL;
        unsigned int outCount = 0;
        Method *methodList = class_copyMethodList(originalClass, &outCount);
        for (unsigned int idx=0; idx 

注意 这段代码的Method Swizzling和恢复都需要主动调用, 并且相比上面其它的示例, 这段代码还添加如锁机制来加之保护. 这个示例是以不同的类来实现的Method Swizzling和恢复, 如果是Category或者是类方法, 根据之前的示例也需要做相应的调整.


ios学习从入门到精通尽在姬长信