????????关注后回复 “进群” ,拉你进程序员交流群????????

作者丨monkery

来源丨码上work(codework88)

背景简述

在日常开发过程中是否有过这样的需求:不修改原来的函数,但是又想在函数的执行前后插入一些代码。这个方式就是面向切面(AOP),在iOS开发中比较知名的框架就是Aspects,而饿了么新出的Stinger框架先不讨论,Aspects的源码精炼巧妙,很值得学习深究,本文主要从源码和应用层面来介绍下

源码解析

先提出几个问题

带着问题去阅读更容易理解

  1. Aspects实现的核心原理是什么

  2. 哪些方法不能被hook

  3. hook的操作是否可以只对某个实例生效,对同一个类的其他实例不生效

  4. block是如何被存储和调用的

基本原理

正常来讲想实现AOP,可以利用runtime的特性进行method swizzle,但Aspects就是造好的轮子,而且更好用,下面简述下Aspects的基本原理

runtime的消息转发机制

在OC中,所有的消息调用最后都会通过objc_msgSend()方法进行访问

  1. 通过objc_msgSend()进行消息调用,为了加快执行速度,这个方法在runtime源码中是用汇编实现的

  2. 然后调用lookUpImpOrForward()方法,返回值是个IMP指针,如果查找到了调用函数的IMP,则进行方法的访问

  3. 如果没有查到对于方法的IMP指针,则进行消息转发机制

  4. 第一层转发:会调用resolveInstanceMethod:、resolveClassMethod:,这次转发是方法级别的,开发者可以动态添加方法进行补救

  5. 第二层转发:如果第一层转发返回NO,则会进行第二层转发,调用forwardingTargetForSelector:,可以把调用转发到另一个对象,这是类级别的转发,调用另一个类的相同的方法

  6. 第三层转发:如果第二层转发返回nil,则会进入这一层处理,这层会调用methodSignatureForSelector:、forwardInvocation:,这次是完整的消息转发,因为你可以返回方法签名、动态指定调用方法的Target

  7. 如果转发都失败,就会crash

Aspects的基本原理

对外暴露的核心API

 1/**2作用域:针对所有对象生效3selector: 需要hook的方法4options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)5block: 需要在selector前后插入执行的代码块6error: 错误信息7*/8+ (id<AspectToken>)aspect_hookSelector:(SEL)selector9                           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。接下来详细分析巧妙的设计

  1. 类A的方法m被添加切面方法

  2. 创建一个类A的子类B,并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. 对于方法m,类B会直接把方法m的IMP指向_objc_msgForward()方法,这样当调用方法m时就会走消息转发流程,触发ABC函数

详细分析

执行入口

 1- (id<AspectToken>)aspect_hookSelector:(SEL)selector2                      withOptions:(AspectOptions)options3                       usingBlock:(id)block4                            error:(NSError **)error {5    return aspect_add(self, selector, options, block, error);6}78static 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    });78    // 第一步:检查是否在黑名单内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}
  1. 不允许hookretainreleaseautoreleaseforwardInvocation:,这些不多解释

  2. 允许hookdealloc,但是只能在dealloc执行前,这都是为了程序的安全性设置的

  3. 检查这个方法是否存在,不存在则不能hook

  4. 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

存储切面信息

存储切面信息主要用到了上面介绍的AspectsContainerAspectIdentifier这两个类,主要操作如下(注释写的已经很详细)

  1. 获取当前类的容器对象aspectContainer,如果没有则创建一个

  2. 创建一个标识符对象identifier,用来存储原方法信息、block、切面时机等信息

  3. 把标识符对象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只对单一对象有效,二:避免了对原有类的侵入

这一步主要做了几个操作

  1. 如果已经存在中间类,则直接返回

  2. 如果是类对象,则不用创建中间类,并把这个类存储在swizzledClasses集合中,标记这个类已经被hook了

  3. 如果存在kvo的情况,那么系统已经帮我们创建好了中间类,那就直接使用

  4. 对于不存在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);56    // Already subclassed7    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,那么把m1IMP指向_objc_msgForward,这样当调用方法m1时就自动触发消息转发机制了,详细实现如下

 1static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {23    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呢,首先我们梳理下整个流程

  1. 方法m1IMP指向了_objc_msgForward,调用m1则会自动触发消息转发机制

  2. 替换forwardInvocation:,把它的IMP指向ASPECTS_ARE_BEING_CALLED__`方法,消息转发时触发的就是`__ASPECTS_ARE_BEING_CALLED

上面操作可以直接看出调用方法m1则会直接触发__ASPECTS_ARE_BEING_CALLED__方法,而__ASPECTS_ARE_BEING_CALLED__方法就是处理切面block用和原有函数的调用时机,详细看下面实现步骤

  1. 根据调用的selector,获取容器对象AspectsContainer,这里面存储了这个类或对象的所有切面信息

  2. AspectInfo会存储当前的参数信息,用于传递

  3. 首先触发函数调用前的block,存储在容器的beforeAspects对象中

  4. 接下来如果存在替换原有函数的block,即insteadAspects不为空,则触发它,如果不存在则调用原来的函数

  5. 触发函数调用后的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];56    // 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 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击????卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

Aspects深度解析-iOS面向切面编程相关推荐

  1. iOS面向切面编程-AOP

    1. AOP简介 AOP: Aspect Oriented Programming 面向切面编程. 面向切面编程(也叫面向方面):Aspect Oriented Programming(AOP),是目 ...

  2. iOS 面向切面编程 Aspects 库的使用

    前言 在使用ios11的系统时,发现UITableView在上拉加载更多时,加载出来数据后tableview的位置有跳动问题. 通过搜索·很快找到了问题的解决方案,需要给tableView设置几个属性 ...

  3. python aop编程_学习笔记: AOP面向切面编程和C#多种实现

    AOP:面向切面编程   编程思想 OOP:一切皆对象,对象交互组成功能,功能叠加组成模块,模块叠加组成系统 类--砖头     系统--房子 类--细胞     系统--人 面向对象是非常适合做大型 ...

  4. Spring(4)——面向切面编程(AOP模块)

    Spring AOP 简介 如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用. AOP 即 Aspect Orien ...

  5. Javascript aop(面向切面编程)之around(环绕)

    Aop又叫面向切面编程,其中"通知"是切面的具体实现,分为before(前置通知).after(后置通知).around(环绕通知),用过spring的同学肯定对它非常熟悉,而在j ...

  6. Autofac的AOP面向切面编程研究

    我的理解是 把系统性的编程工作封装起来 =>我给这个取个名字叫 "Aspect",然后通过AOP技术把它切进我们的业务逻辑代码 => "业务" 这样 ...

  7. C# 中使用面向切面编程(AOP)中实践代码整洁

    1. 前言 最近在看<架构整洁之道>一书,书中反复提到了面向对象编程的 SOLID 原则(在作者的前一本书<代码整洁之道>也是被大力阐释),而面向切面编程(Aop)作为面向对象 ...

  8. WebApi client 的面向切面编程

    .Net的面向切面编程 .Net的服务端应用AOP很常见,在Asp.net MVC与Asp.net WebApi等新框架里到处都有AOP的影子,我们可以把一个服务方法"切"为很多面 ...

  9. 我的控制反转,依赖注入和面向切面编程的理解

    感谢http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/ 的供图 1.什么是控制? 如下图所示,我们看到了 软件系统中 对象的 高耦合现象.全体齿轮的转 ...

最新文章

  1. Hibernate懒加载解析
  2. cassandra——可以预料的查询,如果你的查询条件有一个是根据索引查询,那其它非索引非主键字段,可以通过加一个ALLOW FILTERING来过滤实现...
  3. leetcode 5. 最长回文子串 暴力法、中心扩展算法、动态规划,马拉车算法(Manacher Algorithm)
  4. servlet的体系结构
  5. 服务器系统玩dnf,win7系统玩dnf提示正在连接服务器的解决方法
  6. mapper注入失败,NoSuchBeanDefinitionException: No qualifying bean of type [com.xxx.XxxMapper] found for d
  7. 【Python】pysnooper模块对代码进行调试
  8. 【Flink】Flink Invalid timestamp -1 Timestamp should always be none-negative or null
  9. 苹果屏幕旋转怎么设置_iPhone12屏幕供应商是谁 苹果12屏幕怎么查看是哪家
  10. Windows XP 优化
  11. STEAM 自动安装时提示C++ 安装不了等问题
  12. JS实现继承的几种方式
  13. 关于电感数字传感器的一些问题
  14. [视频发布] 掘金 Podcast 报名中,摩拜单车、美团点评团队分享 Vue 最佳实践
  15. 数字藏品NFT用的国内联盟链有哪些?
  16. BZOJ3772精神污染
  17. 神经网络建模的建模步骤,人工神经网络建模过程
  18. BigDecimal表示0.1
  19. 新闻联播 华为鸿蒙,央视为华为鸿蒙OS科普,苹果比安卓流畅的原因华为也可以...
  20. 寒假每日一题题解(1.29)摘花生(DP水题)

热门文章

  1. 获取全国省地市地图json数据
  2. 数据挖掘过来人的建议
  3. 约书亚—摩西的好助手
  4. ALSA 音频开发部分基础知识
  5. 新大陆C/C++开发实习生面试
  6. Fully-Convolutional Siamese Networks for Object Tracking全文翻译
  7. OpenGL Blend
  8. 说说大型高并发高负载网站的系统架构 1
  9. MySQL安装配置教程-win10
  10. Python中type的意思