一、AOP即面向切面编程
AOP 是 Aspect Oriented Programming 的缩写,译为面向切面编程。用我们最常用的 OOP 来对比理解:
纵向关系 OOP,横向角度 AOP
举个例子:
设计一个日志打印模块。按 OOP 思想,我们会设计一个打印日志 LogUtils 类,然后在需要打印的地方引用即可。
public class ClassA {
private void initView() {
LogUtils.d(TAG, "onInitView");
}
}
public class ClassB {
private void onDataComplete(Bean bean) {
LogUtils.d(TAG, bean.attribute);
}
}
public class ClassC {
private void onError() {
LogUtils.e(TAG, "onError");
}
}
看起来没有任何问题是吧?
但是这个类是横跨并嵌入众多模块里的,在各个模块里分散得很厉害,到处都能见到。从对象组织角度来讲,我们一般采用的分类方法都是以「继承」关系为主线,我们称之为纵向,也就是 OOP。设计时只使用 OOP思想可能会带来两个问题:
对象设计的时候一般都是纵向思维,如果这个时候考虑这些不同类对象的共性,不仅会增加设计的难度和复杂性,还会造成类的接口过多而难以维护(共性越多,意味着接口契约越多)。
需要对现有的对象动态增加某种行为或责任时非常困难。
而AOP就可以很好地解决以上的问题,怎么做到的?除了这种纵向分类之外,我们从横向的角度去观察这些对象,无需再去到处调用 LogUtils 了,声明哪些地方需要打印日志,这个地方就是一个切面,AOP 会在适当的时机为你把打印语句插进切面。
// 只需要声明哪些方法需要打印 log,打印什么内容
public class ClassA {
@Log(msg = "onInitView")
private void initView() {
}
}
public class ClassB {
@Log(msg = "bean.attribute")
private void onDataComplete(Bean bean) {
}
}
public class ClassC {
@Log(msg = "onError")
private void onError() {
}
}
如果说 OOP 是把问题划分到单个模块的话,那么 AOP 就是把涉及到众多模块的某一类问题进行统一管理。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。利用 AOP 思想,这样对业务逻辑的各个部分进行了隔离,从而降低业务逻辑各部分之间的耦合,提高程序的可重用性,提高开发效率。
OOP 与 AOP 的区别
面向目标不同:简单来说 OOP 是面向名词领域,AOP 面向动词领域。
思想结构不同:OOP 是纵向结构,AOP 是横向结构。
注重方面不同:OOP 注重业务逻辑单元的划分,AOP 偏重业务处理过程中的某个步骤或阶段。
OOP 与 AOP 的联系
两者之间是一个相互补充和完善的关系。
二、应用场景
那AOP既然这么有用,除了上面提到的打印日志场景,还有没有其他用处呢?
只要系统的业务模块都需要引用通用模块,就可以使用AOP。以下是一些常用的业务场景:
-
参数校验和判空
系统之间在进行接口调用时,往往是有入参传递的,入参是接口业务逻辑实现的先决条件,有时入参的缺失或错误会导致业务逻辑的异常,大量的异常捕获无疑增加了接口实现的复杂度,也让代码显得雍肿冗长,因此提前对入参进行验证是有必要的,可以提前处理入参数据的异常,并封装好异常转化成结果对象返回给调用方,也让业务逻辑解耦变得独立。 -
Android API23+的权限控制
避免到处都是申请权限和处理权限的代码 -
无痕埋点
-
安全控制
比如全局的登录状态流程控制。 -
日志记录
-
事件防抖
防止View被连续点击触发多次事件 -
性能统计
检测方法耗时其实已经有一些现成的工具,比如 trace view。痛点是这些工具使用起来都比较麻烦,效率低下,而且无法针对某一个块代码或者某个指定的sdk进行查看方法耗时。可以采用 AOP 思想对每个方法做一个切点,在执行之后打印方法耗时。 -
事务处理
声明方法,为特定方法加上事务,指定情况下(比如抛出异常)回滚事务 -
异常处理
替代防御性的 try-Catch。 -
缓存
缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。 -
软件破解
使用 Hook 修改软件的验证类的判断逻辑。 -
热修复
AOP 可以让我们在执行一个方法的前插入另一个方法,运用这个思路,我们可以把有 bug 的方法替换成我们下发的新方法。
三、AOP 方法
Android AOP 常用的方法有 JNI HOOK 和 静态织入。
-
动态织入 Hook 方式
在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。相对于静态AOP更加灵活。但切入的关注点需要实现接口。对系统有一点性能影响。
Dexposed
Xposed
epic 在 native 层修改 java method 对应的 native 指针
-
动态字节码生成
Cglib + Dexmaker Cglib 是一个强大的,高性能的 Code 生成类库,
原理是在运行期间目标字节码加载后,通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。由于是通过子类来代理父类,因此不能代理被final 字段修饰的方法。 -
静态织入方式
在编译期织入,切面直接以字节码的形式编译到目标字节码文件中,这要求使用特殊的 Java 编译器。
在类装载期织入,这要求使用特殊的类装载器。 静态织入对系统无性能影响。但灵活性不够。APT
AspectJ
ASM
Javassist
DexMaker
ASMDEX
这么多方法?有什么区别?
方法作用期比对
AOP 是思想,上面的方法其实都是工具,只不过是插入时机和方式不同。
同:都可以织入逻辑,都体现了 AOP 思想
异:作用的时机不一样,且适用的注解的类型不一样。
方法优缺点、难点比对
四、ASM简介
ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码,添加成员变量,修改父类,添加接口等等。
ASM通过访问者模式依次遍历class字节码中的各个部分,并不断的通过回调的方式通知上层(这有点像SAX解析xml的过程),上层可在业务关心的某个访问点,修改原有逻辑。
之所以可以这么做,是因为java字节码是按照严格的JVM规范生成二进制字节流,ASM只是按照这个规范对java字节码的一次解释,将晦涩难懂的字节码背后对应的JVM指令一条条的转换成ASM API。
比如,一句简单的日志打印
Log.d(“tag”, " onCreate");
转换成ASM API将会是下面这样:
mv.visitLdcInsn(“tag”);
mv.visitLdcInsn(“onCreate”);
mv.visitMethodInsn(INVOKESTATIC, “android/util/Log”, “d”, “(Ljava/lang/String;Ljava/lang/String;)I”, false);
mv.visitInsn(POP);
如果稍懂JVM汇编指令的话,可以看出大致意思。
加载常量"tag"入栈
加载常量"onCreate"入栈
执行Log的静态方法d
方法调用出栈