iOS黑魔法 - Method Swizzling

栏目: IOS · 发布时间: 5年前

内容简介:就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。我们可以使用

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

手动添加

直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太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 SwizzlingMethod Swizzling 本质上就是对 IMPSEL 进行交换。

Method Swizzling原理

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

而且 Method Swizzling 也是__iOS__中 AOP (面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现 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 类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。

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

还记得我们上面的图一和图二吗? Method Swizzling 的实现原理可以理解为”方法互换“。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。

例如我们上面的代码,系统调用 UIViewControllerviewDidLoad 方法时,实际上执行的是我们实现的 swizzlingViewDidLoad 方法。而我们在 swizzlingViewDidLoad 方法内部调用 [self swizzlingViewDidLoad]; 时,执行的是 UIViewControllerviewDidLoad 方法。

Method Swizzling类簇

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

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

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

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

代码示例

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

#import "NSArray+LXZArray.h"
#import "objc/runtime.h"

@implementation NSArray (LXZArray)

+ (void)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 又不一样:joy:。我们可以通过 runtime 函数获取真正的类:

objc_getClass("__NSArrayI");
复制代码

举例

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

“真身”
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

其他自行Google....

JRSwizzle

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

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

#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);
复制代码

Method Swizzling 错误剖析

在上面的例子中,如果只是单独对 NSArrayNSMutableArray 中的单个类进行 Method Swizzling ,是可以正常使用并且不会发生异常的。如果进行 Method Swizzling 的类中,有两个类有继承关系的,并且 Swizzling 了同一个方法。例如同时对 NSArrayNSMutableArray 中的 objectAtIndex: 方法都进行了 Swizzling ,这样可能会导致父类 Swizzling 失效的问题。

对于这种问题主要是两个原因导致的,首先是不要在 + (void)load 方法中调用 [super load] 方法,这会导致父类的 Swizzling 被重复执行两次,这样父类的 Swizzling 就会失效。例如下面的两张图片,你会发现由于 NSMutableArray 调用了 [super load] 导致父类 NSArraySwizzling 代码被执行了两次。

错误代码:

#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也被执行了一次。

iOS黑魔法 - Method Swizzling

父类NSArray执行了第二次Swizzling,这时候就会出现问题,后面会讲具体原因。

iOS黑魔法 - Method Swizzling

这样就会导致程序运行过程中,子类调用 Swizzling 的方法是没有问题的,父类调用同一个方法就会发现 Swizzling 失效了.....具体原因我们后面讲!

还有一个原因就是因为代码逻辑导致 Swizzling 代码被执行了多次,这也会导致 Swizzling 失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。

问题原因

我们上面提到过 Method Swizzling 的实现原理就是对类的 Dispatch Table 进行操作,每进行一次 Swizzling 就交换一次 SELIMP (可以理解为函数指针),如果 Swizzling 被执行了多次,就相当于 SELIMP 被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了:smile:,这也是好多人说 Method Swizzling 不好用的原因之一。

一图胜千言:

iOS黑魔法 - 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);
    });
}
复制代码

这里还要告诉大家一个调试小技巧,已经知道的可以略过:blush:。我们之前说过 IMP 本质上就是函数指针,所以我们可以通过打印函数指针的方式,查看 SELIMP 的交换流程。

先来一段测试代码

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 的实现源码,从源码来看,其实内部实现很简单。核心代码就是交换两个 Methodimp 函数指针,这也就是方法被 swizzling 多次,可能会被换回去的原因,因为每次调用都会执行一次交换操作。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}
复制代码

Method Swizzling危险吗?

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

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

在这个 Demo 中通过 Method Swizzling ,简单实现了一个崩溃拦截功能。实现方式就是将原方法 Swizzling 为自己定义的方法,在执行时先在自己方法中做判断,根据是否异常再做下一步处理。

Demo 只是来辅助读者更好的理解文章中的内容, 应该博客结合 Demo 一起学习,只看 Demo 还是不能理解更深层的原理 Demo 中代码都会有注释,各位可以打断点跟着 Demo 执行流程走一遍,看看各个阶段变量的值。

Demo地址: 刘小壮的Github

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

iOS黑魔法 - Method Swizzling

下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

黑客渗透

黑客渗透

冰的原点 / 齐鲁电子音像出版社 / 2009-4 / 22.00元

菜鸟起飞,从这里开始!本笔记将透露:渗透、术语、脚本、内网、溢出各种攻击相关的手段和名词,总结、技巧、细节、亮点,不断变化的攻击思想。 ASP、PHP、JSP等不同类型的脚本漏洞,ACCESS、MYSQL、MSSQL、ORACLE等不同类型的数据库缺陷,国内、国外已知和末知的渗透工具······一起来看看 《黑客渗透》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具