1、AOP编程思想
1.1、AOP是什么
AOP(Aspect Oriented Programming)直译为面向切面编程。假设把应用程序想象成一个长方体,OOP就是纵向划分系统,把系统分成很多个业务模块,如用户管理,产品展示等模块;AOP横向划分系统,提取各个业务模块可能要重复操作的部分。
AOP编程的原理是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。AOP是OOP的一个补充。
1.2、AOP的使用场景
AOP最常见的使用场景:日志记录、性能统计、安全控制、事务处理、异常处理、调试等。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。
比如,最常见的用户习惯统计。最简单的思路是:在viewDidAppear
和viewDidDisappea
r两个方法中,分别调用对应的第三方接口来实现页面驻留时长统计。
但是这部分代码并不涉及页面逻辑,而且app中的统计事件代码会散布在各个viewcontrolle
r中,你可能会忘记自己添加的统计事件,也会不利于统计相关的修改。
这时候,我们就会想把类似通用的代码从业务逻辑中分离出去,整合到一块。
2、Method Swizzling
2.1、Method Swizzling是什么
在iOS中实现AOP编程思想的一种方式是Method Swizzling
。Method Swizzling
利用Runtime特性把一个方法的实现和另一个方法的实现进行替换,在程序运行时修改Dispatch Table
里SEL
和IMP
的映射关系。
通过 swizzling method 改变目标函数的 selector 所指向的实现,然后在新的实现中实现附加的操作,完成之后再调用原来的处理逻辑。
2.2、Method Swizzling的优势
- 继承:修改多,无法保证他人一定继承基类
- 类别:类别中重写方法会覆盖原来的实现,大多时候,重写一个方法是为了添加一些代码,而不是完全取代它。如果两个类别都实现了相同的方法,只有一个类别的方法会被调用到。
- AOP优势:减少切面业务的重复编码,减少与其他业务的耦合,把琐碎事务从主业务中分离。
2.3、实现方案
- swizzling method的实现原理这里不再说明,相关知识:Objective-C调用方法的原理(包括消息解析与转发)。
- 最常见的:在
+(void)load
中自己实现swizzling method的代码。 - Github 上有一个基于 swizzling method 的开源框架 Aspects:
Aspects
是NSObject
的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中有一些类叫类簇,比如:NSNumber
、NSArray
和NSMutableArray
等。
何为类簇,类簇其实是抽象工厂模式的实现,比如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
,并且子类的类别重写了load
,load
的调用顺序:父类、子类、子类类别。 - 如果有多个子类category都重写了
load
,每个子类category的load
都会被调用一次。 - 如果子类没有重写
load
,子类的默认load不会去调用父类的load,这里与正常的继承不一样。 - 可以根据
Compile Sources
的装载顺序,简单地判断各个类load
的调用顺序,但是有继承关系的类,顺序为父类的方法先调用。 - 一般来说,除了method swizzle,其他的逻辑尽量不要不要放在
load
中,该方法中的逻辑尽量简单。 - 这里补充说一下
initialize
这个方法:第一次给一个类发送消息,调用initialize
(与是否使用相关),只会调用一次,initialize
和load
的顺序没有固定先后;不能显示调用initialize
,即使子类没有重写该方法,子类的默认initialize
也会调用其父类的initialize
;主要用来对一些不方便在编译器初始化的对象赋值,编译时期没有runtime。 -
load
和initialize
内部都使用了锁,都是现成安全的。
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
源码的关键流程:
4.2、Aspect源码的简单介绍
-
AspectOptions
枚举类型,表示block执行的时机。 -
AspectsContainer
表示对象或者类的所有的 Aspects 整体情况。 -
AspectIdentifier
表示一个 Aspect 的具体内容,包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。 -
AspectInfo
表示一个 Aspect 执行环境,主要是 NSInvocation 信息。