谈谈 Android AOP 技术方案(3)

javassit

javassit 是一个开源的字节码创建、编辑类库,现属于 Jboss web 容器的一个子模块,特点是简单、快速,与 AspectJ 一样,使用它不需要了解字节码和虚拟机指令,这里是官方文档

javassit 核心的类库包含 ClassPool,CtClass ,CtMethod 和 CtField。

  • ClassPool:一个基于 HashMap 实现的 CtClass 对象容器。
  • CtClass:表示一个类,可从 ClassPool 中通过完整类名获取。
  • CtMethods:表示类中的方法。
  • CtFields :表示类中的字段。

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 的父类。

详见 调试研究 Shadow 对字节码编辑的正确姿势 

动态代理

动态代理是代理模式的一种实现,用于在运行时动态增强原始类的行为,实现方式是运行时直接生成 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,无需硬编码切面。 重复织入、不织入问题
ASM 面向字节码指令编程,功能强大。 ★★★ 高效,ASM5 开始支持 java8。 切面能力不足,部分场景需硬编码。
Javassit API 简洁易懂,快速开发。 上手快,新人友好,具备运行时加载 class 能力。 切点代码编写需注意 class path 加载问题。
java 动态代理 运行时扩展代理接口功能。 运行时动态增强。 仅支持代理接口,扩展性差,使用反射性能差。