以前几篇文章咱们详细介绍了AOP的几种技术方案,因为AOP技术复杂多样,实际需求也不尽相同,那么咱们应该如何作技术选型呢?html
本篇将会对现有的AOP技术作一个统一的介绍,尤为侧重在Android方向的落地,但愿对你有所帮助,文中内容、示例大都来自工做总结,若有偏颇不妥,欢迎指正。java
这里先统一一下基本名词,以便表述。android
AOP是一种面向切面编程的技术的统称,AOP框架最终都会围绕class字节码的操做展开,不管是对字节码的操做增删改,为方便描述,咱们统称为代码的织入。git
虽然AOP翻译过来叫面向切面编程,但在实际使用过程当中,切面可能退化成了一个点,好比咱们想统计app的冷启动时间,这就很是具体了。若是咱们用AOP的技术实现统计全部函数的耗时时间,天然能统计到相似启动这个阶段的时间。github
从狭义来看实现AOP技术的框架必须是能将切面编程抽象成上层能够直接使用的工具或API,但当咱们将切面降维后,最终面向的就是切点而已。换句话说,只要能将代码织入到某个点那这种技术就必定能够实现AOP,这样AOP技术所涵盖的领域就得以拓展,由于从狭义的角度看目前只有AspectJ符合这个标准。web
从广义上来说,AOP技术能够是任何能实现代码织入的技术或框架,对代码的改动最终都会体如今字节码上,而这类技术也能够叫作字节码加强,通用名词理解便可。编程
下面咱们将介绍一些经常使用的AOP技术。设计模式
首先,从织入的时机的角度看,能够分为源码阶段、class阶段、dex阶段、运行时织入。bash
对于前三项源码阶段、class阶段、dex织入,因为他们都发生在class加载到虚拟机前,咱们统称为静态织入, 而在运行阶段发生的改动,咱们统称为动态织入。微信
常见的技术框架以下表:
织入时机 | 技术框架 |
---|---|
静态织入 | APT,AspectJ、ASM、Javassit |
动态织入 | java动态代理,cglib、Javassit |
静态织入发生在编译器,所以几乎不会对运行时的效率产生影响;动态织入发生在运行期,可直接将字节码写入内存,并经过反射完成类的加载,因此效率相对较低,但更灵活。
动态织入的前提是类还未被加载,你不能将一个已经加载的类通过修改再次加载,这是ClassLoader的限制。可是能够经过另外一个ClassLoader进行加载,虚拟机容许两个相同类名的class被不一样的ClassLoader加载,在运行时也会被认为是两个不一样的类,所以须要注意不能相互赋值, 否则会抛出ClassCastException。
java动态代理、cglib只会建立新的代理类而不是对原有类的字节码直接修改,Javassit可修改原有字节码。
其实利用反射或者hook技术一样能够实现代码行为的改变,但因为这类技术并无真正的改变原有的字节码,因此暂不在谈论范围内,好比xposed,dexposed。
其次,咱们须要关注这些框架具有哪切面编程的能力,这有助于帮助我作技术选型,因为AspectJ、ASM 、Javassit是相对比较完善的AOP框架,所以只对三者进行比较。
能力 | AspectJ | ASM | Javassit |
---|---|---|---|
切面抽象 | ✓ | ||
切点抽象 | ✓ | ||
通知类型抽象 | ✓ | ✓ | ✓ |
其中:
切面抽象:具有筛选过滤class的能力,好比咱们想为Activity的全部生命周期织入代码,那你是否是首先须要具有过滤Activity及其子类的能力。
切点抽象:具体到某个class,是否具有方法、字段、注解访问的能力。
通知类型抽象:是否直接支持在方法前、后、中直接织入代码。
固然不具有能力不表明不能作AOP编程,能够经过其余方法解决,只是易用性的问题。
下面咱们将开始对上述框架逐一介绍,Let' go~~~
APT(Annotation Processing Tool)即注解处理器,在Gradle 版本>=2.2后被annotationProcessor取代。
它用来在编译时扫描和处理注解,扫描过程可以使用 auto-service 来简化寻找注解的配置,在处理过程当中可生成java文件(建立java文件一般依赖 javapoet 这个库)。经常使用于生成一些模板代码或运行时依赖的类文件,好比常见的ButterKnife、Dagger、ARouter,它的优势是简单方便。
以ButterKnife为例:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
复制代码
一句简单的ButterKnife.bind(this)
是如何实现控件的赋值的?
事实上 @Bind 注解在编译期会生成一个MainActivity_ViewBinding类,而ButterKnife.bind(this) 此次调用最终会经过反射建立出MainActivity_ViewBinding对象,并把activity的引用传递给它。
# ButterKnife
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
...
//建立xxx_binding对象并把activity传入
return constructor.newInstance(target, source);
}
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
...
try {
//运行时经过反射加载在编译阶段生成的类
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
}
...
return bindingCtor;
}
复制代码
这样最终在MainActivity_ViewBinding的构造函数中完成控件的赋值。
public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
protected T target;
public MainActivity_ViewBinding(final T target, Finder finder, Object source) {
...
//为控件赋值 其中优化了控件的查找
target.toolbar = finder.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
...
}
}
复制代码
为了在此类中能访问到MainActivity中声明的属性,为此ButterKnife框架要求,使用@Bind注解声明的属性不能是private的。
能够看到ButterKnife中仍然用到了反射,这是为了统一API使用 ButterKnife.bind(this) 做出的牺牲,而Dagger则会经过Component,Module的名字经过动态生成不一样的方法名,所以使用以前须要对工程进行build。
之因此会这样,是由于APT技术的不足,一般只是用来建立新的类,而不能对原有类进行改动,在不能改动的状况下,只能经过反射实现动态化。
AspectJ是一种严格意义上的AOP技术,由于它提供了完整的面向切面编程的注解,这样让使用者能够在不关心字节码原理的状况下完成代码的织入,由于编写的切面代码就是要织入的实际代码。
AspectJ实现代码织入有两种方式,一是自行编写.ajc文件,二是使用AspectJ提供的@Aspect、@Pointcut等注解,两者最终都是经过ajc编译器完成代码的织入。
举个简单的例子,假设咱们想统计全部view的点击事件,使用AspectJ只须要写一个类便可。
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5";
//切面表达式,声明须要过滤的类和方法
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
public void callMethod() {
}
//before表示在方法调用前织入
@before("callMethod()")
public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
//编写业务代码
}
}
复制代码
注解简明直观,上手难度近乎为0。
经常使用的函数耗时统计工具Hugo,就是AspectJ的一个实际应用,Android平台Hujiang开源的AspectJX插件灵感也来自于Hugo,详情见旧文Android 函数耗时统计工具之Hugo。
AspectJ虽然好用,但也存在一些严重的问题。
AspectJ切面表达式支持继承语法,虽然方便了开发,但存在致命的问题,就是在继承树上的类可能都会织入代码,这在多数业务场景下是不适用的,好比无埋点。
另外使用java8语法编写的代码,不会被进入切面范围,也就没法织入代码。
更多详情参见旧文 Android AspectJ详解 。
ASM是很是底层的面向字节码编程的AOP框架,理论上能够实现任何关于字节码的修改,很是硬核。许多字节码生成API底层都是用ASM实现,常见好比Groovy、cglib,所以在Android平台下使用ASM无需添加额外的依赖。完整的学习ASM必须了解字节码和JVM相关知识。
好比要织入一句简单的日志输出
Log.d("tag", " onCreate");
复制代码
使用ASM编写是下面这个样子,没错由于JVM是基于栈的,函数的调用须要参数先入栈,而后执行函数入栈,最后出栈,总共四条JVM指令。
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
复制代码
能够看出ASM与AspectJ有很大的不一样,AspectJ织入的代码就是实际编写的代码,但ASM必须使用其提供的API编写指令。一行java代码可能对应多行ASM API代码,由于一行java代码背后可能隐藏这多个JVM指令。
你没必要担忧不会编写ASM代码,官方提供了ASM Bytecode Outline插件能够直接将java代码生成ASM代码。
ASM的实际使用场景很是普遍,咱们以Matrix为例。
Matrix是微信开源的一个APM框架,其中TraceCanary子模块用于监测帧率低、卡顿、ANR等场景,具有函数耗时统计的功能。
为了实现函数的耗时统计,一般的作法都是在函数执行开始和结束为止进行插桩,最后以两个插桩点的时间差为函数的执行时间。
# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//入口插桩
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
...
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
//出口插桩
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
复制代码
整体上就是每一个方法的开头和结尾处各添加一行代码,而后交由TraceMethod进行统计和计算。
详情见旧文Matrix系列文章(一) 卡顿分析工具之Trace Canary。
接下来,咱们分析一下ASM的不足。
更多详情参见旧文 Android ASM框架详解 。
javassit是一个开源的字节码建立、编辑类库,现属于Jboss web容器的一个子模块,特色是简单、快速,与AspectJ同样,使用它不须要了解字节码和虚拟机指令,这里是官方文档。
javassit核心的类库包含ClassPool,CtClass ,CtMethod和CtField。
javassit API简洁直观,好比咱们想动态建立一个类,并添加一个helloWorld方法。
ClassPool pool = ClassPool.getDefault();
//经过makeClass建立类
CtClass ct = pool.makeClass("test.helloworld.Test");//建立类
//为ct添加一个方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//写入文件
ct.writeFile();
//加载进内存
// ct.toClass();
复制代码
而后,咱们想在helloWorld方法先后织入代码。
ClassPool pool = ClassPool.getDefault();
//获取class
CtClass ct = pool.getCtClass("test.helloworld.Test");
//获取helloWorld方法
CtMethod m = ct.getDeclaredMethod("helloWorld");
//在方法开头织入
m.insertBefore("{ System.out.print(\"before insert\");");
//在方法末尾织入 可以使用this关键字
m.insertAfter("{System.out.println(this.x); }");
//写入文件
ct.writeFile();
复制代码
javassit的语法直观简洁的特色,使得在不少开源项目中都有它的身影。
好比QQ zone的热修复方案,当时遇到的问题是补丁包加载作odex优化时,因为差分的patch包并不依赖其余dex,致使补丁包中的类被打上is_preverfied标签(这有助于运行时提高性能),但在补丁运行时实际会去引用其余dex中的类,就会抛出错误java.lang.IllegalAccessError:Class ref pre-verified class resovled to unexpected implement。
当时qq空间团队的解决方案是在编译阶段为对全部类的构造方法进行插桩,引用一个事先定义好的AnalyseLoad类,而后干预分包过程,让这个类处于一个独立的dex中,这样就避免了上述问题。
这里用的AOP方案就是javassit,详情见 QQ空间补丁方案解析 。
还有最近开源的插件化框架 shadow,shadow框架中的一个需求是,插件包具有独立运行的能力,当运行插件工程时,插件中Activity的父类ShadowActivity继承Activity,当插件做为子模块加载到插件中时ShadowActivity没必要继承系统Activity,只是做为一个代理类就够了。此时shadow团队封装了JavassistTransform,在编译期动态修改Activity的父类。
动态代理是代理模式的一种实现,用于在运行时动态加强原始类的行为,实现方式是运行时直接生成class字节码并将其加载进虚拟机。
JDK自己就提供一个Proxy类用于实现动态代理。 咱们一般使用下面的API建立代理类。
# java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
复制代码
其中在InvocationHandler实现类中定义核心切点代码。
public class InvocationHandlerImpl implements InvocationHandler {
/** 被代理的实例 */
private Object mObj = null;
public InvocationHandlerImpl(Object obj){
this.mObj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前切入点
Object result = method.invoke(this.mObj, args);
//后切入点
return result;
}
}
复制代码
这样在先后切入点的位置能够编写要织入的代码。
在咱们经常使用的Retrofit框架中就用到了动态代理。Retrofit提供了一套易于开发网络请求的注解,而在注解中声明的参数正是经过代理包装以后发出的网络请求。
# Retrofit.create
public <T> T create(final Class<T> service) {
...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//代理
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
复制代码
java动态代理最大的问题是只能代理接口,而不能代理普通类或者抽象类,这是由于默认建立的代理类继承Porxy,而java又不支持多继承,这一点极大的限制了动态代理的使用场景,cglib可代理普通类。
更多详情参见 设计模式之代理模式 。
最后咱们总结一下 上述AOP框架的特色及优劣势,你能够根据自身需求进行技术选型。
技术框架 | 特色 | 开发难度 | 优点 | 不足 |
---|---|---|---|---|
APT | 经常使用于经过注解减小模板代码,对类的建立于加强须要依赖其余框架。 | ★★ | 开发注解简化上层编码。 | 使用注解对原工程具备侵入性。 |
AspectJ | 提供完整的面向切面编程的注解。 | ★★ | 真正意义的AOP,支持通配、继承结构的AOP,无需硬编码切面。 | 重复织入、不织入问题,不支持java8 |
ASM | 面向字节码指令编程,功能强大。 | ★★★ | 高效,ASM5开始支持java8。 | 切面能力不足,部分场景需硬编码。 |
Javassit | API简洁易懂,快速开发。 | ★ | 上手快,新人友好,具有运行时加载class能力。 | 切点代码编写需注意class path加载问题。 |
java动态代理 | 运行时扩展代理接口功能。 | ★ | 运行时动态加强。 | 仅支持代理接口,扩展性差,使用反射性能差。 |