iOS黑魔法-Method Swizzling

xiaoxiao2021-02-28  75

需求

就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:

手动添加

直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

继承

我们可以使用OOP的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。

然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

Category

我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

我们创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码。

我们可以通过下面的这段伪代码来看一下:

#import "UIViewController+EventGather.h" @implementation UIViewController (EventGather) - (void)viewDidLoad { NSLog(@"页面统计:%@", self); } @end
Method Swizzling

我们可以使用苹果的“黑魔法”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类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。

定义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];,这难道不会产生递归调用吗? 答:然而....并不会

转载请注明原文地址: https://www.6miu.com/read-72492.html

最新回复(0)