iOS底层学习 - Runtime之Method Swizzling黑魔法

相信你们对于Method Swizzling并不陌生,在平时的开发中多多少少都有些使用,这也是Runtime的开发应用中比较普遍的用法。可是它确确实实是个黑魔法,一有不慎,就是一座天坑。本章就来研究一下它,让他变成白魔法编程

什么是Method Swizzling?

  1. Method Swizzling(方法交换),顾名思义,就是将两个方法的实现交换,即由原来的A-AImpB-BImp对应关系变成了A-BImpB-AImp设计模式

  2. 每一个类都维护一个方法Method列表,Method则包含SEL和其对应IMP的信息,方法交换作的事情就是把SELIMP的对应关系断开,并和新的IMP生成对应关系。数组

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

  4. Method Swizzling是OC动态性的最好诠释,深刻地去学习并理解其特性,将有助于咱们在业务量不断增大的同时还能保持代码的低耦合度,下降维护的工做量和难度。服务器

能够用图来更好的解释一下app

交换前: 框架

交换后:

Method Swizzling相关函数API

//获取经过SEL获取一个方法
class_getInstanceMethod
复制代码
//获取一个方法的实现
method_getImplementation
复制代码
//获取一个OC实现的编码类型
method_getTypeEncoding
复制代码
//給方法添加实现
class_addMethod
复制代码
//用一个方法的实现替换另外一个方法的实现
class_replaceMethod
复制代码
//交换两个方法的实现
method_exchangeImplementations
复制代码

Method Swizzling使用注意事项

  • 1.方法交换应该保证惟一性和原子性
    • 惟一性:应该尽量在+load方法中实现,这样能够保证方法必定会调用且不会出现异常。
    • 原子性:使用dispatch_once来执行方法交换,这样能够保证只运行一次。
  • 2.必定要调用原始实现
    • 因为iOS的内部实现对咱们来讲是不可见的,使用方法交换可能会致使其代码结构改变,而对系统产生其余影响,所以应该调用原始实现来保证内部操做的正常运行
  • 3.方法名必须不能产生冲突
    • 这个是常识,避免跟其余库产生冲突。
  • 4.作好注释和Log
    • 记录好被影响过的方法,否则时间长了或者其余人debug代码时候可能会对一些输出信息感到困惑。
  • 5.若是非无可奈何,尽可能少用方法交换
    • 虽然方法交换可让咱们高效地解决问题,可是若是处理很差,可能会致使一些莫名其妙的bug。

典型坑点-交换方法主动调用load

第一个坑点比较简单,就是咱们在load中交换完方法后,不作处理的话,若是再去调用load,方法IMP会又被交换回来,致使交换不成功。函数

解决的方法也比较简单,在上述的注意事项1中已经说过,使用单例模式来交换方法,保证方法的交换只执行一次post

典型坑点-子类无实现,交换父类方法

坑点例子

咱们能够经过一个例子来实现,建立父类LGPerson,子类LGStudent和分类LGStudent+LG来进行方法的交换。学习

@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}
@end
**************************************************************
@interface LGStudent : LGPerson

@end

@implementation LGStudent

@end
**************************************************************
复制代码

首先进行普通的方法交换,并在VC里面正常调用,根据打印结果,能够发现咱们在子类交换了父类的方法后,没有产生崩溃,而且子类的分类中交换的方法也正常执行了

@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

- (void)lg_studentInstanceMethod{
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}

@end

*******************************调用*******************************
- (void)viewDidLoad {
    [super viewDidLoad];
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
}
*******************************打印结果*******************************
2020-01-20 10:45:31.809408+0800 006---Method-Swizzling坑[81429:20470219] person对象方法:-[LGPerson personInstanceMethod]
2020-01-20 10:45:31.809568+0800 006---Method-Swizzling坑[81429:20470219] LGStudent分类添加的lg对象方法:-[LGStudent(LG) lg_studentInstanceMethod]

复制代码

可是,若是咱们在调用的时候,父类自己再调用一下这个方法的话,就会出现崩溃,缘由也比较清楚,就是子类将此方法交换了,父类并无交换后方法的IMP,因此会出现找不到方法的崩溃

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}

复制代码

解决方案

一句话总结就是:若是该方法本身没有,则先给本身添加要交换的方法。以后再父类原方法IMP指向交换的方法

在交换方法的时候,先尝试添加一下原方法到类中,并将IMP指向交换的方法

  • 若是成功了,说明该类以前没有,那么须要替换父类原方法的IMP到交换的方法中,这样就行程了上面图中的闭环
  • 若是没有成功,说明该类中自己就有此方法,那么直接进行交换便可
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  lg_studentInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加
    // ✅对应关系:personInstanceMethod(sel) - lg_studentInstanceMethod(imp)
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
    if (success) {// 本身没有 - 交换 - 没有父类进行处理 (重写一个)
        //✅lg_studentInstanceMethod (swizzledSEL) - personInstanceMethod(imp)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 本身有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
复制代码

典型坑点-方法只有声明,没有实现

仍是使用上述例子,若是LGStudent有一个方法- (void)helloword只有生命,没有实现。

就算咱们使用了上述的解决方法,添加了方法,可是因为原方法找不到,为nil。因此会形成死循环调用

咱们能够经过判断原方法是否存在,并添加一个一个空实现来解决这个坑点。以后再进行判断原方法进行交换,这样就能完美解决了

具体代码以下

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {
        ✅// 在oriMethod为nil时,替换后将swizzledSEL复制一个不作任何事的空实现,代码以下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    ✅// 通常交换方法: 交换本身有的方法 -- 走下面 由于本身有意味添加方法失败
    ✅// 交换本身没有实现的方法:
    ✅//   首先第一步:会先尝试给本身添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    ✅//   而后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}
复制代码

Method Swizzling常见应用

如下swizzling方法的具体封装,和上述代码中同样

无侵入埋点

在 iOS 开发中最多见的三种埋点,就是对页面进入次数、页面停留时间、点击事件的埋点。这些均可以经过Method Swizzling来实现。

下面的例子中,咱们经过交换UIViewControllerviewWillAppearviewWillDisappear的方法,来实现了进入界面和退出界面的统计,并记录了相关的类名,经过映射的关系,就能够清楚的知道用户的行为了

@implementation UIViewController (logger)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ✅// 经过 @selector 得到被替换和替换方法的 SEL,做为 SMHook:hookClass:fromeSelector:toSelector 的参数传入 
        SEL fromSelectorAppear = @selector(viewWillAppear:);
        SEL toSelectorAppear = @selector(hook_viewWillAppear:);
        [SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
        
        SEL fromSelectorDisappear = @selector(viewWillDisappear:);
        SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
        
        [SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
    });
}

- (void)hook_viewWillAppear:(BOOL)animated {
    ✅// 先执行插入代码,再执行原 viewWillAppear 方法
    [self insertToViewWillAppear];
    [self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
    ✅// 执行插入代码,再执行原 viewWillDisappear 方法
    [self insertToViewWillDisappear];
    [self hook_viewWillDisappear:animated];
}

- (void)insertToViewWillAppear {
    ✅// 在 ViewWillAppear 时进行日志的埋点
    [[[[SMLogger create]
       message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]]
      classify:ProjectClassifyOperation]
     save];
}
- (void)insertToViewWillDisappear {
    ✅// 在 ViewWillDisappear 时进行日志的埋点
    [[[[SMLogger create]
       message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
      classify:ProjectClassifyOperation]
     save];
}
@end
复制代码

那么点击方法,咱们也能够经过运行时方法替换的方式进行无侵入埋点。

这里最主要的工做是,找到这个点击事件的方法 sendAction:to:forEvent:,而后在 +load() 方法替换成为你定义的方法。完整代码实现以下:

UIViewController生命周期埋点不一样的是,UIButton在一个视图类中可能有多个不一样的继承类,相同 UIButton的子类在不一样视图类的埋点也要区别开。因此,咱们须要经过 “action 选择器名 NSStringFromSelector(action)” +“视图类名 NSStringFromClass([target class])”组合成一个惟一的标识,来进行埋点记录

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ✅// 经过 @selector 得到被替换和替换方法的 SEL,做为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
        SEL fromSelector = @selector(sendAction:to:forEvent:);
        SEL toSelector = @selector(hook_sendAction:to:forEvent:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [self insertToSendAction:action to:target forEvent:event];
    [self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    ✅// 日志记录
    if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
        NSString *actionString = NSStringFromSelector(action);
        NSString *targetName = NSStringFromClass([target class]);
        [[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save];
    }
}
复制代码

除了 UIViewControllerUIButton 控件之外,Cocoa 框架的其余控件均可以使用这种方法来进行无侵入埋点。以 Cocoa 框架中最复杂的 UITableView 控件为例,你可使用 hook setDelegate 方法来实现无侵入埋点。另外,对于 Cocoa 框架中的手势事件(Gesture Event),咱们也能够经过 hook initWithTarget:action:方法来实现无侵入埋点。

防止数组,字典等越界崩溃

这个例子我相信平时在开发中,你们都用到过,由于数组越界等是最容易形成crash的一种方式,并且通常崩溃起来比较严重,因此咱们必定要避免的

在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。因此若是想对NSArray进行Swizzling,必须获取到其真身进行Swizzling,直接对NSArray进行操做是无效的。这是由于Method Swizzling对NSArray这些的类簇是不起做用的。

由于这些类簇类,实际上是一种抽象工厂的设计模式。抽象工厂内部有不少其它继承自当前类的子类,抽象工厂类会根据不一样状况,建立不一样的抽象对象来进行使用。例如咱们调用NSArrayobjectAtIndex:方法,这个类会在方法内部判断,内部建立不一样抽象类进行操做。

因此若是咱们对NSArray类进行Swizzling操做其实只是对父类进行了操做,在NSArray内部会建立其余子类来执行操做,真正执行Swizzling操做的并非NSArray自身,因此咱们应该对其“真身”进行操做。

下面列举了NSArrayNSDictionary本类的类名,能够经过Runtime函数取出本类:

下面是一个常见的例子

@implementation NSArray (CrashHandle)

✅// 若是下面代码不起做用,形成这个问题的缘由大多都是其调用了super load方法。在下面的load方法中,不该该调用父类的load方法。这样会致使方法交换无效
+ (void)load {
    Method originMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(wy_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

✅// 为了不和系统的方法冲突,我通常都会在swizzling方法前面加前缀
- (id)wy_objectAtIndex:(NSUInteger)index {
    ✅// 判断下标是否越界,若是越界就进入异常拦截
    if (self.count-1 < index) {
        @try {
            return [self cm_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 cm_objectAtIndex:index];
    }
}
**************************调用******************************
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 测试代码
    NSArray *array = @[@0, @1, @2, @3];
    [array objectAtIndex:3];
    //原本要奔溃的,可是没有,打印出了信息
    [array objectAtIndex:4];
}

复制代码

以上的两个例子,只是开发中经常使用的,还有不少其余的应用,就须要根据需求来不断调整了。这些都属于AOP面向切面编程的一个实际应用,Method Swizzling也是其在iOS开发中应用的最经常使用的一种AOP思想

参考

iOS runtime实战应用:Method Swizzling

iOS开发·runtime原理与实践

iOS开发高手课

相关文章
相关标签/搜索