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生成的。