转载

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

Android编译期插桩,让程序自己写代码(一) 中我介绍了 APT 技术。

Android编译期插桩,让程序自己写代码(二) 中我介绍了 AspectJ 技术。

本文是这一系列的最后一篇,介绍如何使用 Javassist 在编译期生成字节码。老规矩,直接上图。

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

一、Javassist

Javassist 是一个可以方便操作Java字节码的库,它使Java程序能够在运行时新增或修改Class。 Javassist 直接生成二进制class文件。操作字节码, 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"));
    }
}
复制代码

运行程序,当前项目下生成了以下内容:

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

反编译 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编译期插桩,让程序自己写代码(三)

上图是Android Gradle定义的一系列 TransformJacocoProguardInstantRunMuti-Dex 等功能都是通过继承Transform实现的。当前,我们也可以自定义 Transform

2.1 Transform的工作原理

我们先来了解多个 Transform 是如何配合工作的。直接上图。

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

Transform 之间采用流式处理方式。每个 Transform 需要一个输入,处理完成后产生一个输出,而这个输出又会作为下一个 Transform 的输入。就这样,所有的 Transform 依次完成自己的使命。

Transform 的输入和输出都是一个个的class/jar文件。

2.1.1 输入(Input)

Transform 接收输入时,会把接收的内容封装到一个TransformInput集合中。 TransformInput 由一个JarInput集合和一个DirectoryInput集合组成。 JarInput 代表Jar文件, DirectoryInput 代表目录。

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

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项目拆分成多个组件,而各个组件间互不依赖。

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

既然组件间是互不依赖的,那么它们就不能像普通项目那样进行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 中。编译时, pluginRouterinit() 中注入了如下代码:

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

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

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

原文  https://juejin.im/post/5cef3f676fb9a07ee1690c94
正文到此结束
Loading...