WMRouter源码解析之Transform

现在有很多的框架用到APT的技术,可以很好的解耦,实现编译期生成文件或者修改class文件,实现插桩的功能,Android提供了Transform的接口,在编译期间可以拿到所有编译后的class文件和jar包(包括aar包)。 美团开源的WMRouter中就用到这项技术,在编译期找到所有的ServiceInit_XXX.class文件(包括本地主工程、子工程和依赖的aar/jar包),结合ASM生成ServiceLoaderInit.class文件,在框架运行时进行异步加载所有的Service实现。 今天重点在分析里面的Transform技术,ServiceInit_XXX.class等文件什么功能不会做介绍,这个以后的系列文章会进行阐述。

1. Transform插件开发流程

Transform其实也是可以理解成一个gradle task,是Google写的可以在这个task里面拿到安装包的所有class文件。 为了编译期间能执行该task,需要编写一个gradle plugin(这方面可以参考我之前的文章 一步步自定义Gradle插件),并且在我们的app工程的build.gradle中apply这个插件,看下实现步骤和代码。

第一步,先建一个Java Library工程,最后整体结构如下


WMRouterTransformPlugin.png

其中META-INF中的文件名是插件的名称,在app build.gradle中需要apply:

apply plugin: 'WMRouter'

WMRouter {
    enableDebug = true // 调试开关
    enableLog = true
}

看下WMRouter.properties文件的内容:

implementation-class=com.sankuai.waimai.router.plugin.WMRouterPlugin

上面就是插件的入口WMRouterPlugin,将本文的主角WMRouterTransform注册到project中,这样编译时才会被执行。

public class WMRouterPlugin implements Plugin {

    @Override
    public void apply(Project project) {
        WMRouterExtension extension = project.getExtensions()
                .create(Const.NAME, WMRouterExtension.class);

        WMRouterLogger.info("register transform");
        project.getExtensions().findByType(BaseExtension.class)
                .registerTransform(new WMRouterTransform());

        project.afterEvaluate(p -> WMRouterLogger.setConfig(extension));
    }
}

2. Transform源码

先看下其父类Transform的源码,需要实现下面四个抽象方法:

public abstract class Transform {

    /**
     * Returns the unique name of the transform.
     *
     * 

This is associated with the type of work that the transform does. It does not have to be * unique per variant. */ @NonNull public abstract String getName(); /** * Returns the type(s) of data that is consumed by the Transform. This may be more than * one type. * * This must be of type {@link QualifiedContent.DefaultContentType} */ @NonNull public abstract Set getInputTypes(); /** * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes. */ @NonNull public abstract Set getScopes(); /** * Returns whether the Transform can perform incremental work. * *

If it does, then the TransformInput may contain a list of changed/removed/added files, unless * something else triggers a non incremental run. */ public abstract boolean isIncremental(); /** * @deprecated replaced by {@link #transform(TransformInvocation)}. */ @Deprecated @SuppressWarnings("UnusedParameters") public void transform( @NonNull Context context, @NonNull Collection inputs, @NonNull Collection referencedInputs, @Nullable TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { } }

解释如下:

  1. getName就是这个task的名字,在编译时可以看到,比如打debug包是有下面的task::demoapp:transformClassesWithWMRouterForDebug
  2. getInputTypes,声明我们感兴趣的文件类型,这里就是class,还可以有jar,dex, resource
public static final Set CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
public static final Set CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
public static final Set CONTENT_NATIVE_LIBS =
ImmutableSet.of(NATIVE_LIBS);
public static final Set CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
public static final Set CONTENT_DEX_WITH_RESOURCES =
ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
public static final Set DATA_BINDING_BASE_CLASS_LOG_ARTIFACT =
ImmutableSet.of(ExtendedContentType.DATA_BINDING_BASE_CLASS_LOG);
  1. getScopes, 声明该task作用的范围,一般常用的是这些,我们这里声明作用范围包括主工程,子工程和外部依赖包
enum Scope implements ScopeType {
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
...
}

public static final Set SCOPE_FULL_PROJECT =
      Sets.immutableEnumSet(
              Scope.PROJECT,
              Scope.SUB_PROJECTS,
              Scope.EXTERNAL_LIBRARIES);
  1. isIncremental,是否支持增量编译,这里一般返回false
  2. 主要工作在最后一个方法transformz中,通过入参TransformInvocation可以拿到工程文件。下面就具体看下WMRouterTransform的该方法实现。

3. WMRouterTransform

目的是扫描com.sankuai.waimai.router.generated.service目录下的class文件,这些class文件也是通过编译时生成,这个本文先不介绍。

扫描到的文件名称保存到initClasses容器下。

invocation有两种输入类型,一种是依赖包jarInput(包括aar),一种是目录directoryInput

// WMRouterTransform.java
@Override
public void transform(TransformInvocation invocation) {
        WMRouterLogger.info(TRANSFORM + "start...");
        long ms = System.currentTimeMillis();

        Set initClasses = Collections.newSetFromMap(new ConcurrentHashMap<>());

        for (TransformInput input : invocation.getInputs()) {
            input.getJarInputs().parallelStream().forEach(jarInput -> {
                File src = jarInput.getFile();
                File dst = invocation.getOutputProvider().getContentLocation(
                        jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
                        Format.JAR);
                try {
                    scanJarFile(src, initClasses);
                    FileUtils.copyFile(src, dst);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            input.getDirectoryInputs().parallelStream().forEach(directoryInput -> {
                File src = directoryInput.getFile();
                File dst = invocation.getOutputProvider().getContentLocation(
                        directoryInput.getName(), directoryInput.getContentTypes(),
                        directoryInput.getScopes(), Format.DIRECTORY);
                try {
                    scanDir(src, initClasses);
                    FileUtils.copyDirectory(src, dst);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        File dest = invocation.getOutputProvider().getContentLocation(
                "WMRouter", TransformManager.CONTENT_CLASS,
                ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY);
        generateServiceInitClass(dest.getAbsolutePath(), initClasses);

        WMRouterLogger.info(TRANSFORM + "cost %s ms", System.currentTimeMillis() - ms);
    }

打个断点看了下jarInput打出来的组成,该jarInput是依赖的子工程(scopes:SUB_PROJECT) demolib2(name=:demolib2),里面的文件类型是CLASSES

WMRouterTransform1.png

而编译后的文件包在/demolib2/build/intermediates/intermediate-jars/debug/classes.jar
看下目录截图

jarinput directory.png

这里面就能拿到子工程demolib2中的指定文件,再接着源码往下看.

src就是上面编译后的classes.jar,而每次transform修改后的文件要复制到指定输出目录,否则下一个transform或者task就拿不到文件。

而这里dst就是输出目录,在上面的jarinput中可以看到dst=''WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/17.jar''

// WMRouterTransform.java
input.getJarInputs().parallelStream().forEach(jarInput -> {
    File src = jarInput.getFile();
    File dst = invocation.getOutputProvider().getContentLocation(
            jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
            Format.JAR);
    try {
        scanJarFile(src, initClasses);
        FileUtils.copyFile(src, dst);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

再看下scanJarFile(src, initClasses):

在子工程demolib2的目标目录com.sankuai.waimai.router.generated.service目录下有两个ServiceInit_xxx文件,把这两个文件添加到initClasses保存。

WMRouterTransform0.png

而对于工程里的另外一个子工程demolib1也是同样的逻辑,输出目录在

dst=''WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/18.jar''

WMRouterTransform2.png

再来看下主工程的目录文件扫描过程,结合图片会比较好理解,是directoryInput打出来的组成,该directoryInput是主工程工程(scopes:PROJECT),里面的文件类型是CLASSES

input.getDirectoryInputs().parallelStream().forEach(directoryInput -> {
    File src = directoryInput.getFile();
    File dst = invocation.getOutputProvider().getContentLocation(
            directoryInput.getName(), directoryInput.getContentTypes(),
            directoryInput.getScopes(), Format.DIRECTORY);
    try {
        scanDir(src, initClasses);
        FileUtils.copyDirectory(src, dst);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});
WMRouterTransform3.png

源路径src=WMRouter/demoapp/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,输出路径和前面两个子工程在同一个目录下,包名不一样是22.jar

看下源路径的截图:

WMRouterTransformApp.png

再回到源码,循环扫描完成后就是需要生成目标文件了,生成的目标文件位置在dest="WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/23",文件名是ServiceLoaderInit.class

File dest = invocation.getOutputProvider().getContentLocation(
      "WMRouter", TransformManager.CONTENT_CLASS,
      ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY);
generateServiceInitClass(dest.getAbsolutePath(), initClasses);

WMRouterLogger.info(TRANSFORM + "cost %s ms", System.currentTimeMillis() - ms);
WMRouterTransform4.png

打开看一下历经千辛万苦生产的目标文件长啥样,就是主工程,子工程,依赖包下的所有复合指定包名和后缀的ServiceInit_xxx文件。

package com.sankuai.waimai.router.generated;

import xxx

public class ServiceLoaderInit {
    public static void init() {
        ServiceInit_aea7f96d0419b507d9b0ef471913b2f5.init();
        ServiceInit_f3649d9f5ff15a62b844e64ca8434259.init();
        ServiceInit_eb71854fbd69455ef4e0aa026c2e9881.init();
        ServiceInit_b57118238b4f9112ddd862e55789c834.init();
        ServiceInit_f1e07218f6691f962a9f674eb5b4b8bd.init();
        ServiceInit_4268a3e74040533ba48f2e1679155468.init();
        ServiceInit_e694d982fb5d7a3a8c6b7085829e74a6.init();
        ServiceInit_ee5f6404731417fe1433da40fd3c9708.init();
        ServiceInit_9482ef47a8cf887ff1dc4bf705d5fc0a.init();
        ServiceInit_36ed390bf4b81a8381d45028b37cc645.init();
    }
}

再看下生成文件的操作generateServiceInitClass(dest.getAbsolutePath(), initClasses);

4. ASM

先上源码,

首先构造一个ClassWriter,通过它后面写入到文件

再通过ClassVisitor来构造类ServiceLoaderInit,父类默认Object

通过MethodVisitor来构造init方法,方法体里面内容通过遍历classes调用visitMethodInsn生成

最后通过FileOutputStream写出到文件ServiceLoaderInit.class

private void generateServiceInitClass(String directory, Set classes) {

    if (classes.isEmpty()) {
        WMRouterLogger.info(GENERATE_INIT + "skipped, no service found");
        return;
    }

    try {
        WMRouterLogger.info(GENERATE_INIT + "start...");
        long ms = System.currentTimeMillis();

        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, writer) {
        };
        String className = Const.SERVICE_LOADER_INIT.replace('.', '/');
        cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);

        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
                Const.INIT_METHOD, "()V", null, null);

        mv.visitCode();

        for (String clazz : classes) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, clazz.replace('.', '/'),
                    "init",
                    "()V",
                    false);
        }
        mv.visitMaxs(0, 0);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitEnd();
        cv.visitEnd();

        File dest = new File(directory, className + SdkConstants.DOT_CLASS);
        dest.getParentFile().mkdirs();
        new FileOutputStream(dest).write(writer.toByteArray());

        WMRouterLogger.info(GENERATE_INIT + "cost %s ms", System.currentTimeMillis() - ms);

    } catch (IOException e) {
        WMRouterLogger.fatal(e);
    }
}

5.总结

在整个WMRouterTransform中都有通过日志来打印一些关键节点,看下整个编译过程的日志。

首先是在plugin入口调用的[WMRouter] register transform,

接下来会分别编译子工程router->demolib2->demokotlin->demolib1->demoapp 的compileDebugJavaWithJavac,这里会得到class文件

最后执行:demoapp:transformClassesWithWMRouterForDebug,分别找到10个ServiceInitClass文件,

生成ServiceLoaderInit.class文件耗时19ms,整个Transform过程耗时188ms

最后通过:demoapp:transformClassesWithDexBuilderForDebug打dex包

Executing tasks: [:demoapp:assembleDebug] in project 

[WMRouter] register transform
...
:router:compileDebugJavaWithJavac UP-TO-DATE
:router:processDebugJavaRes NO-SOURCE
:router:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug UP-TO-DATE
...
:demolib2:compileDebugJavaWithJavac
:demolib2:processDebugJavaRes NO-SOURCE
:demolib2:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug
:demokotlin:kaptGenerateStubsDebugKotlin
:demokotlin:kaptDebugKotlin
:demokotlin:compileDebugKotlin
:demokotlin:prepareLintJar UP-TO-DATE
:demokotlin:generateDebugSources UP-TO-DATE
:demokotlin:javaPreCompileDebug
:demokotlin:compileDebugJavaWithJavac
:demokotlin:processDebugJavaRes NO-SOURCE
:demokotlin:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug
:demolib1:generateDebugBuildConfig
...
:demolib1:compileDebugJavaWithJavac
:demolib1:processDebugJavaRes NO-SOURCE
:demolib1:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug UP-TO-DATE
:demoapp:javaPreCompileDebug UP-TO-DATE
...
:demoapp:compileDebugJavaWithJavac
...
:demoapp:transformClassesWithWMRouterForDebug
[WMRouter] Transform: start...
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_9482ef47a8cf887ff1dc4bf705d5fc0a
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_4268a3e74040533ba48f2e1679155468
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_ee5f6404731417fe1433da40fd3c9708
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_f1e07218f6691f962a9f674eb5b4b8bd
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_aea7f96d0419b507d9b0ef471913b2f5
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_f3649d9f5ff15a62b844e64ca8434259
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_e694d982fb5d7a3a8c6b7085829e74a6
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_eb71854fbd69455ef4e0aa026c2e9881
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_36ed390bf4b81a8381d45028b37cc645
[WMRouter]     find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_b57118238b4f9112ddd862e55789c834
[WMRouter] GenerateInit: start...
[WMRouter] GenerateInit: cost 19 ms
[WMRouter] Transform: cost 188 ms
:demoapp:transformClassesWithDexBuilderForDebug
...


BUILD SUCCESSFUL in 21s
107 actionable tasks: 40 executed, 67 up-to-date
w: Detected multiple Kotlin daemon sessions at build/kotlin/sessions

通过上面所有几个步骤就把WMRouter中的Transform技术给说清楚了,没错就是为了生成一个文件费这么大周章。但是也有好处,主app没有强依赖子工程或者依赖包的具体服务,这个技术也可以用于主app分发生命周期,只需要在Application的生命周期中调用生成文件的方法即可。另外其实通过传统的ServiceLoader也可以,但是有一个缺点就是需要运行时去IO读取文件再反射构造调用,而通过这种方式编译期生成文件就可以避免掉IO读取接口文件的步骤,性能是比较好的。

你可能感兴趣的:(WMRouter源码解析之Transform)