开始文章之前先来抛出一个问题,假如有一个已经成形的项目,希望在进入每个控制器的时候添加一个统计,这个怎么实现呢?项目很大,各个控制器的功能都已经很完善,不可能每个手动单独去添加。
也许我们可以考虑使用继承
的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。
这个时候我们就可以使用iOS的黑魔法,Method Swizzling来实现,开始之前先来了解一个概念:
1. SEL和IMP
SEL : 类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。
IMP: 一个函数指针,保存了方法的地址
IMP和SEL关系:
每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject)创建的。在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch table.。Dispatch table是一张SEL和IMP的对应表。
也就是说方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMP,IMP就是一个函数指针,然后执行这个方法。
2. Method Swizzling
还是上面那个问题,我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。
我们创建一个Category来覆盖系统方法,结合Method Swizzling,系统会优先调用Category中的代码,然后在调用原类中的代码。
我们可以通过下面的这段伪代码来看一下:
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"页面统计:%@", self);
}
@end
2.1 Method Swizzling原理
Method Swizzling本质上就是对IMP和SEL进行交换,Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
首先,让我们通过两张图片来了解一下Method Swizzling的实现原理。
上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
2.2 Method Swizzling实现
在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API:
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
我们来继续解决文章开头提到的问题。
先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。
#import "UIViewController+swizzling.h"
#import
@implementation UIViewController (swizzling)
+ (void)load {
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![str containsString:@"UI"]){
NSLog(@"统计打点 : %@", self.class);
}
[self swizzlingViewDidLoad];
}
@end
看到上面的代码,肯定有人会担心,在swizzlingViewDidLoad方法中又调用了[self swizzlingViewDidLoad],难道不会产生递归调用吗?
并不会。
还记得我们上面的图一和图二吗?Method Swizzling的实现原理可以理解为“方法互换”。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。
例如我们上面的代码,系统调用UIViewController的viewDidLoad方法时,实际上执行的是我们实现的swizzlingViewDidLoad方法。而我们在swizzlingViewDidLoad方法内部调用[self swizzlingViewDidLoad]时,执行的是UIViewController的viewDidLoad方法。
2.3 Method Swizzling类簇(特殊情况)
有一点要说明的是,Method Swizzling对NSArray,NSDictionary,NSMutableArray,NSMutableDictionary这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:
#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (void)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-1 < 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
下面我们列举一些常用的类簇的“真身”:
NSArray : __NSArrayI
NSMutableArray :__NSArrayM
NSDictionary :__NSDictionaryI
NSMutableDictionary: __NSDictionaryM
2.4 Method Swizzling常见错误
在load里面再次调用[super load]会导致Method Swizzling被调用两次,最终也交换了两次,导致交换失败。
为了防止这种情况的出现,可以引入GCD里面的函数,如下图:
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
});
}
3. 实际项目经验
最近发现一个好用的魔法method_setImplementation
做iPhoneX适配的时候发现,原有的根据状态栏statusBar判断网络状态的函数由于状态栏布局发生变化失效,暂时想把这个方法用另一种方法来替换,奈何项目里用到的地方比较多,而且希望仅在iPhone X上生效,其他机型不变。
这个时候我们就可以用这个黑魔法了。
代码如下:
if (IS_IPHONEX) { // statusBar布局变化了, 先用NetworkReachability代替着, 有空再研究
Method m = class_getInstanceMethod([IFUtil class], @selector(networkTypeFromStatusBar));
method_setImplementation(m, imp_implementationWithBlock(^NETWORK_TYPE(id self){
AFNetworkReachabilityStatus s = [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus;
switch( s ){
case AFNetworkReachabilityStatusNotReachable: {
return NETWORK_TYPE_NONE;
} break;
case AFNetworkReachabilityStatusReachableViaWiFi: {
return NETWORK_TYPE_WIFI;
} break;
case AFNetworkReachabilityStatusReachableViaWWAN: {
return NETWORK_TYPE_3G;
}
default: {
return NETWORK_TYPE_NONE;
} break;
}
}));
}
}
更多详细讲解移步https://www.jianshu.com/p/ff19c04b34d0