因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章 Method Swizzling , 我们就先以这篇文章中的示例开始说起吧:
#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
简单说明一下以上代码的几个重点:
+ (void)load
方法中添加Method Swizzling的代码,在类初始加载时自动被调用,load方法按照父类到子类,类自身到Category的顺序被调用. xxx_viewWillAppear:
方法中 [self xxx_viewWillAppear:animated];
代码并不会造成死循环,因为Method Swizzling之后, 调用 xxx_viewWillAppear:
实际执行的代码已经是原来viewWillAppear中的代码了. 上面示例是通过Category来新增一个方法然后实现Method Swizzling的,但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明),此时我们应该如何来做Method Swizzling呢?
例如已知一个className为 Car
的类中有一个实例方法 - (void)run:(double)speed
,需要Hook该方法对速度小于120才执行run的代码,下面是一段非常直观的实现代码:
#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);
// 向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 < 120) {
[self xxx_run:speed];
}
}
@end
与之前的流程相比,在前面添加了两个逻辑:
Car
动态添加了一个新的方法,此时 Car
类与 MyCar
类一样具备了 xxx_run:
这个方法, MyCar
的利用价值便结束了; Car
类中 run:
与 xxx_run:
的方法交换,此时需要更新swizzledMethod变量为 Car
中的 xxx_run:
方法所对应的Method. + (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:的方法,但实现为run:中的实现
BOOL registerMethod = class_addMethod(originalClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
if (!registerMethod) {
return;
}
// 尝试向Car类中添加run:方法,但实现为xxx_run:中的实现
BOOL didAddMethod = class_addMethod(originalClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
// 如果添加不成功,则说明Car类中已经实现run:方法,只需要修改它的实现为xxx_run:中的实现即可
if (!didAddMethod) {
method_setImplementation(originalMethod, method_getImplementation(swizzledMethod));
}
}
主要逻辑就只有三步:
Car
类添加 xxx_run:
这个方法,并且指定的IMP为当前 run:
方法的IMP; Car
类中添加 run:
方法,指定其IMP为 xxx_run:
方法的IMP,主要用于覆盖 run:
方法是在 Car
类的父类实现的场景,如果满足这样情况,则整个Method Swizzling就已经完成了; Car
类已经实现了 run:
方法,则直接修改其IMP为 xxx_run:
方法的IMP即可. 以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:
@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);
// 类方法添加,需要将方法添加到MetaClass中
Class metaClass = objc_getMetaClass(class_getName(cls));
BOOL didAddMethod = class_addMethod(metaClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(metaClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
+ (id)xxx_dictionary {
id result = [self xxx_dictionary];
return result;
}
@end
相比实例方法的Method Swizzling,流程有两点差异:
class_getClassMethod(Class cls, SEL name)
,从函数命名便直观体现了和 class_getInstanceMethod(Class cls, SEL name)
的差别; 在上面的代码中我们实现了对 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为空的不会抛出异常:
@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);
originalIMP = method_getImplementation(originalMethod);
swizzledIMP = method_getImplementation(swizzledMethod);
// 向目前类中新添加一个safe_setObject:forKey:的方法,实现为setObject:forKey:
BOOL registerMethod = class_addMethod(originalClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
if (!registerMethod) {
return;
}
// 尝试向目标类中添加setObject:forKey:方法,但实现为safe_setObject:forKey:
BOOL didAddMethod = class_addMethod(originalClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
// 如果添加不成功,刚直接修改setObject:forKey:的实现为safe_setObject:forKey:的实现即可
if (!didAddMethod) {
method_setImplementation(originalMethod, method_getImplementation(swizzledMethod));
}
}
- (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各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:
method_exchangeImplementations
这个方法外, 也可以是调用了 method_setImplementation
方法来单独修改某个方法的IMP, 或者是采用在调用 class_addMethod
方法中设定了IMP而直接就完成了IMP的交换, 总之就是对IMP的交换. 那我们来分别看一下这两件事情是否都还能恢复:
class_addMethod
, 我们首先想到的可能就是有没有对应的remove方法呢, 在Objective-C 1.0的时候有 class_removeMethods
这个方法, 不过在2.0的时候就已经被禁用了, 也就是苹果并不推荐我们这样做, 想想似乎也是挺有道理的, 本来runtime的接口看着就挺让人心惊胆战的, 又是添加又是删除总觉得会出岔子, 所以只能放弃remove的想法, 反正方法添加在那儿倒也没什么太大的影响. class_copyMethodList
方法, 可以直接取出Method列表, 然后我们就可以逐个遍历找到IMP所对应的Method了, 下面是对上一个示例添加恢复之后实现的代码逻辑: #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_addMethod(originalClass,swizzledSelector,originalIMP,originalType);
if (!class_addMethod(originalClass,originalSelector,swizzledIMP,swizzledType)) {
method_setImplementation(originalMethod, swizzledIMP);
}
} 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或者是类方法, 根据之前的示例也需要做相应的调整.