iOS之运行时机制及方法混写method swizzling

公司年底要在新年前发一个版本,最近一直很忙,好久没有更新博客了。正好现在新版本开发的差不多了,抽空总结一下。
由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的Method Swizzling,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题......

需求

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

手动添加

直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...
上面这种方法太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的实现原理

270478-efc2eea5e79a172f.png

图一

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

定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。


#import "UIViewController+swizzling.h"

#import @implementation UIViewController (swizzling)

+ (void)load {

  [super 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(viewDidLoad), 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方法。

Method Swizzling类簇

之前我也说到,在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。

由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?

这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:


#import "NSArray+LXZArray.h"

#import "objc/runtime.h"

@implementation NSArray (LXZArray)

+ (void)load {

  [super load];

  Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));

  Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));

  method_exchangeImplementations(fromMethod, toMethod);

}

- (id)lxz_objectAtIndex:(NSUInteger)index {

  if(self.count-1 < index) {

// 这里做一下异常处理,不然都不知道出错了。

  @try {

  return [self lxz_objectAtIndex:index];

  }

  @catch (NSException *exception) {

  // 在崩溃后会打印崩溃信息,方便我们调试。

  NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);

  NSLog(@"%@", [exception callStackSymbols]);

  return nil;

  }

  @finally {}

  } else{

  return [self lxz_objectAtIndex:index];

  }

}

@end

大家发现了吗,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样????。我们可以通过runtime函数获取真正的类:

objc_getClass("__NSArrayI")

下面我们列举一些常用的类簇的“真身”:

QQ截图20160121113018.png

Method Swizzling封装

在项目中我们肯定会在很多地方用到Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling封装起来,也可以使用一些比较成熟的第三方。
在这里我推荐Github上星最多的一个第三方-jrswizzle

里面核心就两个类,代码看起来非常清爽。

#import @interface NSObject (JRSwizzle)

+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;

+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;

@end

// MethodSwizzle类

#import BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);

BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);

Method Swizzling危险吗?

既然Method Swizzling可以对这个类的Dispatch Table进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为Method Swizzling是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。

这个问题可以引用念茜大神的一句话:使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。

讨论

这里是一些 Method Swizzling的陷阱:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

我将逐一分析这些点,增进对Method Swizzling的理解的同时,并搞懂如何应对。

Method swizzling is not atomic

我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。

Changes behavior of un-owned code

这是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。

想象一下,如果你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。

Possible naming conflicts

命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:

[cpp] view plaincopy

@interface NSView : NSObject  
- (void)setFrame:(NSRect)frame;  
@end  

@implementation NSView (MyViewAdditions)  

- (void)my_setFrame:(NSRect)frame {  
// do custom work  
[self my_setFrame:frame];  
}  

+ (void)load {  
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];  
}  

@end  

这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?

这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:

[cpp] view plaincopy

@implementation NSView (MyViewAdditions)  

static void MySetFrame(id self, SEL _cmd, NSRect frame);  
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);  

static void MySetFrame(id self, SEL _cmd, NSRect frame) {  
// do custom work  
SetFrameIMP(self, _cmd, frame);  
}  

+ (void)load {  
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];  
}  
@end  

看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。

最后给出一个较完美的swizzle方法的定义:

[cpp] view plaincopy

typedef IMP *IMPPointer;  

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {  
IMP imp = NULL;  
Method method = class_getInstanceMethod(class, original);  
if (method) {  
const char *type = method_getTypeEncoding(method);  
imp = class_replaceMethod(class, original, replacement, type);  
if (!imp) {  
imp = method_getImplementation(method);  
}  
}  
if (imp && store) { *store = imp; }  
return (imp != NULL);  
}  

@implementation NSObject (FRRuntimeAdditions)  
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {  
return class_swizzleMethodAndStore(self, original, replacement, store);  
}  
@end  

Swizzling changes the method's arguments

我认为这是最大的问题。想正常调用method swizzling 将会是个问题。

[cpp] view plaincopy

[self my_setFrame:frame];  

直接调用my_setFrame: , runtime做的是

[cpp] view plaincopy

objc_msgSend(self, @selector(my_setFrame:), frame);  

runtime去寻找my_setFrame:的方法实现, _cmd参数为 my_setFrame: ,但是事实上runtime找到的方法实现是原始的 setFrame: 的。

一个简单的解决办法:使用上面介绍的swizzling定义。

The order of swizzles matters

多个swizzle方法的执行顺序也需要注意。假设 setFrame: 只定义在NSView中,想像一下按照下面的顺序执行:

[cpp] view plaincopy

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];  
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];  

What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).

When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.

But what if the order were:

[cpp] view plaincopy

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];  
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];  

Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?

Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!

这段贴了原文,硬翻译太拗口……总结一下就是:多个有继承关系的类的对象swizzle时,从子类对象开始 。 如果先swizzle父类对象,那么后面子类对象swizzle时就无法拿到真正的原始方法实现了。

(感谢评论中 qq373127202 的提醒,在此更正一下,十分感谢)

多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。

Difficult to understand (looks recursive)

(新方法的实现)看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂.
这个问题是已完全解决的了!

Difficult to debug

debug时打出的backtrace,其中掺杂着被swizzle的方法名,一团糟啊!上面介绍的swizzle方案,使backtrace中打印出的方法名还是很清晰的。但仍然很难去debug,因为很难记住swizzling影响过什么。给你的代码写好文档(即使只有你一个人会看到)。养成一个好习惯,不会比调试多线程问题还难的。

结论

如果使用恰当,Method swizzling 还是很安全的.一个简单安全的方法是,仅在load中swizzle。 和许多其他东西一样,它也是有危险性的,但理解它了也就可以正确恰当的使用它了

推荐阅读更多精彩内容