Android编译期插桩,让程序本身写代码(一)中我介绍了APT
技术。html
Android编译期插桩,让程序本身写代码(二)中我介绍了AspectJ
技术。java
本文是这一系列的最后一篇,介绍如何使用Javassist
在编译期生成字节码。老规矩,直接上图。android
Javassist
是一个可以很是方便操做字节码的库。它使Java程序可以在运行时新增或修改类。操做字节码,Javassist
并非惟一选择,经常使用的还有ASM
。相较于ASM
,Javassist
效率更低。可是,Javassist
提供了更友好的API,开发者们能够在不了解字节码的状况下使用它。这一点,ASM
是作不到。Javassist
很是简单,咱们经过两个例子直观的感觉一下。git
这个例子演示了如何经过Javassist
生成一个class二进制文件。github
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"));
}
}
复制代码
运行程序,当前项目下生成了如下内容:api
反编译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扩展一些功能。app
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。ide
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路由框架有两个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生成的。