学习博客:《iOS黑魔法-Method Swizzling》 (这个作者太牛了,写了我一直想知道的类簇的swizz方法)
前言:
使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
一、 一般的swizz
先给要替换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class]; SEL originalSelector = @selector(showSplash:);
SEL swizzledSelector = @selector(swizzledShowSplash:);
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/ 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);
}
});
}
- (void)swizzledShowSplash:(NSString *)message {
if(message && message.length > ){
[self swizzledShowSplash]; // 此时swizzledShowSplash已经和showSplash交换了实现,所以这里实际是在调用showSplash
}
}
二、Method Swizzling类簇
按照上面的例子对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling来做swizz。发现Method Swizzling根本就不起作用。
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
类 | “真身” |
NSArray |
__NSArrayI |
NSMutableArray |
__NSArrayM |
NSDictionary |
__NSDictionaryI |
NSMutableArray |
__NSDictionaryM |
例如:防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃的swizz方法:
#import "NSArray+CRMArray.h" @implementation NSArray (CRMArray)
+ (void)load {
[super load];
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
} - (id)lxz_objectAtIndex:(NSUInteger)index {
if (self.count- < index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self lxz_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} else {
return [self lxz_objectAtIndex:index];
}
}
@end
三、使用场景
通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法添加日志记录功能,这非常有助于调试程序。然而,此做法只在调试程序时用,很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。