Objective-C 之 Method Swizzling

141 阅读6分钟

Method Swizzling 简介

Method Swizzling 用于改变一个已经存在的selector实现。我们可以在程序运行时,通过改变selector所在Class(类)的 methodLists(方法列表)的映射从而改变方法的调用。其实质就是交换两个方法的IMP(方法实现)。

Method (方法) :

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};

MethodSEL(方法声明) 和 IMP(方法实现)关联起来(键值对:SEL: IMP)。当通过消息来调用类中某个函数时,首先会根据SEL,在对象所在Class(类)的 methodLists(方法列表)中查询,然后查找到对应的方法实现IMP,执行方法。

Method swizzling 的作用,便是修改了Method(方法)中SELIMP的对应关系(IMP函数指针指向),使得不同 Method(方法)中的键值对发生了交换。比如交换前两个键值对分别为 SEL A : IMP ASEL B : IMP B,交换之后就变为了 SEL A : IMP BSEL B : IMP A。从而实现调用SEL A方法名,但实际在运行时执行的是IMP B

简单使用

#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
    
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self SwizzlingMethod];
    [self originalFunction];
    [self swizzledFunction];
}

// 交换 原方法 和 替换方法 的方法实现
- (void)SwizzlingMethod {
   
    Class class = [self class];
   
    SEL originalSelector = @selector(originalFunction);
    SEL swizzledSelector = @selector(swizzledFunction);
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 调用交换两个方法的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end

应用场景

新导入的功能,我们需要对所有的新界面做出统一的皮肤修改,例如背景色设置。这时候我们只有导入功能的framework,没办法到具体代码中添加新方法,更不能试图子类化方式来统一修改。更遗憾的是, framework中并没有公开这些需要改变界面的类的头文件。所以也无法通过category的方式来做出改变。 但我们可以在运行时中,窥探出framework中的究竟。通过找到framework中,viewcontroller的基类,例如XXXBase开头的XXXBaseViewcontroller,然后利用method swizzling方式,替换viewdidappear方法的实现,加入我们队UI的额外修改。从而实现在运行时,对新功能界面背景色的修改。

除此之外,很多情况,例如对viewcontroller生命周期函数,统一添加log;对某些方法添加统一的提前判断,等等,都可以通过method swizzling方式完美解决。总而言之,method swizzling就像是一把手术刀,在原有的应用体系中切出一个横面,直达目的,做出修改。

替换UIViewcontroller中的viewDidAppear:方法,在该类的分类中添加 Method Swizzling 交换方法,用普通方式。如下:

#import "UIViewController+swizzling.h"

@implementation UIViewController (swizzling)

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
  	//当前类
    Class class = [self class];
    // 原方法选择器  
    SEL originSEL = @selector(viewDidAppear:);
  	// 替换方原方法选择器  
    SEL swizzleSEL = @selector(swizzleViewDidAppear:);
  
    //根据 SEL,从 class 的结构体 methodLists 中取得对应的 Method,
  	//实例方法用 class_getInstanceMethod(),类方法用 class_getClassMethod()。
  	//从当前的 Class 中寻找对应方法名的实现,若没有则向上遍历父类中查找。若父类中也没有,则返回 NULL。
    Method originMethod = class_getInstanceMethod(class, originSEL);
    Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
  
  	//如果当前类没有 原方法,说明在从父类继承过来的方法实现,
  	//需要在当前类中添加一个 Method 方法 class_addMethod()
  	//如果该 class(不包含父类)已包含具有该名称的方法实现,返回 NO 否则返回YES。
    BOOL didAddMethod = class_addMethod(class, 									//当前类
                                        originSEL, 								//originSEL
                                        method_getImplementation(swizzleMethod),//swizzleMethod 的 IMP
                                        method_getTypeEncoding(swizzleMethod)	//swizzleMethod 的参数类型
                                       );
    if (didAddMethod) { // YES originMethod 添加成功
      	//此时,originMethod 的 SEL 对应 swizzleMethod 的 IMP,只需将 swizzleMethod SEL 对应 originMethod IMP。
      	//替换 swizzleMethod IMP 为 originMethod IMP
      
      	//class_replaceMethod() 替换方法 IMP。
      	//此函数有两种不同的行为方式:
      	//1.如果方法尚不存在,则将添加该方法,如同调用了class_addMethod(),类型指定的类型编码按给定方式使用。
      	//2.如果方法确实存在,则其IMP将被替换,就像调用method_setImplementation(),类型指定的类型编码被忽略。
        class_replaceMethod(class,																 //当前类
                            swizzleSEL,														 //swizzleSEL
                            method_getImplementation(originMethod),//originMethod 的 IMP
                            method_getTypeEncoding(originMethod)	 //originMethod 的参数类型
                           );	
    } else {// NO 类已包含具有该名称的方法实现
        //class中本来就含有 originMethod,只需要交换originalMethod 和swizzledMethod 的实现。
      	//method_exchangeImplementations() 交换两个方法的 IMP。
        method_exchangeImplementations(originMethod,
                                       swizzleMethod
                                      );
    }
  });  
}

- (void)swizzleViewDidAppear:(BOOL)animated {
    [self swizzleViewDidAppear:animated];
}
@end


`swizzleViewDidAppear:`中调用了`swizzleViewDidAppear:`不会死循环,因为`swizzleViewDidAppear:`和`viewDidAppear:`已经交换了实现方法,当`viewcontroller`调用`viewDidAppear:`时,走到了`swizzleViewDidAppear:`,在`swizzleViewDidAppear:`中只有调用[self swizzleViewDidAppear:animated],才能走到`viewDidAppear:`的实现方法中。

### 使用注意

**只在 `+load` 中执行 Method Swizzling。**

程序在启动的时候,会先加载所有的类,这时会调用每个类的 `+load` 方法。而且在整个程序运行周期只会调用一次(不包括外部显示调用)。

`+initialize` 方法的调用时机是在 第一次向该类发送第一个消息的时候才会被调用。如果该类只是引用,没有调用,则不会执行 `+initialize` 方法。

Method Swizzling 影响的是全局状态,`+load` 方法能保证在加载类的时候就进行交换,保证交换结果。而 `+initialize` 方法则不能保证这一点,有可能在使用的时候起不到交换方法的作用。

**Method Swizzling 在 `+load` 中执行时,不要调用 `[super load];`。**

如果在 `+ (void)load`方法中调用 `[super load]` 方法,就会导致父类的 `Method Swizzling` 被重复执行两次,而方法交换也被执行了两次,相当于互换了一次方法之后,第二次又换回去了,从而使得父类的 `Method Swizzling`失效。

**Method Swizzling 应该总是在 `dispatch_once` 中执行。**

Method Swizzling 不是原子操作,`dispatch_once` 可以保证即使在不同的线程中也能确保代码只执行一次。所以,我们应该总是在 `dispatch_once` 中执行 Method Swizzling 操作,保证方法替换只被执行一次。

**使用 Method Swizzling 后要记得调用原生方法的实现。**

在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。

**避免命名冲突和参数 `_cmd` 被篡改。**

避免命名冲突一个比较好的做法是为替换的方法加个前缀以区别原生方法。一定要确保调用了原生方法的所有地方不会因为自己交换了方法的实现而出现意料不到的结果。

避免方法命名冲突另一个更好的做法是使用函数指针,也就是上边提到的 **方案 B**,这种方案能有效避免方法命名冲突和参数 `_cmd` 被篡改。