什么是 AOP
OOP vs AOP
- OOP 的局限:
- OOP 的精髓是把功能或问题模块化,每个模块处理自己的事情,但在现实世界中,并不是所有问题都能完美得划分到模块中。
- 举个最简单而又常见的例子,现在想为每个模块加上日志功能,要求模块运行时候能输出日志。一般的处理都是: - 先设计一个日志输出模块,这个模块提供日志输出 API,比如 Android 中的 Log 类。 - 然后,其他模块需要输出日志的时候调用 Log 类的几个函数,比如 e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。 - 但是,从 OOP 角度看,除了日志模块本身,其他模块的业务动作、绝大部分情况下应该都不会包含日志输出功能。 - 以 ActivityManagerService 为例,显然 ActivityManagerService 的功能点中不包含输出日志这一项。 - 但实际上,软件中的众多模块确实又需要打印日志。
- 这个日志输出功能,从整体来看,都是一个面上的。而这个面的范围,就不局限在单个模块里了,而是横跨多个模块。
- 在 AOP 中
- 我们要认识到 OOP 世界中,有些功能是横跨并嵌入众多模块里的,比如打印日志,数据上报等。这些功能在各个模块里分散得很厉害,可能到处都能见到。
- AOP 的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。
如果说,OOP 如果是把问题划分到单个模块的话,那么 AOP 就是把涉及到众多模块的某一类问题进行统一管理。
- 比如我们可以设计两个 Aspects,一个是管理某个软件中所有模块的日志输出的功能,另外一个是管理该软件中一些特殊函数调用的权限检查。
AOP 实现
- 实现的时间点及思路
- 编译时
- 在 APK 打包过程中对 .clss 文件的字节码进行扫描更改,通过向编译过程添加额外的步骤来修改被编译的类;
- 加载时
- 当目标类被 Dalvik 或者 ART 加载的时候修改才会被执行,这是对 Java 字节码文件或者 Android 的 dex 文件进行的注入操作;
- 运行时
- Hook 某些关键方法,比如:使用动态代理(这可以说并不是真正的代码注入);
- 编译时
- 实现方法
- 切面编程库(AspectJ),案例:Hugo
- 字节码注入(ASM),案例:GrowingIO
AOP 编程的具体使用场景
- 日志记录
- 数据持久化
- 行为监测
- 组件生命周期监控
- 方法执行时间监控
- 用户点击事件埋点
- 弹窗事件埋点
- 数据验证
- 缓存
AspectJ
AspectJ 是什么
- AspectJ 是广泛应用于 JavaEE 开发的 AOP 方案,是面向切面编程在 Java 中的一种具体实现,简单易用、功能强大。
- 正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化;
- 比如:在某个包下的所有类中的某一类方法中、都需要解决一个相似的问题,可以通过 AOP 的编程方式对此进行模块化封装,统一解决。
- AspectJ 提供了简便的语法让我们定义切面逻辑,再通过提供的 AJC 编译器,在 Java 文件编译成 class 文件的过程里,把切面代码织入到目标业务代码里。
本质上,AspectJ 仍然是以代理的方式实现 AOP,我们通过 AspectJ 就能方便的在目标方法执行前后执行我们的埋点代码。
- 正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化;
AspectJ 组成部分
- AspectJ 向 Java 引入了一个新的概念—— join point,它包括几个新的结构:
- pointcuts
- advice
- inter-type declarations
- aspects。
- 含已解释
- join point 是在程序流中被定义好的点;
- pointcut 在那些点上选出特定的 join point 和值;
- advice 是到达 join point 时被执行的代码;
- inter-type declarations 是 AspectJ 具有的不同类型的类型间声明,允许程序员修改程序的静态结构,即其类的成员和类之间的关系。
-
AspectJ 几个名词术语解释
- Cross-cutting concerns
- 即使在面向对象编程中大多数类都是执行一个单一的、特定的功能,它们也有时候需要共享一些通用的辅助功能。
- 比如:我们想要在一个线程进入和退出一个方法时,在数据层和UI层加上输出log的功能。尽管每一个类的主要功能时不同的,但是它们所需要执行的辅助功能是相似的。
- Advice
- 需要被注入到.class字节码文件的代码。通常有三种:before,after和around,分别是在目标方法执行前,执行后以及替换目标代码执行。
- 除了注入代码到方法中外,更进一步的,你还可以做一些别的修改,例如添加成员变量和接口到一个类中。
- Join point
- 程序中执行代码插入的点,例如方法调用时或者方法执行时。
- Pointcut
- 告诉代码注入工具在哪里注入特定代码的表达式(即需要在哪些Joint point应用特定的Advice)。它可以选择一个这样的点(例如,一个单一方法的执行)或者许多相似的点(例如,所有被自定义注解@DebugTrace标记的方法)。
- Aspect
- Aspect 将 pointcut 和 advice 联系在一起。
- 例如:我们通过定义一个 pointcut 和给出一个准确的 advice 实现向我们的程序中添加一个打印日志功能的 aspect。
- Weaving
- 向目标位置(join point)注入代码(advice)的过程。
- Cross-cutting concerns
ASM 字节码插桩
ASM 是什么
- ASM 是字节码操作库,支持对字节码进行编辑,实现类、属性和方法的增删改查;
- 字节码操作库还有 Javaassit 库可以选择,但 ASM 灵活度和效率都是最高的;
- ASM 可以直接产生二进制的 class 文件,也可以增强既有类的功能。
- Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括:
- 类名称
- 方法
- 属性
- Java 字节码(指令)。
- Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括:
字节码插桩
-
利用操作 ASM 字节码实现方法计时,可以的做法是修改 class 文件,在目标方法开始和结束时插入本来需要手动埋点的代码,这个过程称之为字节码插桩。
- 如图所示是Android打包流程:.java 文件 -> .class 文件 -> .dex 文件;
- 只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google 官方在Android Gradle 的 1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件;
- ASM 字节码插桩,要做的就是实现 Transform 进行 .class 文件遍历拿到所有方法,修改完成对原文件进行替换。
ASM 框架中的核心类
- ClassReader
- 该类用来解析编译过的class字节码文件。
- ClassWriter
- 该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
- ClassViitor
- 主要负责 “拜访” 类成员信息,包括:
- 标记在类上的注解,
- 类的构造方法,
- 类的字段,
- 类的方法,
- 静态代码块。
- 主要负责 “拜访” 类成员信息,包括:
- AdviceAdapter
- 实现了 MethodVisitor 接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。
AspectJ vs ASM
共同点
- AspectJ 和 ASM 既支持以注解作为切入点,也支持根据类方法名/类继承关系等规则来确定切入点。
- 注解的作用是提供插入点;
不同点
- AspectJ 本质上仍然是以代理的方式实现 AOP;
- ASM 通过直接对 class 文件进行修改的方式,实现 AOP 埋点需求。