前言
近些年,编译期插桩技术在Android圈越来越普遍。无论是可以生成JAVA源码的ButterKnief、Dagger,还是操作字节码的VirtualAPK,甚至是新兴的语言Kotlin都用到了编译期插桩技术。学习这门技术对我们理解这些框架的原理十分有帮助。另外,我们通过这种技术可以抽离出复杂、重复的代码,降低程序耦合性,提高代码的可复用性,提高开发效率。因此,了解编译期插桩技术十分必要。在介绍这项技术之前,我们先来了解一下Android代码的编译过程以及插桩位置。话不多说,直接上图。
APT
APT(Annotation Processing Tool)是一种编译期注解处理器。它通过定义注解和处理器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成.class文件。
代表框架:ButterKnife、Dagger、ARouter、EventBus3、DataBinding、AndroidAnnotation等。
在介绍如何应用APT技术之前,我们先来了解一些相关的知识。
一、Element
1.简介
Element
是一种在编译期描述.java文件静态结构的一种类型,它可能表示一个package、一个class、一个method或者一个field。Element
的比较应该使用equals
,因为编译期间同一个Element
可能会用两个对象表示。JDK提供了以下5种Element
。
2.Element的存储结构
编译器采用类似Html的Dom树来存储Element。我们用下面的Test.java
来具体说明。
//PackageElement
package me.zhangkuo.compile;
//TypeElement
public class Test {
//VariableElement
private String name;
//ExecutableElement
private Test(){
}
//ExecutableElement
public void setName(/* TypeParameterElement */ String name) {
this.name = name;
}
}
Test.java
用Element树结构描述如下:
我们可以看到 setName(String name)
的ExecutableElement
中并没有子节点TypeParameterElement
。这是因为TypeParameterElement
没有被纳入到Element
树中。不过我们可以通过ExecutableElement
的getTypeParameters()
方法来获取。
此外,再给大家介绍两个Element中十分有用的方法。
public interface Element extends AnnotatedConstruct {
//获取父Element
Element getEnclosingElement();
//获取子Element的集合
List<? extends Element> getEnclosedElements();
}
二、TypeMirror
Element
有一个asType()
方法用来返回TypeMirror
。TypeMirror
表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void
的伪类型。我们一般用TypeMirror进行类型判断。如下段代码,用来比较元素所描述的类型是否是Activity的子类。
/**
* 类型相关工具类
*/
private Types typeUtils;
/**
* 元素相关的工具类
*/
private Elements elementUtils;
private static final String ACTIVITY_TYPE = "android.app.Activity";
private boolean isSubActivity(Element element){
//获取当前元素的TypeMirror
TypeMirror elementTypeMirror = element.asType();
//通过工具类Elements获取Activity的Element,并转换为TypeMirror
TypeMirror viewTypeMirror = elementUtils.getTypeElement(ACTIVITY_TYPE).asType();
//用工具类typeUtils判断两者间的关系
return typeUtils.isSubtype(elementTypeMirror,viewTypeMirror)
}
三、一个简单的ButterKnife
这一节我们通过编写一个简单的ButterKnife
来介绍一下如何编写一个APT框架。APT应该是编译期插桩最简单的一种技术,通过三步就可以完成。
- 定义编译期注解。
我们新增一个Java Library Module命名为apt_api
,编写注解类BindView。
@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}
这里简单介绍一下RetentionPolicy
。RetentionPolicy
是一个枚举,它的值有三种:SOURCE、CLASS、RUNTIME。
- SOURCE:不参与编译,让开发者使用。
- CLASS:参与编译,运行时不可见。给编译器使用。
- RUNTIME:参与编译,运行时可见。给编译器和JVM使用。
- 定义注解处理器。
同样,我们需要新增一个Java Library Module命名为apt_processor
。
我们需要引入两个必要的依赖:一个是我们新增的module apt_annotation
,另一个是google的com.google.auto.service:auto-service:1.0-rc3
(以下简称auto-service
)。
implementation project(':apt_api') api 'com.google.auto.service:auto-service:1.0-rc3'
新增一个类 ButterKnifeProcessor
,继承 AbstractProcessor
。
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
/**
* 元素相关的工具类
*/
private Elements elementUtils;
/**
* 文件相关的工具类
*/
private Filer filer;
/**
* 日志相关的工具类
*/
private Messager messager;
/**
* 类型相关工具类
*/
private Types typeUtils;
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
typeUtils = processingEnv.getTypeUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
auto-service
为我们简化了定义注解处理器的流程。@AutoService
是就是由auto-service
提供的,其作用是用来告诉编译器我们定义的ButterKnifeProcessor
是一个编译期注解处理器。这样在编译时ButterKnifeProcessor
才会被调用。
我们还重写了AbstractProcessor提供的四个方法:getSupportedAnnotationTypes
、getSupportedSourceVersion
、init
、process
。
-
getSupportedAnnotationTypes
表示处理器可以处理哪些注解。这里返回的是我们之前定义的BindView。除了重写方法之外,还可用通过注解来实现。@SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"})
-
getSupportedSourceVersion
表示处理器可以处理的Java版本。这里我们采用最新的JDK版本就可以了。同样,我们也可以通过注解来实现。@SupportedSourceVersion(value = SourceVersion.latestSupported())
-
init
方法主要用来做一些准备工作。我们一般在这里初始化几个工具类。上述代码我们初始了与元素相关的工具类elementUtils
、与日志相关的工具类messager
、与文件相关的filer
以及与类型相关工具类typeUtils
。我们接下来会看到process
主要就是通过这几个类来生成代码的。 -
process
用来完成具体的程序写代码功能。在具体介绍process
之前,请允许我先推荐一个库:javapoet。javapoet
是由神奇的square
公司开源的,它提供了非常人性化的api,来帮助开发者生成.java源文件。它的README.md
文件为我们提供了丰富的例子,是我们学习的主要工具。private Map<TypeElement, List<Element>> elementPackage = new HashMap<>(); private static final String VIEW_TYPE = "android.view.View"; private static final String VIEW_BINDER = "me.zhangkuo.apt.ViewBinding"; @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (set == null || set.isEmpty()) { return false; } elementPackage.clear(); Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class); //收集数据放入elementPackage中 collectData(bindViewElement); //根据elementPackage中的数据生成.java代码 generateCode(); return true; } private void collectData(Set<? extends Element> elements){ Iterator<? extends Element> iterable = elements.iterator(); while (iterable.hasNext()) { Element element = iterable.next(); TypeMirror elementTypeMirror = element.asType(); //判断元素的类型是否是View或者是View的子类型。 TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType(); if (typeUtils.isSubtype(elementTypeMirror, viewTypeMirror) || typeUtils.isSameType(elementTypeMirror, viewTypeMirror)) { //找到父元素,这里认为是@BindView标记字段所在的类。 TypeElement parent = (TypeElement) element.getEnclosingElement(); //根据parent不同存储的List中 List<Element> parentElements = elementPackage.get(parent); if (parentElements == null) { parentElements = new ArrayList<>(); elementPackage.put(parent, parentElements); } parentElements.add(element); }else{ throw new RuntimeException("错误处理,BindView应该标注在类型是View的字段上"); } } } private void generateCode(){ Set<Map.Entry<TypeElement,List<Element>>> entries = elementPackage.entrySet(); Iterator<Map.Entry<TypeElement,List<Element>>> iterator = entries.iterator(); while (iterator.hasNext()){ Map.Entry<TypeElement,List<Element>> entry = iterator.next(); //类元素 TypeElement parent = entry.getKey(); //当前类元素下,注解了BindView的元素 List<Element> elements = entry.getValue(); //通过JavaPoet生成bindView的MethodSpec MethodSpec methodSpec = generateBindViewMethod(parent,elements); String packageName = getPackage(parent).getQualifiedName().toString(); ClassName viewBinderInterface = ClassName.get(elementUtils.getTypeElement(VIEW_BINDER)); String className = parent.getQualifiedName().toString().substring( packageName.length() + 1).replace('.', '$'); ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding"); try { //生成 className_ViewBinding.java文件 JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName) .addModifiers(PUBLIC) .addSuperinterface(viewBinderInterface) .addMethod(methodSpec) .build() ).build().writeTo(filer); } catch (IOException e) { e.printStackTrace(); } } } private MethodSpec generateBindViewMethod(TypeElement parent,List<Element> elementList) { ParameterSpec.Builder parameter = ParameterSpec.builder(TypeName.OBJECT, "target"); MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView"); bindViewMethod.addParameter(parameter.build()); bindViewMethod.addModifiers(Modifier.PUBLIC); bindViewMethod.addStatement("$T temp = ($T)target",parent,parent); for (Element element : elementList) { int id = element.getAnnotation(BindView.class).value(); bindViewMethod.addStatement("temp.$N = temp.findViewById($L)", element.getSimpleName().toString(), id); } return bindViewMethod.build(); }
process的
代码比较长,但是它的逻辑非常简单看,主要分为收集数据和生成代码两部分。我为关键的地方都加了注释,就不再详细解释了。到这里我们基本上完成了注解器的编写工作 -
使用注解
在build.gradle中引入我们定义的注解和注解处理器。
implementation project(':apt_api') annotationProcessor project(":apt_processor")
应用注解
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_content)
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
tvContent.setText("这就是ButterKnife的原理");
}
}
到这里,这篇文件就结束了。什么?你还没说ButterKnife
这个类呢。好吧,这个真的很简单,直接贴代码吧。
public class ButterKnife {
static final Map<Class<?>, Constructor<? extends ViewBinding>> BINDINGS = new LinkedHashMap<>();
public static void inject(Object object) {
if (object == null) {
return;
}
try {
Class<?> cls = object.getClass();
Constructor<? extends ViewBinding> constructor = findBindingConstructorForClass(cls);
ViewBinding viewBinding = constructor.newInstance();
viewBinding.bindView(object);
} catch (Exception e) {
}
}
private static Constructor<? extends ViewBinding> findBindingConstructorForClass(Class<?> cls) throws Exception {
Constructor<? extends ViewBinding> constructor = BINDINGS.get(cls);
if (constructor == null) {
String className = cls.getName();
Class<?> bindingClass = cls.getClassLoader().loadClass(className + "_ViewBinding");
constructor = (Constructor<? extends ViewBinding>) bindingClass.getConstructor();
BINDINGS.put(cls, constructor);
}
return constructor;
}
}
Android编译期插桩,让程序自己写代码(二)
AspectJ
在上图中,我们可以清楚的看到AspectJ的插桩位置是.java与.class之间。这很容易使人联想到编译器。事实上,AspectJ就是一种编译器,它在Java编译器的基础上增加了关键字识别和编译方法。因此,AspectJ可以编译Java代码。它还提供了Aspect程序。在编译期间,将开发者编写的Aspect程序织入到目标程序中,扩展目标程序的功能。
AspectJ可以应用于Android和后端开发中。在后端,AspectJ 应用更为广泛一些,著名的Spring框架就对AspectJ提供了支持。不过,近些年,AspectJ技术在Android领域也开始崭露头角,比较知名的有JakeWharton的hugo。另外,一些企业也开始探索AspectJ在埋点、权限管理等方面的应用。
关于AspectJ更为详细的介绍,请大家移步邓平凡大神的博客深入理解Android之AOP。这篇文章对于初次接触AspectJ的人来说十分友好,笔者最初就是通过它进入AspectJ殿堂的。珠玉在前,本文就不再介绍AspectJ的基础知识了。那本文要说些什么呢?
- 一个简单的Hugo框架。
- 从字节码分析AspectJ。
Hugo
Hugo是JakeWharton基于AspectJ开源的一个调试框架,其功能是通过注解的方式可以打印出方法的运行时间,方便开发者性能调优。今天,我们就来看一下它的庐山真面目。
配置AspectJ
Hugo是基于AspectJ的,那首先我们就要支持AspectJ。这里向大家推荐沪江的AspectJX,它不仅使用简单,而且还支持过滤一些aar或jar包。
首先我们需要在根build.gradle中依赖AspectJX
dependencies { classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4' }
在app项目的build.gradle里应用插件,并添加aspectj的依赖库
apply plugin: 'android-aspectjx'
api 'org.aspectj:aspectjrt:1.8.9'
这样就配置完成了,是不是很简单啊。
注意:笔者这里采用的gradle版本是3.0.1,如果没有编译通过,检查一下gradle版本。
定义DebugLog注解
十分简单,直接上代码
@Target({METHOD, CONSTRUCTOR})
@Retention(CLASS)
public @interface DebugLog {
}
编写Aspect
@Aspect
public class Hugo {
@Pointcut("execution(@com.hugo.example.lib.DebugLog * *(..))")
public void method() {}
@Pointcut("execution(@com.hugo.example.lib.DebugLog *.new(..))")
public void constructor() {}
@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
Class<?> cls = codeSignature.getDeclaringType();
String methodName = codeSignature.getName();
long startNanos = System.nanoTime();
Object result = joinPoint.proceed();
long stopNanos = System.nanoTime();
long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
StringBuilder builder = new StringBuilder();
builder.append("methodName:")
.append(methodName)
.append(" ---- executeTime:")
.append(lengthMillis);
Log.e(asTag(cls), builder.toString());
return result;
}
private static String asTag(Class<?> cls) {
if (cls.isAnonymousClass()) {
return asTag(cls.getEnclosingClass());
}
return cls.getSimpleName();
}
}
如果你学习了深入理解Android之AOP,那么这段代码应该很容易能看懂。这里我简单解释一下。
- 选取注解了DebugLog的method和constructor作为pointcut。
- 在原方法执行前织入开始时间,在原方法执行后织入结束时间,并计算出运行时间。通过Log.v打印出来。
到此,这个简化版的Hugo基本就介绍完了。你可以做个Demo试一下了。
- 笔者DebugLog的包名是com.hugo.example.lib。因此在method()和constractor()中的注解内容是com.hugo.example.lib.DebugLog。
- 其实,真正的Hugo框架核心也只有一个类。它只是在日志打印时,输出了更多的方法信息。本文为了方便读者理解作了简化。
测试
我们定义一个Test类,然后在Activity启动的时候调用myThread()方法。Test类如下:
public class Test {
@DebugLog
public void myMethod1() throws Exception{
Thread.sleep(1000);
}
}
我们看一下日志,方法的运行时间被完美的打印出来了。
从字节码分析AspectJ
我们仍然以Test为例,看一下Test反编之后的字节码。
反编译的内容看起来不太方便,我在这里把它转换成了如下代码:
为了观看方便,上图将代码分为4部分。
我们先看第一部分,这是一个静态代码块,也就是说在类加载的时候,程序会AspectJ提供的Factory类,创建一个类型为JoinPoint.StaticPart静态实例STATIC_PART。深入理解Android之AOP中对JoinPoint.StaticPart介绍如下:
thisJoinPointStaticPart对象:在advice代码中可直接使用,代表JPoint中那些不变的东西。比如这个JPoint的类型,JPoint所处的代码位置等。这里thisJoinPointStaticPart就是代码中的JoinPoint.StaticPart。
第二部分是我们之前定义的myThread方法,它在编译期间被替换了。在运行时,它首先通过Factory的静态方法makeJP创建一个JoinPoint对象。makeJp是一个重载方法,我们看一下。
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target) {
return new JoinPointImpl(staticPart, _this, target, NO_ARGS);
}
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target, Object[] args) {
return new JoinPointImpl(staticPart, _this, target, args);
}
通过我列出来了两个,可以看到JoinPoint除了包含了我们第一步中提到了STATIC_PART对象,还包括了this,target对象,以及方法参数。这和深入理解Android之AOP中对thisJoinpoint描述也是一致的:
thisJoinpoint对象:在advice代码中可直接使用。代表JPoint每次被触发时的一些动态信息,比如参数啊之类的。
创建完JoinPoint对象后,随后调用了第三部分中的advice()方法。advice()方法大部分都是我们在Hugo中编写的织入代码,这里只有一个不同,那就是joinPoint.proceed()不见了,替换成了源代码中具体的处理逻辑。
总结
通过上述分析,我们可以清楚的感知到AspectJ提供了非常强大的功能。但同时,由于其为每个切入点生成一个JoinPoint.StaticPar静态实例和在运行过程中生成的JoinPoint以及一些其它的封装,这必然会导致程序在内存和处理速度等方面受影响。因此,在小范围内使用AspectJ是可以的,但是如果涉及范围较大就要慎重考虑了。
Android编译期插桩,让程序自己写代码(三)
本文是这一系列的最后一篇,介绍如何使用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 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组件中调用时,只需要
outer.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生成的。