iOS之方法变换(Method Swizzling)
方法变换:(Method Swizzling)
先来看看swizzle的意思:(用酒棒等)搅和
在计算机学科中,指针变换(pointer swizzling)是指将基于名字或位置的引用转变为直接的指针引用。 然而在Objective-C中,这个词的起源并不完全知道,但关于这一借鉴其实也很好理解,method swizzling可以通过选择器来改变它引用的函数指针。
Method swizzling指的是改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C中方法的调用能够在运行时进改变——通过改变类的调度表(dispatch table)中选择器到最终函数间的映射关系。
C语言是静态语言,它的工作方式是通过函数调用,这样在编译时我们就已经确定程序如何运行的。
Objective-C是动态语言,它并非通过调用类的方法来执行功能,而是给对象发送消息,对象在接收到消息之后会去找匹配的方法来运行。这种做法就把C语言在编译时的工作挪到了运行时来做,可以获得额外的灵活性。
Objective-C中有个@selector,在很多地方被翻译成“选择器”。实际上,对于类的实例对象来说,类的方法是用一个数字来代表的,并非是我们看到的一串字符串。通过这个@selector就可以把这个方法的名字转成所对应的数字。当一个类确定后,实际上每个方法的@selector的值就是固定的。
假设我们有个A方法,那么@selector(A)就是一个数字,我们的对象在接收到一个消息后就去查找对应的方法并运行;
如果我们把@selector(B)的数字换成了原来@selector(A)的数字,那么此时对象虽然受到A消息,但会去运行B方法!
在iOS中,这是完全可以实现的,那么我们什么时候会需要这么做呢?我觉得有2个时候:
1. **,毋庸讳言,这绝对是**的利器,不解释了。
2. 在开发调试过程中,如果你对某个库里的方法不确定或者觉得需要扩展的时候,你可以自己写一个去代替它。因为Objective-C是有Category的,所以扩展功能没啥必要,但调试时增加一些打印语句是很方便实际的。
举个例子,NSString里面的lowercaseString方法,如果我不太清楚这个方法都干了什么,我就可以自己写个方法来替换它,这个方法里面增加打印语句,这样log里面就一目了然了。
首先需要增加一个NSString的Category
- @interface NSString (wzTest)
- - (NSString*)myLowerString;
- @end
- @implementation NSString (wzTest)
- - (NSString*)myLowerString
- {
- NSString *lowerString = [self myLowerString];
- NSLog(@"%@ => %@", self, lowerString);
- return lowerString;
- }
- @end
其次替换系统原来的lowercaseString方法,使用runtime里面的方法。
- Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
- Method swapMethod = class_getInstanceMethod([NSString class], @selector(myLowerString));
- method_exchangeImplementations(originalMethod, swapMethod);
- NSString *testStr = @"thIs is THE Test STRING";
- NSLog(@"lowerString of testStr=%@", [testStr lowercaseString]);
我们来看一下log的结果:
2014-05-29 22:17:55.514 testTableView[1582:a0b] thIs is THE Test STRING => this is the test string
2014-05-29 22:17:55.514 testTableView[1582:a0b] lowerString of testStr=this is the test string
我们可以看到,系统中使用是继续使用lowercaseString方法的,不过实际执行的是我们新增的方法。当你不需要这样做的时候,关闭method swizzling方法就可以恢复了。
我们的例子中是增加了打印语句,实际上还可以做更多地操作。这在用第三方库调试的时候是非常有用的一个方法,可以很方便的查看变量的内容或做一些其他工作。调试结束后,关闭method swizzling就可以正常的工作。
需求
就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:
方案1:手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...
上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。
方案2:继承
我们可以使用OOP
的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。
方案3:Category
我们可以为UIViewController
建一个Category
,然后在所有控制器中引入这个Category
。当然我们也可以添加一个PCH
文件,然后将这个Category
添加到PCH
文件中。
我们创建一个Category
来覆盖系统方法,系统会优先调用Category
中的代码,然后在调用原类中的代码。
我们可以通过下面的这段伪代码来看一下:
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"页面统计:%@", self);
}
@end
方案4:Method Swizzling
Method Swizzling
本质上就是对IMP
和SEL
进行交换。
Method Swizzling原理
Method Swizzing
是发生在运行时的,主要用于在运行时将两个Method
进行交换,我们可以将Method Swizzling
代码写到任何地方,但是只有在这段Method Swilzzling
代码执行完毕之后互换才起作用。
而且Method Swizzling
也是iOS中AOP
(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP
编程。
首先,让我们通过两张图片来了解一下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
。
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);
实现思路
就拿上面我们说的页面统计的需求来说吧,这个需求在很多公司都很常见,我们下面的Demo就通过Method Swizzling
简单的实现这个需求。
我们先给UIViewController
添加一个Category
,然后在Category
中的+(void)load
方法中添加Method Swizzling
方法,我们用来替换的方法也写在这个Category
中。由于load
类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
+ (void) load方法,是所有NSObject子类都具备的用于运行时加载处理的一个类方法,详细说明如下:
定义Method Swizzling
中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling
的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@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方法。
上一篇: 测试开发进阶(四十七)
下一篇: 1073 多选题常见计分法