Method Swizzle

1、AOP编程思想

1.1、AOP是什么

AOP(Aspect Oriented Programming)直译为面向切面编程。假设把应用程序想象成一个长方体,OOP就是纵向划分系统,把系统分成很多个业务模块,如用户管理,产品展示等模块;AOP横向划分系统,提取各个业务模块可能要重复操作的部分。
AOP编程的原理是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。AOP是OOP的一个补充。

1.2、AOP的使用场景

AOP最常见的使用场景:日志记录、性能统计、安全控制、事务处理、异常处理、调试等。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。

比如,最常见的用户习惯统计。最简单的思路是:在viewDidAppearviewDidDisappear两个方法中,分别调用对应的第三方接口来实现页面驻留时长统计。
但是这部分代码并不涉及页面逻辑,而且app中的统计事件代码会散布在各个viewcontroller中,你可能会忘记自己添加的统计事件,也会不利于统计相关的修改。
这时候,我们就会想把类似通用的代码从业务逻辑中分离出去,整合到一块。

2、Method Swizzling

2.1、Method Swizzling是什么

在iOS中实现AOP编程思想的一种方式是Method SwizzlingMethod Swizzling利用Runtime特性把一个方法的实现和另一个方法的实现进行替换,在程序运行时修改Dispatch TableSELIMP的映射关系。
通过 swizzling method 改变目标函数的 selector 所指向的实现,然后在新的实现中实现附加的操作,完成之后再调用原来的处理逻辑。

2.2、Method Swizzling的优势

  • 继承:修改多,无法保证他人一定继承基类
  • 类别:类别中重写方法会覆盖原来的实现,大多时候,重写一个方法是为了添加一些代码,而不是完全取代它。如果两个类别都实现了相同的方法,只有一个类别的方法会被调用到。
  • AOP优势:减少切面业务的重复编码,减少与其他业务的耦合,把琐碎事务从主业务中分离。

2.3、实现方案

  • swizzling method的实现原理这里不再说明,相关知识:Objective-C调用方法的原理(包括消息解析与转发)。
  • 最常见的:在+(void)load中自己实现swizzling method的代码。
  • Github 上有一个基于 swizzling method 的开源框架 Aspects:AspectsNSObject的Category,任何NSObject类要hook实例方法或者类方法,只需调用Aspects提供的一个简单方法,就可以实现hook,并且可以安排附加代码的执行时机。

3、常见的实现

3.1、普通类的swizzling

关键代码与注释:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod([self class], @selector(testMethod));
        Method swizzleMethod = class_getInstanceMethod([self class
                                                        ],@selector(swizzle_testMethod));
/**************第一种替换方式**********/
        // 如果当前类没有实现名为originalSelector的方法,originalMethod如果不为空,拿到的则是其父类的名为originalSelector的方法的IMP,这里先给当前类新增originalSelector的IMP,对应的IMP是swizzleMethod中的IMP
        BOOL didAddMethod = class_addMethod([self class], @selector(testMethod), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        if (didAddMethod) {// 如果当前类已经有名为originalSelector的实现,则返回NO
            // 将swizzleSelector的IMP替换成父类originalSelector的IMP。
            class_replaceMethod([self class], @selector(swizzle_testMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            // 如果当前类都有实现待替换的两个方法,直接交换
          //   这两个参数没有先后顺序
            method_exchangeImplementations(originalMethod, swizzleMethod);
        }
 /**************第二种替换方式,有问题,不要用**********/
        // class_replaceMethod这个方法的效果:
        // 如果在当前类有对应的实现,执行method_setImplementation
        // 如果当前类没有对应的实现,执行class_addMethod
        // 这儿的先后顺序是有讲究的,应该优先把新的方法指定原IMP,再修改原有的方法的IMP
        // 如果先执行原方法指向新的IMP,那么在执行完瞬间方法被调用容易引发死循环,这样的顺序至少保证如果没有hook成功,也只是调用原来的实现,不会出现死循环
/////error///////// 这段代码有问题,hook自己写的testMethod,无法输出附加的代码
/////error/////////  而且如果父类同时也写了一样的swizzle的代码,hook会被抵消
//        class_replaceMethod([self class], @selector(swizzle_testMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
//        class_replaceMethod([self class], @selector(testMethod), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    });
}

3.2、类簇类的swizzling

在Objective-C中有一些类叫类簇,比如:NSNumberNSArrayNSMutableArray等。
何为类簇,类簇其实是抽象工厂模式的实现,比如NSArray并不是一个具体类,其实它是一个抽象类,它对外提供了统一访问的接口,它的真实的类型会根据具体情况创建。
所以,我们在给类簇类做method swizzle并不能用[self class]获得其真实的类名。
比如,我们可以通过以下代码来得到NSMutableArray的真实类型:

object_getClass([[NSMutableArray alloc] init]);
objc_getClass("__NSArrayM");

上面代码中__NSArrayM是NSMutableArray的真实类名;
对应的一些常见类簇的真实类名:

真实类名
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionary
NSMutableDictionaary __NSDictionaryM

3.3、为什么在+load()方法中实现swizzling

  • load在源文件被程序装载时自动调用(并不能手动调用,与该类是否被使用无关),在main()之前执行。
  • 当子类重写了load,并且子类的类别重写了loadload的调用顺序:父类、子类、子类类别。
  • 如果有多个子类category都重写了load,每个子类category的load都会被调用一次。
  • 如果子类没有重写load,子类的默认load不会去调用父类的load,这里与正常的继承不一样。
  • 可以根据Compile Sources的装载顺序,简单地判断各个类load的调用顺序,但是有继承关系的类,顺序为父类的方法先调用。
  • 一般来说,除了method swizzle,其他的逻辑尽量不要不要放在load中,该方法中的逻辑尽量简单。
  • 这里补充说一下initialize这个方法:第一次给一个类发送消息,调用initialize(与是否使用相关),只会调用一次,initializeload的顺序没有固定先后;不能显示调用initialize,即使子类没有重写该方法,子类的默认initialize也会调用其父类的initialize;主要用来对一些不方便在编译器初始化的对象赋值,编译时期没有runtime。
  • loadinitialize内部都使用了锁,都是现成安全的。

3.4、为什么swizzling的实现要放在dispatch_once

什么情况下,+load会被调用两次?

3.5、第三方jrswizzle

4、使用消息转发实现(分析Aspect源码)

4.1、Aspect源码的实现思路

对于待 hook 的 selector,将其指向 objc_msgForward / _objc_msgForward_stret;同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook 住 forwardInvocation 函数,使他指向自己的实现。

当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation 函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。

Aspect源码的关键流程:

Method Swizzle_第1张图片

4.2、Aspect源码的简单介绍

  • AspectOptions枚举类型,表示block执行的时机。
  • AspectsContainer表示对象或者类的所有的 Aspects 整体情况。
  • AspectIdentifier表示一个 Aspect 的具体内容,包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。
  • AspectInfo表示一个 Aspect 执行环境,主要是 NSInvocation 信息。

  • Glow:Method Swizzling 和 AOP 实践
  • iOS黑魔法-Method Swizzling
  • Method Swizzling的各种姿势
  • 细说OC中的load和initialize方法
  • WeRead:面向切面编程之 Aspects 源码解析及应用

你可能感兴趣的:(Method Swizzle)