Method Swizzling——方法欺骗。Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。
应用场景
例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。
实现示例
@implementation UIViewController (Test)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(my_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if(didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)my_viewWillAppear:(BOOL)animated {
[self my_viewWillAppear:animated];
NSLog(@"调用了自己的方法");
}
@end
上面的代码将我们的方法my_viewWillAppear与viewWillAppear方法对应的指针函数交换了。
所以,当调用viewWillAppear时会调用我们自己定义的方法。我们可以在一处地方注入我们新的操作而不用写得到处都是。
Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load
会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。
Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。
调用_cmd
- (void)my_viewWillAppear:(BOOL)animated {
[self my_viewWillAppear:animated];
NSLog(@"调用了自己的方法");
}
咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。
class_replaceMethod与method_exchangeImplementations的区别。
Objective-C 提供了以下 API 来动态替换类方法或实例方法的实现:
- class_replaceMethod 替换类方法的定义
- method_exchangeImplementations 交换 2 个方法的实现
- method_setImplementation 设置 1 个方法的实现
class_replaceMethod
在苹果的文档(如下图所示)中能看到,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用class_addMethod
来为该类增加一个新方法,也因为如此,class_replaceMethod
在调用时需要传入types
参数
method_exchangeImplementations
的内部实现相当于调用了 2 次method_setImplementation
方法
例子优化版本
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(my_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_addMethod
和method_setImplementation
两种场景, 对于第一个class_replaceMethod
来说, 如果viewWillAppear:
实现在父类, 则执行class_addMethod
, 否则就执行method_setImplementation
将原方法的IMP指定新的代码块; 而第二个class_replaceMethod
完成的工作便只是将新方法的IMP指向原来的代码.
其他姿势的Method Swizzling
类方法的Method Swizzling
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(dictionary);
SEL swizzlidSelector = @selector(my_dictionary);
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzlidMethod = class_getClassMethod(class, swizzlidSelector);
if(!originalMethod || !swizzlidMethod) {
return ;
}
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzlidIMP = method_getImplementation(swizzlidMethod);
const char* originalType = method_getTypeEncoding(originalMethod);
const char* swizzlidType = method_getTypeEncoding(swizzlidMethod);
Class metaClass = objc_getMetaClass(class_getName(class));
class_replaceMethod(metaClass, swizzlidSelector, originalIMP, originalType);
class_replaceMethod(metaClass, originalSelector, swizzlidIMP, swizzlidType);
});
}
+ (NSDictionary *)my_dictionary {
id result = [self my_dictionary];
NSLog(@"调用了自己的dic");
return result;
}
其实基本没有什么区别,区别只是在获取Method调用的方法是class_getClassMethod ,还有就是最后class_replaceMethod时,第一个参数传入的是元类。这么做的主要原因是类方法的methodList存储的位置在元类中。
类簇中的Method Swizzling
关于类簇可以参见这篇博客。
在上面的代码中我们实现了对NSDictionary中的+ (id)dictionary方法的交换,但如果我们用类似代码尝试对- (id)objectForKey:(id)key方法进行交换后, 你便会发现这似乎并没有什么用.
id obj1 = [NSArray alloc]; // __NSPlacehodlerArray *
id obj2 = [NSMutableArray alloc]; // __NSPlacehodlerArray *
id obj3 = [obj1 init]; // __NSArrayI *
id obj4 = [obj2 init]; // __NSArrayM *
原因就是alloc以后产生的并不是我们想的那个类,而是一个中间类,我们的实例方法是存储在中间类的方法列表里。所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling
+ (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];
}
}
小结
Method Swizzling是runtime应用中最常见的黑魔法之一。它很强大,可以灵活的修改方法的实现,但它也很危险,我们需要更加仔细的理解runtime而不是简单的粘贴复制。
相关文章
Method Swizzling的各种姿势
黑魔法 - Method Swizzling
Objective-C Runtime 运行时之四:Method Swizzling