Aspects深度解析-iOS面向切面编程
共 20476字,需浏览 41分钟
·
2021-07-07 15:08
作者丨monkery
来源丨码上work(codework88)
背景简述
在日常开发过程中是否有过这样的需求:不修改原来的函数,但是又想在函数的执行前后插入一些代码。这个方式就是面向切面(AOP),在iOS开发中比较知名的框架就是Aspects,而饿了么新出的Stinger框架先不讨论,Aspects的源码精炼巧妙,很值得学习深究,本文主要从源码和应用层面来介绍下
源码解析
先提出几个问题
带着问题去阅读更容易理解
Aspects实现的核心原理是什么
哪些方法不能被hook
hook的操作是否可以只对某个实例生效,对同一个类的其他实例不生效
block是如何被存储和调用的
基本原理
正常来讲想实现AOP,可以利用runtime的特性进行method swizzle,但Aspects就是造好的轮子,而且更好用,下面简述下Aspects的基本原理
runtime的消息转发机制
在OC中,所有的消息调用最后都会通过objc_msgSend()
方法进行访问
通过
objc_msgSend()
进行消息调用,为了加快执行速度,这个方法在runtime源码中是用汇编实现的然后调用
lookUpImpOrForward()
方法,返回值是个IMP
指针,如果查找到了调用函数的IMP
,则进行方法的访问如果没有查到对于方法的
IMP
指针,则进行消息转发机制第一层转发:会调用
resolveInstanceMethod:、resolveClassMethod:
,这次转发是方法级别的,开发者可以动态添加方法进行补救第二层转发:如果第一层转发返回
NO
,则会进行第二层转发,调用forwardingTargetForSelector:
,可以把调用转发到另一个对象,这是类级别的转发,调用另一个类的相同的方法第三层转发:如果第二层转发返回
nil
,则会进入这一层处理,这层会调用methodSignatureForSelector:、forwardInvocation:
,这次是完整的消息转发,因为你可以返回方法签名、动态指定调用方法的Target如果转发都失败,就会crash
Aspects的基本原理
对外暴露的核心API
1/**
2作用域:针对所有对象生效
3selector: 需要hook的方法
4options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)
5block: 需要在selector前后插入执行的代码块
6error: 错误信息
7*/
8+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
9 withOptions:(AspectOptions)options
10 usingBlock:(id)block
11 error:(NSError **)error;
12/**
13作用域:针对当前对象生效
14*/
15- (id<AspectToken>)aspect_hookSelector:(SEL)selector
16 withOptions:(AspectOptions)options
17 usingBlock:(id)block
18 error:(NSError **)error;
上面介绍了消息的转发机制,而Aspects
就是利用了消息转发机制,通过hook第三层的转发方法forwardInvocation:
,然后根据切面的时机来动态调用block。接下来详细分析巧妙的设计
类A的方法m被添加切面方法
创建一个类A的子类B,并hook子类B的
forwardInvocation:
方法拦截消息转发,使forwardInvocation:
的IMP
指向事先准备好的ASPECTS_ARE_BEING_CALLED
函数(后面简称ABC
函数),block方法的执行就在ABC
函数中把类A的对象的isa指针指向B,这样就把消息的处理转发到类B上,类似
KVO
的机制,同时会更改class
方法的IMP,把它指向类A的class
方法,当外界调用class
时获取的还是类A,并不知道中间类B的存在对于方法m,类B会直接把方法m的
IMP
指向_objc_msgForward()
方法,这样当调用方法m时就会走消息转发流程,触发ABC
函数
详细分析
执行入口
1- (id<AspectToken>)aspect_hookSelector:(SEL)selector
2 withOptions:(AspectOptions)options
3 usingBlock:(id)block
4 error:(NSError **)error {
5 return aspect_add(self, selector, options, block, error);
6}
7
8static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
9 __block AspectIdentifier *identifier = nil;
10 // 添加自旋锁,block内容的执行时互斥的
11 aspect_performLocked(^{
12 if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
13 // 获取容器,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
14 AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
15 // 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
16 identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
17 if (identifier) {
18 [aspectContainer addAspect:identifier withOptions:options];
19
20 // Modify the class to allow message interception.
21 aspect_prepareClassAndHookSelector(self, selector, error);
22 }
23 }
24 });
25 return identifier;
26}
执行入口调用了aspect_add(self, selector, options, block, error)
方法,这个方法时线程安全的,接下来一步步解析具体做了什么
过滤拦截: aspect_isSelectorAllowedAndTrack
精简版的源码,已经添加了注释
1static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
2 static NSSet *disallowedSelectorList;
3 static dispatch_once_t pred;
4 dispatch_once(&pred, ^{ // 初始化黑名单列表,有些方法时禁止hook的
5 disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
6 });
7
8 // 第一步:检查是否在黑名单内
9 NSString *selectorName = NSStringFromSelector(selector);
10 if ([disallowedSelectorList containsObject:selectorName]) {
11 ...
12 return NO;
13 }
14
15 // 第二步:dealloc方法只能在调用前插入
16 AspectOptions position = options&AspectPositionFilter;
17 if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
18 ...
19 return NO;
20 }
21 // 第三步:检查类是否存在这个方法
22 if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
23 ...
24 return NO;
25 }
26
27 // 第四步:如果是类而非实例(这个是类,不是类方法,是指hook的作用域对所有对象都生效),则在整个类即继承链中,同一个方法只能被hook一次,即对于所有实例对象都生效的操作,整个继承链中只能被hook一次
28 if (class_isMetaClass(object_getClass(self))) {
29 ...
30 } else {
31 return YES;
32 }
33 return YES;
34}
不允许hook
retain
、release
、autorelease
、forwardInvocation:
,这些不多解释允许hook
dealloc
,但是只能在dealloc
执行前,这都是为了程序的安全性设置的检查这个方法是否存在,不存在则不能hook
Aspects对于hook的生效作用域做了区分:所有实例对象&某个具体实例对象。对于所有实例对象在整个继承链中,同一个方法只能被hook一次,这么做的目的是为了规避循环调用的问题(详情可以了解下
supper
关键字)
关键类结构
AspectOptions
是个枚举,用来定义切面的时机,即原有方法调用前、调用后、替换原有方法、只执行一次(调用完就删除切面逻辑)
1typedef NS_OPTIONS(NSUInteger, AspectOptions) {
2 AspectPositionAfter = 0, /// 原有方法调用前执行 (default)
3 AspectPositionInstead = 1, /// 替换原有方法
4 AspectPositionBefore = 2, /// 原有方法调用后执行
5
6 AspectOptionAutomaticRemoval = 1 << 3 /// 执行完之后就恢复切面操作,即撤销hook
7};
AspectIdentifier类
简单理解话就是一个存储model,主要用来存储hook方法的相关信息,如原有方法、切面block、切面时机等
1@interface AspectIdentifier : NSObject
2...其他省略
3@property (nonatomic, assign) SEL selector; // 原来方法的SEL
4@property (nonatomic, strong) id block; // 保存要执行的切面block,即原方法执行前后要调用的方法
5@property (nonatomic, strong) NSMethodSignature *blockSignature; // block的方法签名
6@property (nonatomic, weak) id object; // target,即保存当前对象
7@property (nonatomic, assign) AspectOptions options; // 是个枚举,表示切面执行时机,上面已经有介绍
8@end
AspectsContainer类
容器类,以关联对象的形式存储在当前类或对象中,主要用来存储当前类或对象所有的切面信息
1@interface AspectsContainer : NSObject
2...其他省略
3@property (atomic, copy) NSArray <AspectIdentifier *>*beforeAspects; // 存储原方法调用前要执行的操作
4@property (atomic, copy) NSArray <AspectIdentifier *>*insteadAspects;// 存储替换原方法的操作
5@property (atomic, copy) NSArray <AspectIdentifier *>*afterAspects;// 存储原方法调用后要执行的操作
6@end
存储切面信息
存储切面信息主要用到了上面介绍的AspectsContainer
、AspectIdentifier
这两个类,主要操作如下(注释写的已经很详细)
获取当前类的容器对象
aspectContainer
,如果没有则创建一个创建一个标识符对象
identifier
,用来存储原方法信息、block、切面时机等信息把标识符对象
identifier
添加到容器中
1static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
2 ...
3 // 获取容器对象,主要用来存储当前类或对象所有的切面信息,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
4 AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
5 // 创建标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
6 identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
7 if (identifier) {
8 // 把identifier添加到容器中
9 [aspectContainer addAspect:identifier withOptions:options];
10 ...
11 }
12 return identifier;
13}
创建中间类
这一步的操作类似kvo的机制,隐式的创建一个中间类,一:可以做到hook只对单一对象有效,二:避免了对原有类的侵入
这一步主要做了几个操作
如果已经存在中间类,则直接返回
如果是类对象,则不用创建中间类,并把这个类存储在
swizzledClasses
集合中,标记这个类已经被hook了如果存在kvo的情况,那么系统已经帮我们创建好了中间类,那就直接使用
对于不存在kvo且是实例对象的,则单独创建一个继承当前类的中间类
midcls
,并hook它的forwardInvocation:
方法,并把当前对象的isa指针指向midcls
,这样就做到了hook操作只针对当前对象有效,因为其他对象的isa指针指向的还是原有类
1static Class aspect_hookClass(NSObject *self, NSError **error) {
2 Class statedClass = self.class;
3 Class baseClass = object_getClass(self);
4 NSString *className = NSStringFromClass(baseClass);
5
6 // Already subclassed
7 if ([className hasSuffix:AspectsSubclassSuffix]) {
8 return baseClass;
9
10 // We swizzle a class object, not a single object.
11 }else if (class_isMetaClass(baseClass)) {
12 return aspect_swizzleClassInPlace((Class)self);
13 }else if (statedClass != baseClass) {
14 // Probably a KVO class. Swizzle in place. Also swizzle meta classes in place.
15 return aspect_swizzleClassInPlace(baseClass);
16 }
17
18 // Default case. Create dynamic subclass.
19 const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
20 Class subclass = objc_getClass(subclassName);
21
22 if (subclass == nil) {
23 subclass = objc_allocateClassPair(baseClass, subclassName, 0);
24 // hook forwardInvocation方法
25 aspect_swizzleForwardInvocation(subclass);
26 // hook class方法,把子类的class方法的IMP指向父类,这样外界并不知道内部创建了子类
27 aspect_hookedGetClass(subclass, statedClass);
28 aspect_hookedGetClass(object_getClass(subclass), statedClass);
29 objc_registerClassPair(subclass);
30 }
31 // 把当前对象的isa指向子类,类似kvo的用法
32 object_setClass(self, subclass);
33 return subclass;
34}
替换forwardInvocation:方法
从下面的代码可以看到,主要功能就是把当前类的forwardInvocation:
替换成__ASPECTS_ARE_BEING_CALLED__
,这样当触发消息转发的时候,就会调用__ASPECTS_ARE_BEING_CALLED__
方法
对于__ASPECTS_ARE_BEING_CALLED__
方法是Aspects
的核心操作,主要就是做消息的调用和分发,控制方法的调用的时机,下面会详细介绍
1// hook forwardInvocation方法,用来拦截消息的发送
2static void aspect_swizzleForwardInvocation(Class klass) {
3 // If there is no method, replace will act like class_addMethod.
4 IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
5 if (originalImplementation) {
6 class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
7 }
8 AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
9}
自动触发消息转发机制
Aspects
的核心原理是消息转发,那么必要出的就是怎么自动触发消息转发机制
runtime中有个方法_objc_msgForward
,直接调用可以触发消息转发机制,著名的JSPatch
框架也是利用了这个机制
假如要hook的方法叫m1
,那么把m1
的IMP
指向_objc_msgForward
,这样当调用方法m1
时就自动触发消息转发机制了,详细实现如下
1static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
2
3 Method targetMethod = class_getInstanceMethod(klass, selector);
4 IMP targetMethodIMP = method_getImplementation(targetMethod);
5 if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
6 ...
7 // We use forwardInvocation to hook in. 把函数的调用直接触发转发函数,转发函数已经被hook,所以在转发函数时进行block的调用
8 class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
9 }
10}
核心转发函数处理
上面一切准备就绪,那么怎么触发之前添加的切面block呢,首先我们梳理下整个流程
方法
m1
的IMP
指向了_objc_msgForward
,调用m1
则会自动触发消息转发机制替换
forwardInvocation:
,把它的IMP
指向ASPECTS_ARE_BEING_CALLED__`方法,消息转发时触发的就是`__ASPECTS_ARE_BEING_CALLED
上面操作可以直接看出调用方法m1
则会直接触发__ASPECTS_ARE_BEING_CALLED__
方法,而__ASPECTS_ARE_BEING_CALLED__
方法就是处理切面block用和原有函数的调用时机,详细看下面实现步骤
根据调用的
selector
,获取容器对象AspectsContainer
,这里面存储了这个类或对象的所有切面信息AspectInfo
会存储当前的参数信息,用于传递首先触发函数调用前的block,存储在容器的
beforeAspects
对象中接下来如果存在替换原有函数的block,即
insteadAspects
不为空,则触发它,如果不存在则调用原来的函数触发函数调用后的block,存在在容器的
afterAspects
对象中
1static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
2 AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
3 AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
4 AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
5
6 // Before hooks. 方法执行之前调用
7 aspect_invoke(classContainer.beforeAspects, info);
8 aspect_invoke(objectContainer.beforeAspects, info);
9
10 // Instead hooks. 替换原方法或者调用原方法
11 BOOL respondsToAlias = YES;
12 if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
13 aspect_invoke(classContainer.insteadAspects, info);
14 aspect_invoke(objectContainer.insteadAspects, info);
15 }else {
16 Class klass = object_getClass(invocation.target);
17 do {
18 if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
19 [invocation invoke];
20 break;
21 }
22 }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
23 }
24
25 // After hooks. 方法执行之后调用
26 aspect_invoke(classContainer.afterAspects, info);
27 aspect_invoke(objectContainer.afterAspects, info);
28
29 ...
30 // Remove any hooks that are queued for deregistration.
31 [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
32}
总结
Aspects的核心原理是利用了消息转发机制,通过替换消息转发方法来实现切面的分发调用,这个思想和巧妙而且应用很广泛,值得学习
目前这个库已经很长时间没有维护了,原子操作的支持使用的还是自旋锁,目前这种锁已经不安全了
另外使用这个库是需要注意类似原理的其他框架,可能会有冲突,如JSPatch
,不过JSPatch
已经被封杀了,但类似需求有很多
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取