0%

面向切面编程(AOP)

什么是AOP

OOP和AOP

  • OOP(Object Oriented Program,面向对象编程):侧重对功能进行模块化和封装继承。关注静态的对象层次结构。
  • AOP(Aspect Oriented Program,面向切面编程):侧重对横跨各个模块中的公共功能进行统一处理。关注动态的运行时的业务流程。

例如使用OOP封装了一个日志框架,包含打印、缓存、上报等功能。看起来很完美,实际上存在以下问题:

  1. 对调用方来说,需要在多处地方添加方法调用,和调用方的主业务耦合,违背单一职责原则。代码分散,需求变动的时候需要改动多处。
  2. 难以对现有对象(如三方库中的类)动态添加功能,需要通过继承或组合的方式扩展对象功能。
  3. 编写代码的时候需要考虑不同类之间的共性,再提取方法或者接口。

如下图

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。

    关于切点,有两种说法,刚开始看概念很容易混乱,两种理解都可以,只是角度不同。

    1. 切点是指写匹配规则的地方,定义在切面中。关注从哪里切入(切入的点)
    2. 切点是指匹配到的结果(目标JoinPoint),是JoinPoint的子集。关注切到的结果(切到的点)
  • 通知(Advice):Pointcut拦截到JoinPoint之后要做的事情,即要增强的功能(要注入的代码,如log打印,权限检查等)。常见的通知类型如前置通知(在Pointcut之前执行)、后置通知、异常通知、最终通知、环绕通知。

  • 切面(Aspect):切入点+通知=切面

  • 织入(Weaving):注入代码(Advice)到目标位置(JoinPoint)的过程。分为静态织入(编译期或类加载期)和动态织入(运行期)

  • 目标对象(Target):被代理的对象

  • 代理(Proxy):一个类被增强实际上是生成一个代理类,调用的时候实际上是调用代理类的方法。

实现原理

  1. 静态织入:编译期织入代码。又分为不同阶段:java、class、dex代码。如APT、AspectJ、ASM、Javassit、ASMDex、DexMaker、操作AST、手动修改class文件。
  2. 动态织入:运行时织入代码。又分为类加载和运行。如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注解解析主要分为两种:

  1. 运行时反射解析当前类(注解作用域:RUNTIME):性能较低。
  2. 编译期生成代理类和代理对象(注解作用域:SOURCE、CLASS):需要生成额外代码

APT(Annotation Processing Tool,注解处理器)

APT介绍和实践

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

AspectJ介绍和案例

ASM

ASM 插桩工具 :

操作灵活 : 可以在字节码任何位置,自定义修改、插入、删除相关逻辑 ;

上手很难 : 对 .class 字节码文件有比较深入的了解,编写过程较复杂

AOP技术 功能 性能 面向接口编程 编程难度
直接改写class文件 完全控制类 无明显性能代价 不要求 高,要求对class文件结构和Java字节码有深入的了解
JDK Instrument 完全控制类 无论是否改写,每个类装入时都要执行 hook 程序 不要求 高,要求对 class 文件结构和 Java 字节码有深刻了解
JDK Proxy 只能改写 method 反射引入性能代价 要求
ASM 几乎能完全控制类 无明显性能代价 不要求 中,能操纵需要改写部分的 Java 字节码
AspectJ

基于ASM的字节码处理工具:HunterHibeaver

基于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动态代理需要定义接口的问题

缺点:

  1. 无法代理final类和方法:Cglib是通过子类代理实现的
  2. 无法在Android中使用,Android加载的是dex文件,而Cglib生成的是class文件:需要结合Dexmaker生成dex字节码文件。参考将cglib动态代理思想带入Android开发

AOP应用场景

  1. 参数校验和判空
  2. 运行时权限验证或申请
  3. 运行时登录验证
  4. 检查网络连接
  5. 日志管理:输出调试日志。如打印代码行数、入参、出参等
  6. 重要生命周期添加打印
  7. 无侵入埋点
  8. 事件防抖
  9. 异常统一处理
  10. 事务处理
  11. 性能监控,如统计方法耗时
  12. 方法调用线程切换
  13. 对变量和方法返回值进行缓存和注入,内存缓存和持久缓存
  14. 对数据和常量加密
  15. 热修复:新方法替换旧方法

AOP和字节码插桩的区别和联系

  • AOP:是一种编程思想,OOP的延续。将系统中非核心的业务提取出来,进行单独处理。AOP中用到了IoC和DI的思想
  • 字节码插桩:即修改字节码,是实现AOP的一种技术。AOP除了修改字节码之外还可以修改源码、抽象语法树等。

除了上面的场景之外,字节码插桩和修改还可以应用于:

  1. 三方库Hook,不需要修改三方库源码
  2. 应用破解

AOP与IoC(控制反转)、DI(依赖注入)的区别和联系

区别:AOP强调对代码运行过程进行修改。IoC和DI强调不同对象间的依赖关系,以及如何依赖。

联系:AOP和IoC用到的技术类似,例如AspectJ、APT、字节码修改等,既可用于AOP,也可以用于IoC

Tips:IoC和DI是同一个概念的不同描述,也可以说通过依赖注入来实现控制反转。

AOP实战

编写AOP思路:

  1. 考虑注入代码的时机、以及注入是否有限制(如final、private限制),选择合适的AOP工具(使用场景、性能等)
    1. 编译期:生成java、class、dex文件
    2. 运行期:类加载、运行时
  2. 考虑要注入代码的地方:如方法调用、执行、类初始化等时机
  3. 考虑怎么找到要注入代码的地方
    1. 匹配类名、方法名
    2. 使用注解标记
  4. 考虑怎么处理代码
    1. 在代码前后插入逻辑
    2. 替换目标代码

框架

  1. Lancet轻量级的框架,编译速度快,支持增量编译
  2. Lancet语法简单,易于上手。AspectJ需要学习的语法比较多。
  3. Lancet仅支持hook具体的方法,不能像AspectJ一样根据自定义的注解来Hook一个类或者任意的方法。

使用场景建议

  1. 如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。
  2. 如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。

结语

参考文章:

欢迎关注我的其它发布渠道