Android编译期插桩,让程序自己写代码

前言

近些年,编译期插桩技术在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树中。不过我们可以通过ExecutableElementgetTypeParameters()方法来获取。

此外,再给大家介绍两个Element中十分有用的方法。

public interface Element extends AnnotatedConstruct {
    //获取父Element
    Element getEnclosingElement();
    //获取子Element的集合
    List<? extends Element> getEnclosedElements();
}
二、TypeMirror

Element有一个asType()方法用来返回TypeMirrorTypeMirror表示 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 {
}

这里简单介绍一下RetentionPolicyRetentionPolicy是一个枚举,它的值有三种: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提供的四个方法:getSupportedAnnotationTypesgetSupportedSourceVersioninitprocess

  • getSupportedAnnotationTypes表示处理器可以处理哪些注解。这里返回的是我们之前定义的BindView。除了重写方法之外,还可用通过注解来实现。

    @SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"})
    
  • getSupportedSourceVersion表示处理器可以处理的Java版本。这里我们采用最新的JDK版本就可以了。同样,我们也可以通过注解来实现。

    @SupportedSourceVersion(value = SourceVersion.latestSupported())
    
  • init方法主要用来做一些准备工作。我们一般在这里初始化几个工具类。上述代码我们初始了与元素相关的工具类elementUtils、与日志相关的工具类messager、与文件相关的filer以及与类型相关工具类typeUtils。我们接下来会看到process主要就是通过这几个类来生成代码的。

  • process用来完成具体的程序写代码功能。在具体介绍process之前,请允许我先推荐一个库:javapoetjavapoet是由神奇的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,那么这段代码应该很容易能看懂。这里我简单解释一下。

  1. 选取注解了DebugLog的method和constructor作为pointcut。
  2. 在原方法执行前织入开始时间,在原方法执行后织入结束时间,并计算出运行时间。通过Log.v打印出来。

到此,这个简化版的Hugo基本就介绍完了。你可以做个Demo试一下了。

  1. 笔者DebugLog的包名是com.hugo.example.lib。因此在method()和constractor()中的注解内容是com.hugo.example.lib.DebugLog。
  2. 其实,真正的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。相较于ASMJavassist效率更低。但是,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的要求并不高,掌握了上面两个例子就可以实现我们大部分需求了。如果你想了解更高级的用法,请移步这里。接下来,我只介绍两个类:CtClassClassPool

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定义的一系列TransformJacocoProguardInstantRunMuti-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提供了两种类型的输入方式:

    1. CLASSES: java编译后的字节码,可能是jar包也可能是目录。
    2. 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中定义了以下几种范围:

    1. PROJECT: 只处理当前项目。
    2. SUB_PROJECTS: 只处理子项目。
    3. PROJECT_LOCAL_DEPS: 只处理项目本地依赖库(本地jars、aar)。
    4. PROVIDED_ONLY: 只处理以provided方式提供的依赖库。
    5. EXTERNAL_LIBRARIES: 只处理所有外部依赖库。
    6. SUB_PROJECTS_LOCAL_DEPS: 只处理子项目的本地依赖库(本地jars、aar)
    7. 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中。编译时,pluginRouterinit()中注入了如下代码:

private void init() { 
		ARouterImpl var1 = new ARouterImpl();
  	var.register(mRouterMap);
}

plugin中的代码有点多,我就不贴出来了。这一节的代码都在这里

这个Demo非常简单,但是它对于理解ARouter、WMRouter等路由框架的原理十分有用。它们在处理路由表的注册时,都是采用编译期字节码注入的方式,只不过它们没有使用javassit,而是使用了效率更高的ASM。它们用起来更方便是因为,它们利用APT技术把路径和Activity之间的映射变透明了。即:类似于Demo中的ARouterImpl这种代码,都是通过APT生成的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值