欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS之方法变换(Method Swizzling)

程序员文章站 2024-03-22 08:06:04
...

方法变换:(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

[objc] view plain copy
  1. @interface NSString (wzTest)  
  2.   
  3. - (NSString*)myLowerString;  
  4.   
  5. @end  
  6.   
  7. @implementation NSString (wzTest)  
  8.   
  9. - (NSString*)myLowerString  
  10. {  
  11.     NSString *lowerString = [self myLowerString];  
  12.     NSLog(@"%@ => %@"self, lowerString);  
  13.     return  lowerString;  
  14. }  
  15.   
  16. @end  
      这里有一个地方解释一下,在myLowerString方法里面,看起来递归调用了自身。但是,我们会用原来的lowercaseString方法去替换自己写的myLowerString方法,所以这里并没有调用自身,而是调用了原来的lowercaseString方法。这点请注意一下。    

      其次替换系统原来的lowercaseString方法,使用runtime里面的方法。

[objc] view plain copy
  1. Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));  
  2. Method swapMethod = class_getInstanceMethod([NSString class], @selector(myLowerString));  
  3. method_exchangeImplementations(originalMethod, swapMethod);  
  4. NSString *testStr = @"thIs is THE Test STRING";  
  5. 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本质上就是对IMPSEL进行交换。

Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

而且Method Swizzling也是iOSAOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

首先,让我们通过两张图片来了解一下Method Swizzling的实现原理
iOS之方法变换(Method Swizzling)
图一
iOS之方法变换(Method Swizzling)
图二

上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。

OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。

在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SELIMP(可以理解为函数指针)进行对应。而我们的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子类都具备的用于运行时加载处理的一个类方法,详细说明如下:

iOS之方法变换(Method Swizzling)



定义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方法。