最近项目中需要导入新功能,说是新功能,实际上完全是一个新的APP。只不过在我们的项目中开放了一个管理中心,可以调用或者跳转到新功能中,实现对新APP的集成。通过将新功能封装成库(framework),在项目中调用。这其中就会遇到各种各样的问题。逼不得已,有些问题用到了method swizzling来解决。自己一直不强记具体怎么写,不过临时找起来也挺麻烦的,这里就做个笔记,下次方便copy。
-原理
理解method swizzling先要从method开始。在oc的世界里,很多类型实际上都是c语言中的结构体。例如说method,本质是objc_method :
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name;//方法名
char *method_types;//参数类型
IMP method_imp;//指向方法具体实现的函数指针
};
一目了然,是由方法名,参数类型和方法的实现组成。
同样,SEL也是结构体:
typedef struct objc_selector *SEL;
objc_selector并没有在头文件中查到具体结构,不过通过下面代码可以打印出结果:
- (NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b {
return (a > b) ? a : b;
}
NSLog(@"SEL=%s", @selector(maxIn:theOther:));
输出:SEL=maxIn:theOther:
SEL sel1 = @selector(maxIn:theOther:);
NSLog(@"sel : %p", sel1);
输出:sel : 0x100002d72
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。所以在同一个类中,不能存在2个同名的方法,即使参数类型不同也不行。不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的selector时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据selector 去查找具体的方法实现IMP,然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。
IMP实际上是一个函数指针,指向方法实现的首地址。
typedef id (*IMP)(id, SEL, ...);
这个函数的第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。
所以很容易理解objc_method结构体这么做的原因,当通过消息来调用类中某个函数时,首先会根据SEL,即方法名在对象的方法列表中查询,然后查找到对应的方法实现IMP,执行方法。method swizlling的作用,便是修改method中SEL和IMP的对应关系(IMP函数指针指向),使SEL与新的swizllingIMP对应,从而实现原先调用SEL方法名,但实际在运行时执行的使我们swizllingIMP中的代码。盗图如下,selector2和selector3实现IMP交换:
-应用场景
比方说最开头时提到的情况,新导入的功能,我们需要对所有的新界面做出统一的皮肤修改,例如背景色设置。这时候我们只有导入功能的framework,没办法到具体代码中添加新方法,更不能试图子类化方式来统一修改。更遗憾的是,framework中并没有公开这些需要改变界面的类的头文件。所以也无法通过category的方式来做出改变。
但我们可以在运行时中,窥探出framework中的究竟。通过找到framework中,viewcontroller的基类,例如XXXBase开头的XXXBaseViewcontroller,然后利用method swizzling方式,替换viewdidappear方法的实现,加入我们队UI的额外修改。从而实现在运行时,对新功能界面背景色的修改。
除此之外,很多情况,例如对viewcontroller生命周期函数,统一添加log;对某些方法添加统一的提前判断,等等,都可以通过method swizzling方式完美解决。总而言之,method swizzling就像是一把手术刀,在原有的应用体系中切出一个横面,直达目的,做出修改。
-实现
首先实现一个具有代表性的场景:替换UIViewcontroller中的viewDidAppear:方法。
#import "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
+ (void)load {
Class class = [self class];
//取得函数名称
SEL originSEL = @selector(viewDidAppear:);
SEL swizzleSEL = @selector(swizzleViewDidAppear:);
//根据函数名,从class的method list中取得对应的method结构体,如果是实例方法用class_getInstanceMethod,类方法用class_getClassMethod()。会从当前的Class中寻找对应方法名的实现,若没有则向上遍历父类中查找。若父类中也没有,则返回NULL。
Method originMethod = class_getInstanceMethod(class, originSEL);
Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
//class_addMethod向class中添加对应方法名和方法实现。如果该class(不包含父类)已含有该方法名,则返回NO。
BOOL didAddMethod = class_addMethod(class, originSEL,
method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
//因为已向class中添加了swizzledMethod实现对应的方法名,只需要替换swizzledSelector的实现为originalMethod。
class_replaceMethod(class,
swizzleSEL,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
} else {
//class中本来就含有originSEL的method,只需要交换originalMethod和swizzledMethod的实现。
method_exchangeImplementations(originMethod,
swizzleMethod);
}
}
// 我们自己实现的方法,也就是和self的swizzleViewDidAppear方法进行交换的方法。
- (void)swizzleViewDidAppear:(BOOL)animated {
[self swizzleViewDidAppear:animated];
}
@end
代码中有详细的注释,实现思路不再赘述。稍微说一下的是,swizzleViewDidAppear:中调用了[self swizzleViewDidAppear:animated],初学者可能会提问,说这样不是会死循环么?其实不然,因为swizzleViewDidAppear:和viewDidAppear:已经交换了实现方法,当viewcontroller的viewDidAppear:被调用时,走到了swizzleViewDidAppear:,在swizzleViewDidAppear:中只有调用[self swizzleViewDidAppear:animated],才能走到viewDidAppear:的实现方法中。
另外,swizzle方法都是会写在+ load这个类方法中。可以看一下objective-c对这个方法的解释。简单来说,oc的runtime会首先加载各个类或者类的category中的+load方法。当我们需要利用method swizzling来修改类的布局时,需要在类或者类的category的+load方法中修改,从而保证对类的布局尽早做出修改。+load的调用顺序需要注意一下:
-对于有依赖关系的两个库中,被依赖的类的load会优先调用。但在一个库之内,调用顺序是不确定的。
-对于一个类而言,没有load方法实现就不会调用,不会考虑对NSObject的继承。
-一个类的load方法不用写明[super load],父类就会收到调用,并且在子类之前
-Category的load也会收到调用,但顺序上在主类的load调用之后
-拓展 AOP编程思想运用
理解了method swizzling的原理和实现,很容易的联想到,在日志添加,事件统计等具有面向性,统一性的功能上,有很大的作用。这一类功能,如果不通过method swizzling方式,可能需要利用继承或者category,来改写或者在每个类的方法中添加重复的代码片段,即增加工作量,又破坏了原有逻辑的连贯性。这时,AOP编程思想就显得非常重要。
Aspect Oriented Programming(AOP),即面向切面编程。在利用java开发大型系统时,AOP经常会被提到,例如性能监测,访问控制,事务管理、缓存、对象池管理以及日志记录等等,这些功能也都是具有横切性的特性。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在iOS中,AOP思想的运用没有在Java那么广泛。但method swizzling方式的运用,给了在oc世界实践AOP编程思想的可能。通过将功能横切注入,实现既不破坏原有代码整体性,又能实现添加或替换的功能。
github上第三方库Aspects已经利用method swizzling为我们提供了AOP实现的思路。Aspects中提供了两个接口:
+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
AspectOptions给了我们选择,可以将你的操作block添加在目的方法之前,之后执行,或者是直接替换。有了这两个API,我们甚至可以跟在Java中一样,通过配置文件的方式,来实现面向切面的操作。
例如我们设置dictionary格式:
NSDictionary*config =@{
@"MainViewController":@{
GLLoggingPageImpression:@"page imp - main page",
GLLoggingTrackedEvents:@[
@{
GLLoggingEventName:@"button one clicked",
GLLoggingEventSelectorName:@"buttonOneClicked:",
GLLoggingEventHandlerBlock: ^(id aspectInfo) {
NSLog(@"button one clicked");
},
},
@{
GLLoggingEventName:@"button two clicked",
GLLoggingEventSelectorName:@"buttonTwoClicked:",
GLLoggingEventHandlerBlock: ^(id aspectInfo) {
NSLog(@"button two clicked");
},
},
],
},
@"DetailViewController":@{
GLLoggingPageImpression:@"page imp - detail page",
}
};
然后通过解析,利用Aspects的API,实现AOP。具体实现可以参考Demo。
-两面性
任何事物都是具有两面性的,method swizzling也是一样。在我们看到它极大的扩展了我们改变程序运行时的能力的同时,也很容易发现带来的副作用。
- 运行时改变,代码原有逻辑与运行时不一致,很难在review代码过程中找出可能存在的问题
- method的实现需要在运行时状态才能确定,增加了debug难度
- 大量method swizzling的运用,或者未加管理的添加,导致方式实现指向混乱
- 全局修改容易带来副作用。新导入的模块例如framework,并不需要现有项目中method swizzling的处理,甚至可能出现冲突。
所以说,method swizzling是一把利刃,如果玩不好很容易误伤自己或他人。并且你也很难保证所有人都是玩刀的好手。method swizzling要解决的难题,是否最终应该通过应用架构的设计来完善?或者它的作用范围更应该局限在架构的整体设计中,而不该在具体功能中出现。