Android编译期插桩,让程序自己写代码(一) 中我介绍了 APT
技术。
Android编译期插桩,让程序自己写代码(二) 中我介绍了 AspectJ
技术。
本文是这一系列的最后一篇,介绍如何使用 Javassist
在编译期生成字节码。老规矩,直接上图。
Javassist
是一个可以方便操作Java字节码的库,它使Java程序能够在运行时新增或修改Class。 Javassist
直接生成二进制class文件。操作字节码, Javassist
并不是唯一选择,常用的还有 ASM
。相较于 ASM
, Javassist
效率更低。但是, Javassist
提供了更友好的API,开发者们可以在不了解字节码的情况下使用它。这一点, ASM
是做不到。 Javassist
非常简单,我们通过两个例子直观的感受一下。
这个例子演示了如何通过 Javassist
生成一个class二进制文件。
public class Main { static ClassPool sClassPool = ClassPool.getDefault(); public static void main(String[] args) throws Exception { //构造新的Class MyThread。 CtClass myThread = sClassPool.makeClass("com.javassist.example.MyThread"); //设置MyThread为public的 myThread.setModifiers(Modifier.PUBLIC); //继承Thread myThread.setSuperclass(sClassPool.getCtClass("java.lang.Thread")); //实现Cloneable接口 myThread.addInterface(sClassPool.get("java.lang.Cloneable")); //生成私有成员变量i CtField ctField = new CtField(CtClass.intType,"i",myThread); ctField.setModifiers(Modifier.PRIVATE); myThread.addField(ctField); //生成构造方法 CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType}, myThread); constructor.setBody("this.i = $1;"); myThread.addConstructor(constructor); //构造run方法的方法声明 CtMethod runMethod = new CtMethod(CtClass.voidType,"run",null,myThread); runMethod.setModifiers(Modifier.PROTECTED); //为run方法添加注Override注解 ClassFile classFile = myThread.getClassFile(); ConstPool constPool = classFile.getConstPool(); AnnotationsAttribute overrideAnnotation = new AnnotationsAttribute(constPool,AnnotationsAttribute.visibleTag); overrideAnnotation.addAnnotation(new Annotation("Override",constPool)); runMethod.getMethodInfo().addAttribute(overrideAnnotation); //构造run方法的方法体。 runMethod.setBody("while (true){" + " try {" + " Thread.sleep(1000L);" + " } catch (InterruptedException e) {" + " e.printStackTrace();" + " }" + " i++;" + " }"); myThread.addMethod(runMethod); //输出文件到当前目录 myThread.writeFile(System.getProperty("user.dir")); } } 复制代码
运行程序,当前项目下生成了以下内容:
反编译 MyThread.class
,内容如下:
package com.javassist.example; public class MyThread extends Thread implements Cloneable { private int i; public MyThread(int var1) { this.i = var1; } @Override protected void run() { while(true) { try { Thread.sleep(1000L); } catch (InterruptedException var2) { var2.printStackTrace(); } ++this.i; } } } 复制代码
这个例子演示如何修改class字节码。我们为第一个例子中生成的MyTread.class扩展一些功能。
public class Main { static ClassPool sClassPool = ClassPool.getDefault(); public static void main(String[] args) throws Exception { //为ClassPool指定搜索路径。 sClassPool.insertClassPath(System.getProperty("user.dir")); //获取MyThread CtClass myThread = sClassPool.get("com.javassist.example.MyThread"); //将成员变量i变成静态的 CtField iField = myThread.getField("i"); iField.setModifiers(Modifier.STATIC|Modifier.PRIVATE); //获取run方法 CtMethod runMethod = myThread.getDeclaredMethod("run"); //在run方法开始处插入代码。 runMethod.insertBefore("System.out.println(/"开始执行/");"); //输出新的二进制文件 myThread.writeFile(System.getProperty("user.dir")); } } 复制代码
运行,再反编译 MyThread.class
,结果如下:
package com.javassist.example; public class MyThread extends Thread implements Cloneable { private static int i; public MyThread(int var1) { this.i = var1; } @Override protected void run() { System.out.println("开始执行"); while(true) { try { Thread.sleep(1000L); } catch (InterruptedException var2) { var2.printStackTrace(); } ++this.i; } } } 复制代码
编译期插桩对于 Javassist
的要求并不高,掌握了上面两个例子就可以实现我们的大部分的需求了。如果你想了解更高级的用法,请移步 这里 。接下来,我只介绍两个类: CtClass 和 ClassPool 。
CtClass
表示字节码中的一个类。CtClass为我们提供了可以构造一个完整 Class
的API,例如继承父类、实现接口、增加字段、增加方法等。除此之外, CtClass
还提供了 writeFile()
方法,方便我们直接输出二进制文件。
ClassPool
是CtClass的容器。 ClassPool
可以新建(makeClass)或获取(get) CtClass
对象。在获取CtClass对象时,即调用 ClassPool.get()
方法,需要在 ClassPool
中指定查找路径。否则,ClassPool怎么知道去哪里加载字节码文件呢。ClassPool通过链表维护这些查找路径,我们可以通过 insertClassPath()
将路径插入到链表的表头,通过 appendClassPath()
插入到链表表尾。
Javassist
只是操作字节码的工具。要实现编译期生成字节码还需要 Android Gradle
为我们提供入口,而 Transform
就是这个入口。接下来我们进入了 Transform
环节。
Transform是Android Gradle提供的,可以操作字节码的一种方式。App编译时,我们的源代码首先会被编译成class,然后再被编译成dex。在class编译成dex的过程中,会经过一系列 Transform
处理。
上图是Android Gradle定义的一系列 Transform
。 Jacoco
、 Proguard
、 InstantRun
、 Muti-Dex
等功能都是通过继承Transform实现的。当前,我们也可以自定义 Transform
。
我们先来了解多个 Transform
是如何配合工作的。直接上图。
Transform
之间采用流式处理方式。每个 Transform
需要一个输入,处理完成后产生一个输出,而这个输出又会作为下一个 Transform
的输入。就这样,所有的 Transform
依次完成自己的使命。
Transform
的输入和输出都是一个个的class/jar文件。
Transform
接收输入时,会把接收的内容封装到一个TransformInput集合中。 TransformInput
由一个JarInput集合和一个DirectoryInput集合组成。 JarInput
代表Jar文件, DirectoryInput
代表目录。
Transform
的输出路径是不允许我们自由指定的,必须根据名称、作用范围、类型等由 TransformOutputProvider 生成。具体代码如下:
String dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) 复制代码
我们先看一下继承 Transform
需要实现的方法。
public class CustomCodeTransform extends Transform { @Override public String getName() { return null; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return null; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return null; } @Override public boolean isIncremental() { return false; } @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation); } } 复制代码
getName():给 Transform
起一个名字。
getInputTypes(): Transform
要处理的输入类型。DefaultContentType提供了两种类型的输入方式:
TransformManager
为我们封装了InputTypes。具体如下:
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES); public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES); public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES); 复制代码
getScopes(): Transform
的处理范围。它约定了 Input
的接收范围。Scope中定义了以下几种范围:
TransformManager
也为我们封装了常用的Scope。具体如下:
public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT); public static final Set<Scope> SCOPE_FULL_PROJECT = Sets.immutableEnumSet( Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES); public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING = new ImmutableSet.Builder<ScopeType>() .addAll(SCOPE_FULL_PROJECT) .add(InternalScope.MAIN_SPLIT) .build(); public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS = ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS); 复制代码
isIncremental():是否支持增量更新。
transform():这里就是我们具体的处理逻辑。通过参数TransformInvocation,我们可以获得输入,也可以获取决定输出的 TransformOutputProvider
。
public interface TransformInvocation { /** * Returns the inputs/outputs of the transform. * @return the inputs/outputs of the transform. */ @NonNull Collection<TransformInput> getInputs(); /** * Returns the output provider allowing to create content. * @return he output provider allowing to create content. */ @Nullable TransformOutputProvider getOutputProvider(); } 复制代码
下面到了集成Transform环节。集成Transform需要自定义gradle 插件。写给 Android 开发者的 Gradle 系列(三)撰写 plugin介绍了自定义gradle插件的步骤,我们跟着它就可以实现一个插件。然后就可以将CustomCodeTransform注册到gradle的编译流程了。
class CustomCodePlugin implements Plugin<Project> { @Override void apply(Project project) { AppExtension android = project.getExtensions().getByType(AppExtension.class); android.registerTransform(new RegisterTransform()); } } 复制代码
在Android领域,组件化经过多年的发展,已经成为一种非常成熟的技术。组件化是一种项目架构,它将一个app项目拆分成多个组件,而各个组件间互不依赖。
既然组件间是互不依赖的,那么它们就不能像普通项目那样进行Activity跳转。那应该怎么办呢?下面我们就来具体了学习一下。
我们的Activity路由框架有两个module组成。一个module用来提供API,我们命名为 common
;另一个module用来处理编译时字节码的注入,我们命名为 plugin
。
我们先来看一下 common
。它只有两个类,如下:
public interface IRouter { void register(Map<String,Class> routerMap); } 复制代码
public class Router { private static Router INSTANCE; private Map<String, Class> mRouterMap = new ConcurrentHashMap<>(); //单例 private static Router getInstance() { if (INSTANCE == null) { synchronized (Router.class) { if (INSTANCE == null) { INSTANCE = new Router(); } } } return INSTANCE; } private Router() { init(); } //在这里字节码注入。 private void init() { } /** * Activity跳转 * @param context * @param activityUrl Activity路由路径。 */ public static void startActivity(Context context, String activityUrl) { Router router = getInstance(); Class<?> targetActivityClass = router.mRouterMap.get(activityUrl); Intent intent = new Intent(context,targetActivityClass); context.startActivity(intent); } } 复制代码
common
的这两个类十分简单。 IRouter
是一个接口。 Router
对外的方法只有一个 startActivity
。
接下来,我们跳过 plugin
,先学习一下框架怎么使用。假如我们的项目被拆分成app、A、B三个module。其中app是一个壳工程,只负责打包,依赖于A、B。A和B是普通的业务组件,A、B之间互不依赖。现在,A组件中有一个AActivity,B组件想跳转到AActivity。怎么做呢?
在A组件中新建一个 ARouterImpl
实现 IRouter
。
public class ARouterImpl implements IRouter { private static final String AActivity_PATH = "router://a_activity"; @Override public void register(Map<String, Class> routerMap) { routerMap.put(AActivity_PATH, AActivity.class); } } 复制代码
在B组件中调用时,只需要
Router.startActivity(context,"router://a_activity"); 复制代码
是不是很神奇?其实奥妙就在 plugin
中。编译时, plugin
在 Router
的 init()
中注入了如下代码:
private void init() { ARouterImpl var1 = new ARouterImpl(); var.register(mRouterMap); } 复制代码
plugin
中的代码有点多,我就不贴出来了。这一节的代码都在 这里 。
这个Demo非常简单,但是它对于理解ARouter、WMRouter等路由框架的原理十分有用。它们在处理路由表的注册时,都是采用编译期字节码注入的方式,只不过它们没有使用 javassit
,而是使用了效率更高的 ASM
。它们用起来更方便是因为,它们利用APT技术把路径和Activity之间的映射变透明了。即:类似于Demo中的 ARouterImpl
这种代码,都是通过APT生成的。