前言
Android编译期插桩,让程序自己写代码(一)中我介绍了APT
技术。
Android编译期插桩,让程序自己写代码(二)中我介绍了AspectJ
技术。
本文是这一系列的最后一篇,介绍如何使用Javassist
在编译期生成字节码。老规矩,直接上图。
一、Javassist
Javassist
是一个能够非常方便操作字节码的库。它使Java程序能够在运行时新增或修改类。操作字节码,Javassist
并不是唯一选择,常用的还有ASM
。相较于ASM
,Javassist
效率更低。但是,Javassist
提供了更友好的API,开发者们可以在不了解字节码的情况下使用它。这一点,ASM
是做不到。Javassist
非常简单,我们通过两个例子直观的感受一下。
1.1 第一个例子
这个例子演示了如何通过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;
}
}
}
复制代码
1.2 第二个例子
这个例子演示如何修改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。
1.3 CtClass
CtClass
表示字节码中的一个类。CtClass为我们提供了可以构造一个完整Class
的API,例如继承父类、实现接口、增加字段、增加方法等。除此之外,CtClass
还提供了writeFile()
方法,方便我们直接输出二进制文件。
1.4 ClassPool
ClassPool
是CtClass的容器。ClassPool
可以新建(makeClass)或获取(get)CtClass
对象。在获取CtClass对象时,即调用ClassPool.get()
方法,需要在ClassPool
中指定查找路径。否则,ClassPool怎么知道去哪里加载字节码文件呢。ClassPool通过链表维护这些查找路径,我们可以通过insertClassPath()
\appendClassPath()
将路径插入到链表的表头\表尾。
Javassist
只是操作字节码的工具。要实现编译期生成字节码还需要Android Gradle
为我们提供入口,而Transform
就是这个入口。接下来我们进入了Transform
环节。
二、Transform
Transform是Android Gradle提供的,可以操作字节码的一种方式。App编译时,我们的源代码首先会被编译成class,然后再被编译成dex。在class编译成dex的过程中,会经过一系列Transform
处理。
上图是Android Gradle定义的一系列Transform
。Jacoco
、Proguard
、InstantRun
、Muti-Dex
等功能都是通过继承Transform实现的。当前,我们也可以自定义Transform
。
2.1 Transform的工作原理
我们先来了解多个Transform
是如何配合工作的。直接上图。
Transform
之间采用流式处理方式。每个Transform
需要一个输入,处理完成后产生一个输出,而这个输出又会作为下一个Transform
的输入。就这样,所有的Transform
依次完成自己的使命。
Transform
的输入和输出都是一个个的class/jar文件。
2.1.1 输入(Input)
Transform
接收输入时,会把接收的内容封装到一个TransformInput集合中。TransformInput
由一个JarInput集合和一个DirectoryInput集合组成。JarInput
代表Jar文件,DirectoryInput
代表目录。
2.1.2 输出(Output)
Transform
的输出路径是不允许我们自由指定的,必须根据名称、作用范围、类型等由TransformOutputProvider生成。具体代码如下:
String dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
复制代码
2.2 自定义Transform
2.2.1 继承Transform
我们先看一下继承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提供了两种类型的输入方式:- CLASSES: java编译后的字节码,可能是jar包也可能是目录。
- RESOURCES: 标注的Java资源。
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中定义了以下几种范围:- PROJECT: 只处理当前项目。
- SUB_PROJECTS: 只处理子项目。
- PROJECT_LOCAL_DEPS: 只处理项目本地依赖库(本地jars、aar)。
- PROVIDED_ONLY: 只处理以provided方式提供的依赖库。
- EXTERNAL_LIBRARIES: 只处理所有外部依赖库。
- SUB_PROJECTS_LOCAL_DEPS: 只处理子项目的本地依赖库(本地jars、aar)
- TESTED_CODE: 只处理测试代码。
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(); } 复制代码
2.2.2自定义插件,集成Transform
下面到了集成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());
}
}
复制代码
三、一个简易的组件化Activity路由框架
在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生成的。