前言
首先我们了解几个概念,什么是切面编程?切面编程的实际应用?
切面编程(AOP):什么是切面?举个栗子:切一根萝卜,切成三段;那这根萝卜就好比是一个功能模块,一段一段的萝卜好比是不同类实例化的对象,一根切成仨段的时候就是OOP(以VC为主组成);要是多个萝卜一起切就是AOP(以Modle组成项目模块)一刀好几段,切面的萝卜就像相同类实例化的对象;
为什么要用到切面编程呢? 比如我们有一个需求,需要在不修改原函数情况下,在函数执行前添加一些方法;这就需要用到切面编程思想;饿了么有出一款类似新框架,Stinger暂不讨论,本章介绍一下Aspect,是切面编程的实际应用,详细介绍从源码和应用层。
Aspects原理解析以及实践
Aspects概念
Aspects源码地址:Aspects源码
Aspects就是利用runtime特性进行method swizzle,hook住对应的方法进行添加或修改等操作;一个封装好的轮子。
Aspects原理
1.基本原理
知道了Aspects 是利用runtime的特性,我们来了解一下基本的runtime消息转发机制;
1.首先由objc_msgSend()发起消息的汇编查找流程;
2.然后会调用lookUpImpOrForward() 来查找IMP指针,如果找到了则调用函数的IMP,进行方法访问;否则没找到IMP方法,下一步则进入消息转发机制。
3.第一层转发:由
receiveInstanceMethod()
、receiveClassMethod
方法进行接收,这层是方法层面的,可以通过开发者进行动态方法添加进行补救;4.第二层转发:如果上层没找到
SEL
,则进入第二层转发,调用forwardingTargetForSelector()
,这层是类级别转发,可以把调用转到另一个类对象,调用另一个类的相同方法。5.第三层转发:如果第二次没找到,返回nil;则进入最后一次转发,这层会调用
methodSignatureForSelector()
,forwardInvocation()
;这层是动态方法签名,和动态指定调用方法的target,可以从这层进行消息方法的替换添加等操作;6.若上述都没找到SEL,则会cash抛出异常;
基本原理分析:
从对外暴露的核心API分析:
/**
作用域:针对所有对象生效
selector: 需要hook的方法
options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)
block: 需要在selector前后插入执行的代码块
error: 错误信息
*/
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/**
作用域:针对当前对象生效
*/
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
从上面介绍的消息转发机制可以看出,Aspects
就是利用了转发机制的第三层forwardInvocation()
,Hook住其方法,然后利用切面编程思维来动态调用block。下面来细分其设计:
(1)A类的方法.m被添加切面方法;
(2)新建一个B类继承于A类,并且hook住B类forwardInvocation()
方法拦截转发,让forwardInvocation()
的IMP指向事先准备好的__ASPECTS_ARE_BEING_CALLED__
(后面简称为ABC
函数),block方法执行就在ABC
函数中。
(3)把类A的对象isa指针指向B,这样就把消息转发处理放在B类中了。酷似KVO
机制,同时会更改class的IMP指向,把他指向A类的class方法,当外接调用class
时还是获取A类,并没发现B类中间存在。
(4)对于A类方法m,B类会直接把方法m的IMP
指向_objc_msgForward()
方法,这样当调用方法m时就会走消息转发流程,触发ABC
函数
2.详细解析
我们先从实例类入口
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
__block AspectIdentifier *identifier = nil;
// 添加自旋锁,block内容的执行时互斥的
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// 获取容器,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
// 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
可以清晰看出执行入口实际上是调用aspect_add(self, selector, options, block, error)
,这个方法里面有加一些线程安全的操作,接下来我们来分析一下:
(一)过滤拦截
使用aspect_isSelectorAllowedAndTrack()
方法来过滤方法拦截,下面来看看精简版源码:
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{ // 初始化黑名单列表,有些方法时禁止hook的
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
// 第一步:检查是否在黑名单内
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
...
return NO;
}
// 第二步: dealloc方法只能在调用前插入
AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
...
return NO;
}
// 第三步:检查类是否存在这个方法
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
...
return NO;
}
// 第四步:如果是类而非实例(这个是类,不是类方法,是指hook的作用域对所有对象都生效),则在整个类即继承链中,同一个方法只能被hook一次,即对于所有实例对象都生效的操作,整个继承链中只能被hook一次
if (class_isMetaClass(object_getClass(self))) {
...
} else {
return YES;
}
return YES;
}
从上面注解,以及代码执行可知:
(1)不可hookretain
、release
、autorelease
、forwardInvocation:
,在系统内的一些方法,也是在黑名单类的方法,这边不做过多解释;
(2)可以hookdealloc
,但只能在dealloc执行前,这都是为了程序安全性设置;
(3)检查方法是否存在,否则不进行hook
(4)Aspects对于hook的生效作用域做了区分:所有实例对象&某个具体实例对象。对于所有实例对象在整个继承链中,同一个方法只能被hook一次,这么做的目的是为了规避循环调用的问题(详情可以了解下supper
关键字)
(二)存储切面信息
一些切面信息主要存储在AspectIdentifier
,AspectsContainer
这两类中,关键操作如下(注解已详尽):
(1)创建AspectContainer
类,有则取创建好的,没有则新建一个;
(2)创建一个标识对象identifier
,来存储原方法的方法信息,切面时机,切面block等 ;
(3)把标识对象identifier
添加到容器中
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
...
// 获取容器对象,主要用来存储当前类或对象所有的切面信息,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
// 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
// 把identifier添加到容器中
[aspectContainer addAspect:identifier withOptions:options];
...
}
return identifier;
}
(三)创建中间类
这一步为关键中的一步,创建隐性中间类对原有类进行方法hook。酷似KVO的机制,其特点为:1.可以做到只对当一对象进行hook,2.避免原有类的入侵;
这一步骤中的关键操作为:
(1) 检查是否已创建中间类,有则返回,无则新建;
(2) 如果对象是类对象,则不需要创建中间类,并把这个类存储在swizzleClasses
集合中,同时标记这个类为已被hook了;
(3) 如若存在KVO的情况,系统已经帮我们创建好了中间类,则取出直接使用;
(4)对于不存在kvo且是实例对象的,则单独创建一个继承当前类的中间类midcls
,并hook它的forwardInvocation:
方法,并把当前对象的isa指针指向midcls
,这样就做到了hook操作只针对当前对象有效,因为其他对象的isa指针指向的还是原有类。
static Class aspect_hookClass(NSObject *self, NSError **error) {
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// Already subclassed; 判断是否有中间类
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
}else if (statedClass != baseClass) {
// Probably a KVO class. Swizzle in place. Also swizzle meta classes in place.
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
// hook forwardInvocation方法
aspect_swizzleForwardInvocation(subclass);
// hook class方法,把子类的class方法的IMP指向父类,这样外界并不知道内部创建了子类
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 把当前对象的isa指向子类,类似kvo的用法
object_setClass(self, subclass);
return subclass;
}
下面来看看如何替换拦截:
// hook forwardInvocation方法,用来拦截消息的发送
static void aspect_swizzleForwardInvocation(Class klass) {
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
从上面源码可以看出,其主要功能就是把forwardInvocation()
替换成ASPECTS_ARE_BEING_CALLED
,对于__ASPECTS_ARE_BEING_CALLED__
方法是Aspects
的核心操作,主要就是做消息的调用和分发,控制方法的调用的时机,下面会详细介绍。
(四)关键类结构
AspectOptions 枚举
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// 原有方法调用前执行 (default)
AspectPositionInstead = 1, /// 替换原有方法
AspectPositionBefore = 2, /// 原有方法调用后执行
AspectOptionAutomaticRemoval = 1 << 3 /// 执行完之后就恢复切面操作,即撤销hook
};
通过源码注解可以看出,这个枚举是用来定义切面的时机;即有调用前,替换时,执行后,只执行一次(调用完一次就删除切面逻辑)
AspectIdentifier 类
@interface AspectIdentifier : NSObject
...(省略不关键)
@property (nonatomic, assign) SEL selector; // 原来方法的SEL
@property (nonatomic, strong) id block; // 保存要执行的切面block,即原方法执行前后要调用的方法
@property (nonatomic, strong) NSMethodSignature *blockSignature; // block的方法签名
@property (nonatomic, weak) id object; // target,即保存当前对象
@property (nonatomic, assign) AspectOptions options; // 是个枚举,表示切面执行时机,上面已经有介绍
@end
从这个类来看,简单理解成一个Model,主要用来存储hook方法的相关信息。如切面方法,切面bolck,切面时机等;
AspectsContainer类
@interface AspectsContainer : NSObject
...
@property (atomic, copy) NSArray <AspectIdentifier *>*beforeAspects; // 存储原方法调用前要执行的操作
@property (atomic, copy) NSArray <AspectIdentifier *>*insteadAspects;// 存储替换原方法的操作
@property (atomic, copy) NSArray <AspectIdentifier *>*afterAspects;// 存储原方法调用后要执行的操作
@end
这是一个容器类,以关联对象的形式储存储存在当前对象或者类对象中,主要储存当前对象或者类对象的切面信息。
(五)自动触发消息转发机制
Aspect的核心原理就是消息转发机制,那有必要说一下如何自动触发自动转发机制的;
runtime中有个方法是_objc_msgForward
,直接调用可以触发消息转发机制,JSPatch
也是利用这个机制;
如果我们需要hook住方法t1
,那么把t1
的IMP
指针直接指向_objc_msgForward
,那么t1
方法调用直接触发消息自动转发机制了,实现如下:
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
...
// We use forwardInvocation to hook in. 把函数的调用直接触发转发函数,转发函数已经被hook,所以在转发函数时进行block的调用
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
}
}
(六)核心转发函数处理
从上面处理OK后,接下来我们如何触发切面的Block,下面我们来梳理一下:
(1)方法t1
的IMP
指向_objc_msgForward
,调用t1
方法则会触发自动消息转发机制;
(2)第二部是替换forwardInvocation()
,替换成__ASPECTS_ARE_BEING_CALLED__
方法,消息自动转发的时候调用的其实就是__ASPECTS_ARE_BEING_CALLED__
;
从上面分析可以看出,直接调用t1
方法触发消息自动转发机制,其实就是触发了__ASPECTS_ARE_BEING_CALLED__
方法;而__ASPECTS_ARE_BEING_CALLED__
就是处理切面处理原有Block和触发时机的方法;下面我们来详细分析它的方法:
- 根据调用的selector,获取容器对象
AspectContainer
,里面储存了这个类和对象的所有切面信息; - 然后获取当前参数信息的
AspectInfo
,用于传递参数信息; - 首先触发函数调用前的block,存储在容器的
beforeAspects
对象中; - 下面则是替换所有原有对象函数的block,即是
insteadAspects
不为空,则触发发它,若不存在则还是调用原有函数 - 触发函数调用后的block,存在在容器的
afterAspects
对象中;
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
// Before hooks. 方法执行之前调用
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks. 替换原方法或者调用原方法
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks. 方法执行之后调用
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
...
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
总结
Aspects的核心原理是利用了消息转发机制,通过替换消息转发方法来实现切面的分发调用,这个思想很巧妙而且应用很广泛,很多三方库都利用了这个原理,值得学习;
目前这个库已经很长时间没有维护了,原子操作的支持使用的还是自旋锁,目前这种锁已经不安全了;
另外使用这个库是需要注意类似原理的其他框架,可能会有冲突,如JSPatch
,不过JSPatch
已经被封杀了,但类似需求有很多
参考文章:Aspects深度解析