天天看点

黑色魔法- Method Swizzling开发需求Method SwizzlingMethod Swizzling危险吗?

由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的`Method Swizzling`,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题....

小编自己有一个学习交流群,里面都是有多年开发经验的iOS大牛在里面交流,有需要的伙伴可以进来一起交流群号(681503716)(验证编码:大鲨),不定时也会分享ARKit技术,移动架构,支付宝,底层,高级进阶学习不等的视频教程资料           

开发需求

如果产品经理突然说:”在所有页面添加统计功能,也就是用户进入这个页面就统计一次”。我们会想到下面的一些方法:

- 手动添加

直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…

上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

- 继承

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

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

- Category

我们可以为

UIViewController

建一个

Category

,然后在所有控制器中引入这个

Category

。当然我们也可以添加一个

PCH

文件,然后将这个

Category

添加到

PCH

文件中。

- Method Swizzling

我们可以使用苹果的“黑色魔法”

Method Swizzling

Method Swizzling

本质上就是对

IMP

SEL

进行交换。

Method Swizzling

原理

Method Swizzing

是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将

Method Swizzling

代码写到任何地方,但是只有在这段

Method Swilzzling

代码执行完毕之后互换才起作用。

使用注意

类簇设计模式

在iOS中

NSNumber

NSArray

NSDictionary

等这些类都是类簇(

Class Clusters

),一个

NSArray

的实现可能由多个类组成。

所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。

下面列举了

NSArray

NSDictionary

本类的类名,可以通过

Runtime

函数取出本类。

注意要点

  • Swizzling

    应该总在

    +load

    中执行
  • Swizzling

    应该总是在

    dispatch_once

  • Swizzling

    +load

    中执行时,不要调用

    [super load]

    。如果多次调用了

    [super load]

    ,可能会出现“

    Swizzle

    无效”的假象,原理见下图:

封装

在项目中我们肯定会在很多地方用到

Method Swizzling

,而且在使用这个特性时有很多需要注意的地方。我们可以将

Method Swizzling

封装起来,也可以使用一些比较成熟的第三方。

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

#import <Foundation/Foundation.h>
@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 <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);           

错误剖析

在上面的例子中,如果只是单独对

NSArray

NSMutableArray

中的单个类进行

Method Swizzling

,是可以正常使用并且不会发生异常的。如果进行

Method Swizzling

的类中,有两个类有继承关系的,并且

Swizzling

了同一个方法。例如同时对

NSArray

NSMutableArray

中的o

bjectAtIndex

:方法都进行了

Swizzling

,这样可能会导致父类

Swizzling

失效的问题。

对于这种问题主要是两个原因导致的,首先是不要在

+ (void)load

方法中调用

[super load]

方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。例如下面的两张图片,你会发现由于

NSMutableArray

调用了

[super load]

导致父类

NSArray

Swizzling

代码被执行了两次。

#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
    // 这里不应该调用super,会导致父类被重复Swizzling
    [super load];

    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
    method_exchangeImplementations(fromMethod, toMethod);
}           

这里由于在子类中调用了

super

,导致

NSMutableArray

执行时,父类

NSArray

也被执行了一次。

父类

NSArray

执行了第二次

Swizzling

,这时候就会出现问题,后面会讲具体原因。

这样就会导致程序运行过程中,子类调用

Swizzling

的方法是没有问题的,父类调用同一个方法就会发现

Swizzling

失效了…..具体原因我们后面讲!

还有一个原因就是因为代码逻辑导致

Swizzling

代码被执行了多次,这也会导致

Swizzling

失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。

问题原因

我们上面提到过

Method Swizzling

的实现原理就是对类的

Dispatch Table

进行操作,每进行一次

Swizzling

就交换一次

SEL

IMP

(可以理解为函数指针),如果

Swizzling

被执行了多次,就相当于

SEL

IMP

被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行…..这样换来换去的结果,能不能成功就看运气了,这也是好多人说

Method Swizzling

不好用的原因之一。

从这张图中我们也可以看出问题产生的原因了,就是

Swizzling

的代码被重复执行,为了避免这样的原因出现,我们可以通过GCD的

dispatch_once

函数来解决,利用

dispatch_once

函数内代码只会执行一次的特性。

在每个

Method Swizzling

的地方,加上

dispatch_once

函数保证代码只被执行一次。当然在实际使用中也可以对下面代码进行封装,这里只是给一个示例代码。

#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
        method_exchangeImplementations(fromMethod, toMethod);
    });
}           

这里还要告诉大家一个调试小技巧,已经知道的可以略过。我们之前说过

IMP

本质上就是函数指针,所以我们可以通过打印函数指针的方式,查看

SEL

IMP

的交换流程。

先来一段测试代码:

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

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));           

看到这个打印结果,大家应该明白什么问题了吧:

2016-04-13 14:16:33.477 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.479 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302]      0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302]      0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302]      0x1851b7020           

Method Swizzling危险吗?

既然

Method Swizzling

可以对这个类的

Dispatch Table

进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为

Method Swizzling

是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。

这个问题可以引用念茜大神的一句话:

使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。

继续阅读