美文网首页组件化插件化
WMRouter源码解析之Transform

WMRouter源码解析之Transform

作者: juexingzhe | 来源:发表于2020-06-02 17:27 被阅读0次

    现在有很多的框架用到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<Project> {
    
        @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.
         *
         * <p>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.
         *
         * <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
         */
        @NonNull
        public abstract Set<ContentType> getInputTypes();
    
    
        /**
         * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
         */
        @NonNull
        public abstract Set<? super Scope> getScopes();
    
    
        /**
         * Returns whether the Transform can perform incremental work.
         *
         * <p>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<TransformInput> inputs,
                @NonNull Collection<TransformInput> 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<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);
    public static final Set<ContentType> CONTENT_NATIVE_LIBS =
    ImmutableSet.of(NATIVE_LIBS);
    public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
    public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
    ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
    public static final Set<ContentType> 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> 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<String> 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<String> 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

        本文链接:https://www.haomeiwen.com/subject/wifpzhtx.html