iOS黑魔法-Method Swizzling



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

需求

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

手动添加

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

继承

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

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

Category

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

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

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

1
2
3
4
5
6
#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:

1
2
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的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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:方法,取下标时数组越界导致的崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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函数获取真正的类:

1
objc_getClass( "__NSArrayI" )

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

QQ截图20160121113018.png

Method Swizzling封装

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

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

1
2
3
4
5
6
7
8
#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 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。


转自  http://www.cocoachina.com/ios/20160121/15076.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值