什么是AOP
OOP和AOP
- OOP(Object Oriented Program,面向对象编程):侧重对功能进行模块化和封装继承。关注静态的对象层次结构。
- AOP(Aspect Oriented Program,面向切面编程):侧重对横跨各个模块中的公共功能进行统一处理。关注动态的运行时的业务流程。
例如使用OOP封装了一个日志框架,包含打印、缓存、上报等功能。看起来很完美,实际上存在以下问题:
- 对调用方来说,需要在多处地方添加方法调用,和调用方的主业务耦合,违背单一职责原则。代码分散,需求变动的时候需要改动多处。
- 难以对现有对象(如三方库中的类)动态添加功能,需要通过继承或组合的方式扩展对象功能。
- 编写代码的时候需要考虑不同类之间的共性,再提取方法或者接口。
如下图
graph TB subgraph 验证用户方法封装 a[验证用户] b[验证用户] c[验证用户] end 流程1[开始]-->a-->查询余额 流程2[开始]-->b-->取款 流程3[开始]-->c-->转账
需要在每个流程中加入验证用户的步骤,即使对重复代码进行了封装,但还是免不了要调用方法。
解决:将验证用户的方法抽离出来(切面),在编译或者运行期间讲代码织入到目标位置(连接点)。我们把这种方法叫AOP。
AOP能够将这些与业务无关,却被业务模块所共同调用的逻辑封装起来(切面);在不侵入业务代码的情况下,将这些公共的功能插入到主业务流程中(连接点)。减少系统的重复代码,降低模块间的耦合度,提高可维护性。
AOP的思想可以让我们在写代码的时候只考虑主业务流程,而不用考虑这些不重要的流程。
AOP和OOP是互补的关系
相关概念
横切关注点(Cross-cutting concerns):面向对象模型中大多数类会实现单一特定的功能,但有时候也需要添加一些公共的附属功能。例如日志、埋点等功能会分布在不同对象层次中,与对象自身的功能没有关系。这些散布在各处的无关的代码称为横切关注点。
连接点(JoinPoint):程序运行时的一些执行点,可以在这些地方插入代码。如方法调用、方法执行、类初始化、构造、变量get和set等。
切点(Pointcut):用于选择和匹配我们想要插入代码的目标JoinPoint。
关于切点,有两种说法,刚开始看概念很容易混乱,两种理解都可以,只是角度不同。
- 切点是指写匹配规则的地方,定义在切面中。关注从哪里切入(切入的点)
- 切点是指匹配到的结果(目标JoinPoint),是JoinPoint的子集。关注切到的结果(切到的点)
通知(Advice):Pointcut拦截到JoinPoint之后要做的事情,即要增强的功能(要注入的代码,如log打印,权限检查等)。常见的通知类型如前置通知(在Pointcut之前执行)、后置通知、异常通知、最终通知、环绕通知。
切面(Aspect):切入点+通知=切面
织入(Weaving):注入代码(Advice)到目标位置(JoinPoint)的过程。分为静态织入(编译期或类加载期)和动态织入(运行期)
目标对象(Target):被代理的对象
代理(Proxy):一个类被增强实际上是生成一个代理类,调用的时候实际上是调用代理类的方法。
实现原理
- 静态织入:编译期织入代码。又分为不同阶段:java、class、dex代码。如APT、AspectJ、ASM、Javassit、ASMDex、DexMaker、操作AST、手动修改class文件。
- 动态织入:运行时织入代码。又分为类加载和运行。如Java动态代理、Cglib、Javassit、XPosed、Dexposed、自定义类加载器。
graph TB subgraph 编译期 源代码--"APT"-->java文件--"AspectJ"-->class1[class字节码]--"Javassist、ASM、AspectJ"-->dex字节码 end subgraph 运行期 dex字节码--"Javassist、ASM、ASMDex、DexMaker"-->ClassLoader类加载--"Dexposed、Xposed"-->运行 end
Android注解解析主要分为两种:
- 运行时反射解析当前类(注解作用域:RUNTIME):性能较低。
- 编译期生成代理类和代理对象(注解作用域:SOURCE、CLASS):需要生成额外代码
APT(Annotation Processing Tool,注解处理器)
AOP技术 | 作用时机 | 输入 | 输出 | 代表框架 | |
---|---|---|---|---|---|
APT | 编译期 | Java文件 | Java文件 | DataBinding、Dagger2、ButterKnife、EventBus3、DBFlow | |
方法优缺点、难点比对
//todo
方法 | 作用时机 | 操作对象 | 优点 | 缺点 | 为了上手,我需要掌握什么? |
---|---|---|---|---|---|
APT | 编译期:还未编译为 class 时 | .java 文件 | 1. 可以织入所有类;2. 编译期代理,减少运行时消耗 | 1. 需要使用 apt 编译器编译;2. 需要手动拼接代理的代码(可以使用 Javapoet 弥补);3. 生成大量代理类 | 设计模式和解耦思想的灵活应用 |
AspectJ | 编译期、加载时 | .java 文件 | 功能强大,除了 hook 之外,还可以为目标类添加变量,接口。也有抽象,继承等各种更高级的玩法。 | 1. 不够轻量级;2. 定义的切点依赖编程语言,无法兼容Lambda语法;3. 无法织入第三方库;4. 会有一些兼容性问题,如:D8、Gradle 4.x等 | 复杂的语法,但掌握几个简单的,就能实现绝大多数场景 |
Javassist | 编译期:class 还未编译为 dex 时或运行时 | class 字节码 | 1. 减少了生成子类的开销;2. 直接操作修改编译后的字节码,直接绕过了java编译器,所以可以做很多突破限制的事情,例如,跨 dex 引用,解决热修复中 CLASS_ISPREVERIFIED 问题。 | 运行时加入切面逻辑,产生性能开销。 | 1. 自定义 Gradle 插件;2. 掌握groovy 语言 |
ASM | 编译期或运行期字节码注入 | class 字节码 | 小巧轻便、性能好,效率比Javassist高 | 学习成本高 | 需要熟悉字节码语法,ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。 |
ASMDEX | 编译期和加载时:转化为 .dex 后 | Dex 字节码,创建 class 文件 | 可以织入所有类 | 学习成本高 | 需要对 class 文件比较熟悉,编写过程复杂。 |
DexMaker | 同ASMDEX | Dex 字节码,创建 dex 文件 | 同ASMDEX | 同ASMDEX | 同ASMDEX |
Cglib | 运行期生成子类拦截方法 | 字节码 | 没有接口也可以织入 | 1. 不能代理被final字段修饰的方法;2. 需要和 dexmaker 结合使用 | – |
xposed | 运行期hook | – | 能hook自己应用进程的方法,能hook其他应用的方法,能hook系统的方法 | 依赖三方包的支持,兼容性差,手机需要root | – |
dexposed | 运行期hook | – | 只能hook自己应用进程的方法,但无需root | 1. 依赖三方包的支持,兼容性差;2. 只能支持 Dalvik 虚拟机 | – |
epic | 运行期hook | – | 支持 Dalvik 和 Art 虚拟机 | 只适合在开发调试中使用,碎片化严重有兼容性问题 | – |
AspectJ
ASM
ASM 插桩工具 :
操作灵活 : 可以在字节码任何位置,自定义修改、插入、删除相关逻辑 ;
上手很难 : 对 .class 字节码文件有比较深入的了解,编写过程较复杂
AOP技术 | 功能 | 性能 | 面向接口编程 | 编程难度 |
---|---|---|---|---|
直接改写class文件 | 完全控制类 | 无明显性能代价 | 不要求 | 高,要求对class文件结构和Java字节码有深入的了解 |
JDK Instrument | 完全控制类 | 无论是否改写,每个类装入时都要执行 hook 程序 | 不要求 | 高,要求对 class 文件结构和 Java 字节码有深刻了解 |
JDK Proxy | 只能改写 method | 反射引入性能代价 | 要求 | 低 |
ASM | 几乎能完全控制类 | 无明显性能代价 | 不要求 | 中,能操纵需要改写部分的 Java 字节码 |
AspectJ |
基于Javassist的字节码处理工具:DroidAssist
ASM是一个框架/库,它为您提供了一个API来操作现有的字节码和/或轻松生成新的字节码。
另一方面,AspectJ是一种基于Java语言的语言扩展,具有自己的语法,专门用于通过面向方面的编程概念扩展Java运行时的功能。它包括一个编译器/编织器,可以在编译时或运行时运行。
它们的相似之处在于它们都通过现有字节码的字节码操作和/或生成新的字节码来实现其目标。
ASM更通用,因为它没有关于如何修改现有字节码的意见,它只是为您提供了一个API,您可以随心所欲地使用它。另一方面,AspectJ更具体,范围更窄,它只支持一些预定义的AOP结构,但它为您提供了一个界面(aspectj语言),如果您可以在这些结构中使用它,则更容易使用它为您提供。
对于我见过的大多数用例,AspectJ绰绰有余,但在极少数情况下,ASM可能是一个不错的选择,但是你需要更多的编程努力取得类似的成果。
ASMDex
类似ASM的字节码操作库,运行在Android平台,操作dex字节码
DexMaker
提供JavaAPI,用于编译期或运行时生成Dex字节码
Javassist
原理:使用Gradle Task或Transform,在dex文件生成之前,修改class字节码。不需要生成子类代理
框架:热修复HotFix
Javassist & ASM 对比
Javassist抽象出源代码级的API,比ASM中实际的字节码操作更容易使用
Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
总的来说ASM比Javassist快得多,并且提供了更好的性能。Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
总之,如果有人需要更简单的方法来动态操作或创建Java类,那么应该使用Javassist API 。如果需要注重性能地方,应该使用ASM库。
Cglib(Code Generation Library)
原理:底层采用ASM字节码框架,运行期间类加载的时候为目标类创建一个字类,代理拦截父类方法调用。解决了JDK动态代理需要定义接口的问题
缺点:
- 无法代理final类和方法:Cglib是通过子类代理实现的
- 无法在Android中使用,Android加载的是dex文件,而Cglib生成的是class文件:需要结合Dexmaker生成dex字节码文件。参考将cglib动态代理思想带入Android开发
AOP应用场景
- 参数校验和判空
- 运行时权限验证或申请
- 运行时登录验证
- 检查网络连接
- 日志管理:输出调试日志。如打印代码行数、入参、出参等
- 重要生命周期添加打印
- 无侵入埋点
- 事件防抖
- 异常统一处理
- 事务处理
- 性能监控,如统计方法耗时
- 方法调用线程切换
- 对变量和方法返回值进行缓存和注入,内存缓存和持久缓存
- 对数据和常量加密
- 热修复:新方法替换旧方法
AOP和字节码插桩的区别和联系
- AOP:是一种编程思想,OOP的延续。将系统中非核心的业务提取出来,进行单独处理。AOP中用到了IoC和DI的思想
- 字节码插桩:即修改字节码,是实现AOP的一种技术。AOP除了修改字节码之外还可以修改源码、抽象语法树等。
除了上面的场景之外,字节码插桩和修改还可以应用于:
- 三方库Hook,不需要修改三方库源码
- 应用破解
AOP与IoC(控制反转)、DI(依赖注入)的区别和联系
区别:AOP强调对代码运行过程进行修改。IoC和DI强调不同对象间的依赖关系,以及如何依赖。
联系:AOP和IoC用到的技术类似,例如AspectJ、APT、字节码修改等,既可用于AOP,也可以用于IoC
Tips:IoC和DI是同一个概念的不同描述,也可以说通过依赖注入来实现控制反转。
AOP实战
编写AOP思路:
- 考虑注入代码的时机、以及注入是否有限制(如final、private限制),选择合适的AOP工具(使用场景、性能等)
- 编译期:生成java、class、dex文件
- 运行期:类加载、运行时
- 考虑要注入代码的地方:如方法调用、执行、类初始化等时机
- 考虑怎么找到要注入代码的地方
- 匹配类名、方法名
- 使用注解标记
- 考虑怎么处理代码
- 在代码前后插入逻辑
- 替换目标代码
框架
- Lancet轻量级的框架,编译速度快,支持增量编译
- Lancet语法简单,易于上手。AspectJ需要学习的语法比较多。
- Lancet仅支持hook具体的方法,不能像AspectJ一样根据自定义的注解来Hook一个类或者任意的方法。
使用场景建议
- 如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。
- 如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。
结语
参考文章: