iOS中Method swizzling的使用

一、Method Swizzling简介

  Method Swizzling被业内称为黑魔法、黑科技。字面意思是方法交换,其中交换的是方法的实现
具体点的来说,我们用@selector(方法选择器) 取出来的是一个方法的编号(指向方法的指针) ,用SEL类型表示;它所指向的是一个IMP(方法实现的指针) ,而我们交换的就是这个IMP,从而达到方法实现交换的效果。

  • @selector(方法名)

@selector()方法名称的描述,只取类方法的编号不记录具体的方法,具体的方法是 IMP,取出的结果是SEL类型。

(1)编译时,通过编译器指令@selector 来获取.

//定义一个类方法的指针,selector查找是当前类(包含子类)的方法
SEL aSelector = @selector(methodName);

(2)运行时,通过字符串来获取一个方法名 NSSelectorFromString

SEL aSelector = NSSelectorFromString(@"methodName");

Objective-C 数据结构中,存在一个name - selector 的映射表如图:

iOS中Method swizzling的使用_第1张图片
The Selector table

  方法以 selector 作为索引。selector的数据类型是SEL。虽然SEL 定义成 char*,我们可以把它想象成int。每个方法的名字对应一个唯一的 int 值。比如, 方法 addObject:可能 对应的是 12。 当寻找该方法时,使用的是 selector,而不是名字 @"addObject:"

在编译的时候,只要有方法的调用,编译器都会通过 selector 来查找,所以 (假设addObjectselector12)

[myObject addObject: param];

将会编译变成:

objc_msgSend(myObject, 12, param);

  这里,objc_msgSend()函数将会使用 myObjectisa 指针来找到 myObject 的类空间结构,并在类空间结构中查找 selector 12所对应的方法。如果没有找到,那么将使用指向父类的指针找到父类空间结构进行 selector 12 的查找。 如果仍然没有找到,就继续往父类的父类一 直找,直到找到为止, 如果到了根类NSObject中仍然找不到,将会抛出异常。

  • IMP:(Implementation缩写)

一个函数指针,保存了方法地址。
(1)它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。

(2)当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由IMP这个函数指针指向了这个方法实现的。
IMP的声明为:

typedef id(*IMP)(id, SEL,...);

IMP是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id。也就是说IMP是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在语言里面一样使用这个函数指针。

//Method 是一个类实例,里面的结构体有一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP  - 指向该方法的具体实现的函数指针。
typedef struct objc_method *Method;
typedef struct objc_ method {
    SEL method_name;    //方法名
    char *method_types; //方法类型
    IMP method_imp;    //具体方法实施的指针
};
//获取了这个实例方法类Mehtod
Method method = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc")); 
//通过实例方法类获取对应的地址IMP
IMP classResumeIMP = method_getImplementation(method); 

二、Method Swizzling原理

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

  • 每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists);方法列表(MethodLists)中保存selector的方法名和方法实现(IMP,指向Method实现的指针)的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
    iOS中Method swizzling的使用_第2张图片
    MethodLists图
  • 开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用Method Swizzling来替换selector对应IMP后的方法列表示意图。
    iOS中Method swizzling的使用_第3张图片
    hook后的MethodLists示意图

三、Method Swizzling的具体用途AOP(面向切面编程)

  举个例子,比如说:在所有页面添加统计功能,也就是用户进入这个页面就统计一次。

方案:

(1) 手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…
上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

(2)继承
我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

(3)Category
我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

(4)Method Swizzling
我们可以使用苹果的“黑魔法”Method SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

Method Swizzling的常见用途:

  1. 页面统计(AOP)、NSMutableArrayinsert等插入nilhook:給全局图片名称添加前缀,分类中为已有的属性或者方法添加钩子(增加一段代码)。
  2. 用于记录或者存储,比方说记录ViewController进入次数、Btn的点击事件、ViewController的停留时间等等。 可以通过Runtime获取到具体ViewControllerBtn信息,然后传给服务器。
  3. 添加需要而系统没提供的方法,比方说修改Statusbar颜色。用于轻量化、模块化处理。
  4. Method Swizzle动态给指定的方法添加代码,以解决Cross-cutting concern的编程方式叫做Aspect Oriented Programming,将逻辑处理和事件记录的代码解耦。
  5. AOP可以把琐碎的事务从主逻辑中分离出来,作为单独的模块,它是对面向对象编程模式的一种补充。
  6. 比较好的AOP库,封装了runtimeMethod Swizzling这些黑科技,该库只有两个API
+ (id)aspect_hookSelector:(SEL)selector
                                  withOptions:(AspectOptions)options
                                   usingBlock:(id)block
                                        error:(NSError **)error;
      - (id)aspect_hookSelector:(SEL)selector
                                  withOptions:(AspectOptions)options
                                   usingBlock:(id)block
                                        error:(NSError **)error;

注意要点

  • swizzling 应该只在 +load 中完成。

  • swizzling 应该只在 dispatch_once 中完成。由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatchdispatch_once 满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

  • 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的。

  • xxx_viewWillAppear:方法中[self xxx_viewWillAppear:animated];代码并不会造成死循环,因为Method Swizzling之后,调用[self xxx_viewWillAppear:animated];实际执行的代码已经是原来viewWillAppear中的代码了。

(1) +load的执行时机:+load 方法会在加载类的时候就被调用,也就是iOS 应用启动的时候,就会加载所有的类,main函数之前,就会调用每个类的 +load 方法

(2) 子类的+load方法会在它的所有父类(不包括父类分类中的+load)的+load方法执行之后执行
分类的+load方法会在所有的主类的+load方法执行之后执行

(3) 不同的类之间的+load方法的调用顺序是不确定的

(4) + initialize:方法类似一个懒加载,initialize是在类或者其子类的第一个方法被调用前调用,且默认只加载一次;+ initialize的调用发生在+init 方法之前。

+load+ initialize差异比较

+(void)load +(void)initialize
执行时机 在main函数之前执行 在类的方法被第一次调用时执行
若自身未定义,是否沿用父类的方法?
类别中的定义 全都执行,但后于类中的方法 覆盖类中的方法,只执行一次

swizzling代码如下

NSObject+Swizzling.m

#import "NSObject+Swizzling.h"
#import 

@implementation NSObject (Swizzling)

/**
 交换两个对象方法的实现
 
 @param srcClass 被替换方法的类
 @param srcSel 被替换的方法编号
 @param swizzledSel 用于替换的方法编号
 */
+ (void)ff_swizzleInstanceMethodWithSrcClass:(Class)srcClass
                                      srcSel:(SEL)srcSel
                                 swizzledSel:(SEL)swizzledSel{
    
    Method srcMethod = class_getInstanceMethod(srcClass, srcSel);
    Method swizzledMethod = class_getInstanceMethod(srcClass, swizzledSel);
    if (!srcClass || !srcMethod || !swizzledMethod) return;
    
    //加一层保护措施,如果添加成功,则表示该方法不存在于本类,而是存在于父类中,不能交换父类的方法,否则父类的对象调用该方法会crash;添加失败则表示本类存在该方法
    BOOL addMethod = class_addMethod(srcClass, srcSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (addMethod){
        //添加方法实现IMP成功后,再将原有的实现替换到swizzledMethod方法上,从而实现方法的交换,并且未影响到父类方法的实现
        class_replaceMethod(srcClass, swizzledSel, method_getImplementation(srcMethod), method_getTypeEncoding(srcMethod));
    }else{
        //添加失败,调用交互两个方法的实现
        method_exchangeImplementations(srcMethod, swizzledMethod);
    }
}

@end

交换例子

#import "UIViewController+swizzling.h"
#import 
#import "NSObject+Swizzling.h"

@implementation UIViewController (swizzling)

+ (void)load {
    [super load];
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // 原方法名和替换方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        [NSObject ff_swizzleInstanceMethodWithSrcClass:class srcSel:originalSelector swizzledSel:swizzledSelector];
    });
}

/**
 页面出现的时候会进入到这里实现,即使在子类重写了viewDidAppear:方法,
 那么在调用[super viewDidAppear:animated]的时候还是会进入这里。
 */
- (void)swizzle_viewDidAppear:(BOOL)animated {
    //cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例一样。
    NSLog(@"%@ --- %@ (IMP = UIViewController swizzle_viewDidAppear)",self, NSStringFromSelector(_cmd));
    //这里面调用自己在method swizzle交换了方法实现后就不会出现循环了
    //swizzle_viewDidAppear:方法的实现其实是调用UIViewController的viewDidAppear的原生实现方法
    [self swizzle_viewDidAppear:animated];
}

@end

这里只是简单介绍了一下正确swizzle的思路,还有很多不完善的地方,推荐大家去看RSSwizzle,swizzle思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。

你可能感兴趣的:(iOS中Method swizzling的使用)