iOS黑魔法 - Method Swizzling

该文章属于<简书 — 刘小壮>原创,转载请注明:

<简书 — 刘小壮> http://www.jianshu.com/p/ff19c04b34d0git


公司年末要在新年前发一个版本,最近一直很忙,很久没有更新博客了。正好如今新版本开发的差很少了,抽空总结一下。github

因为最近开发新版本,就避免不了在开发和调试过程当中引发崩溃,以及诱发一些以前的__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 SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

Method Swizzling原理

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

并且Method Swizzling也是__iOS__中AOP(面相切面编程)的一种实现方式,咱们能够利用苹果这一特性来实现AOP编程。

原理分析

首先,让咱们经过两张图片来了解一下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];,这难道不会产生递归调用吗? 答:然而....并不会😏。

还记得咱们上面的图一和图二吗?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又不同😂。咱们能够经过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也被执行了一次。

第一次

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

第二次

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

还有一个缘由就是由于代码逻辑致使Swizzling代码被执行了屡次,这也会致使Swizzling失效,其实原理和上面的问题是同样的,咱们下面讲讲为何会出现这个问题。

问题缘由

咱们上面提到过Method Swizzling的实现原理就是对类的Dispatch Table进行操做,每进行一次Swizzling就交换一次SELIMP(能够理解为函数指针),若是Swizzling被执行了屡次,就至关于SELIMP被交换了屡次。这就会致使第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了😄,这也是好多人说Method Swizzling很差用的缘由之一。

一图胜千言:

Dispatch Table 交换流程

从这张图中咱们也能够看出问题产生的缘由了,就是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本质上就是函数指针,因此咱们能够经过打印函数指针的方式,查看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中,并且左侧有目录,方便阅读。

Runtime PDF

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

相关文章
相关标签/搜索