因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章Method Swizzling, 我们就先以这篇文章中的示例开始说起吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
#import <objc/runtime.h>
@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
|
简要说明一下以上代码的几个重点:
- 通过在Category的
+ (void)load
方法中添加Method Swizzling的代码,在类初始加载时自动被调用,load方法按照父类到子类,类自身到Category的顺序被调用.
- 在dispatch_once中执行Method Swizzling是一种防护措施,以保证代码块只会被执行一次并且线程安全,不过此处并不需要,因为当前Category中的load方法并不会被多次调用.
- 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的.
xxx_viewWillAppear:
方法中[self xxx_viewWillAppear:animated];
代码并不会造成死循环,因为Method Swizzling之后, 调用xxx_viewWillAppear:
实际执行的代码已经是原来viewWillAppear中的代码了.
其实以上的代码也可以简写为以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
+ (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,originalSelector,swizzledIMP,swizzledType); class_replaceMethod(class,swizzledSelector,originalIMP,originalType); }
|
这是因为class_replaceMethod
方法其实能够覆盖到class_addMethod
和method_setImplementation
两种场景, 对于第一个class_replaceMethod
来说, 如果viewWillAppear:
实现在父类, 则执行class_addMethod
, 否则就执行method_setImplementation
将原方法的IMP指定新的代码块; 而第二个class_replaceMethod
完成的工作便只是将新方法的IMP指向原来的代码.
除了以上的场景之外,其它场景下我们如何使用Method Swizzling呢?
1.在不同类之间实现Method Swizzling
上面示例是通过Category来新增一个方法然后实现Method Swizzling的, 但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明), 此时我们应该如何来做Method Swizzling呢?
例如已知一个className为Car
的类中有一个实例方法- (void)run:(double)speed
, 目前需要Hook该方法对速度小于120才执行run的代码, 按照方法交换的流程, 代码应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
#import <objc/runtime.h>
@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);
|
与之前的流程相比,在前面添加了两个逻辑:
- 利用runtime向目标类
Car
动态添加了一个新的方法,此时Car
类与MyCar
类一样具备了xxx_run:
这个方法,MyCar
的利用价值便结束了;
- 为了完成后续
Car
类中run:
与xxx_run:
的方法交换,此时需要更新swizzledMethod变量为Car
中的xxx_run:
方法所对应的Method.
以上所有的逻辑也可以合并简化为以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
+ (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
所完成的工作却是不一样的.
- 第一个
class_replaceMethod
与之前的逻辑一致, 当run:
方法是实现在Car
类或Car
的父类, 分别执行method_setImplementation
或class_addMethod
;
- 第二个
class_replaceMethod
则直接在Car
类中注册了xxx_run:
方法, 并且指定的IMP为当前run:
方法的IMP;
2.如何实现类方法的Method Swizzling
以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@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,originalSelector,swizzledIMP,swizzledType); class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType); }
+ (id)xxx_dictionary { id result = [self xxx_dictionary]; return result; }
@end
|
相比实例方法的Method Swizzling,流程有两点差异:
- 获取Method的方法变更为
class_getClassMethod(Class cls, SEL name)
,从函数命名便直观体现了和class_getInstanceMethod(Class cls, SEL name)
的差别;
- 对于类方法的动态添加,需要将方法添加到MetaClass中,因为实例方法记录在class的method-list中, 类方法是记录在meta-class中的method-list中的.
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便已经能解决该问题, 下面一个简单的示例通过交换NSMutableDictionary
的setObject:forKey:
方法,让调用这个方法时当参数object或key为空的不会抛出异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
@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<NSCopying>)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各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:
- class_addMethod 添加一个新的方法, 可能是把其它类中实现的方法添加到目标类中, 也可能是把父类实现的方法添加一份在子类中, 可能是添加的实例方法, 也可能是添加的类方法, 总之就是添加了方法.
- 交换IMP 交换方法的实现IMP,完成这个步骤除了使用
method_exchangeImplementations
这个方法外, 也可以是调用了method_setImplementation
方法来单独修改某个方法的IMP, 或者是采用在调用class_addMethod
方法中设定了IMP而直接就完成了IMP的交换, 总之就是对IMP的交换.
那我们来分别看一下这两件事情是否都还能恢复:
- 对于
class_addMethod
, 我们首先想到的可能就是有没有对应的remove方法呢, 在Objective-C 1.0的时候有class_removeMethods
这个方法, 不过在2.0的时候就已经被禁用了, 也就是苹果并不推荐我们这样做, 想想似乎也是挺有道理的, 本来runtime的接口看着就挺让人心惊胆战的, 又是添加又是删除总觉得会出岔子, 所以只能放弃remove的想法, 反正方法添加在那儿倒也没什么太大的影响.
- 针对IMP的交换, 在Method Swizzling时做的交换动作, 如果需要恢复其实要做的动作还是交换回来罢了, 所以是可以做到的, 不过需要怎样做呢? 对于同一个类, 同一个方法, 可能会在不同的地方被多次做Method Swizzling, 所以要回退某一次的Method Swizzling, 我们就需要记录下来这一次交换的时候是哪两个IMP做了交换, 恢复的时候再换回来即可. 另一个问题是如果已经经过多次交换, 我们怎样找到这两个IMP所对应的Mehod呢, 还好runtime提供了一个
class_copyMethodList
方法, 可以直接取出Method列表, 然后我们就可以逐个遍历找到IMP所对应的Method了, 下面是对上一个示例添加恢复之后实现的代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
#import <objc/runtime.h>
@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; unsigned int outCount = 0; Method *methodList = class_copyMethodList(originalClass, &outCount); for (unsigned int idx=0; idx < outCount; idx++) { Method aMethod = methodList[idx]; IMP aIMP = method_getImplementation(aMethod); if (aIMP == kMySafeSwizzledIMP) { method_setImplementation(aMethod, kMySafeOriginalIMP); } else if (aIMP == kMySafeOriginalIMP) { method_setImplementation(aMethod, kMySafeSwizzledIMP); } } kMySafeOriginalIMP = NULL; kMySafeSwizzledIMP = NULL; } while (NO); [kMySafeLock unlock]; }
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey { if (anObject && aKey) { [self safe_setObject:anObject forKey:aKey]; } else if (aKey) { [(NSMutableDictionary *)self removeObjectForKey:aKey]; } }
@end
|
注意 这段代码的Method Swizzling和恢复都需要主动调用, 并且相比上面其它的示例, 这段代码还添加如锁机制来加之保护. 这个示例是以不同的类来实现的Method Swizzling和恢复, 如果是Category或者是类方法, 根据之前的示例也需要做相应的调整.