方法变换:(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
这里有一个地方解释一下,在myLowerString方法里面,看起来递归调用了自身。但是,我们会用原来的lowercaseString方法去替换自己写的myLowerString方法,所以这里并没有调用自身,而是调用了原来的lowercaseString方法。这点请注意一下。其次替换系统原来的lowercaseString方法,使用runtime里面的方法。
我们来看一下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
的实现原理
![](https://i-blog.csdnimg.cn/blog_migrate/a40c1abe926bed06d299e46469ca2377.webp?x-image-process=image/format,png)
![](https://i-blog.csdnimg.cn/blog_migrate/1b7411bf3e448149b86da0b553afd415.webp?x-image-process=image/format,png)
上面图一中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方法。