Android Gradle 中的Transform

Posted 好人静

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 中的Transform相关的知识,希望对你有一定的参考价值。

前言

         逐步整理的一系列的总结:

        Android Gradle插件开发初次交手(一)

        Android Gradle的基本概念梳理(二)

       Android 自定义Gradle插件的完整流程(三) 

       Android 自定义Task添加到任务队列(四)

       Android 自定义Gradle插件的Extension类(五)

        android Gradle 中的Transform(六)

       Android Gradle之Java字节码(七)

       Android Gradle 中的字节码插桩之ASM(八)

      Android Gradle 中的使用ASMified插件生成.class的技巧(九)

      Android Gradle 中的实例之动态修改AndroidManifest文件(十)


        一个APP从代码到最后生成一个可用的apk程序包,其实需要经历编译、打包、apk生成内容以及最后签名的整个过程。前面不管是在 Android 自定义Gradle插件的完整流程(三)中的二 Task,还是在 Android 自定义Task添加到任务队列(四)中定义的二 在自定义插件中添加Task都是添加在APP的编译过程通过添加自定义Task来增加一些任务功能。

        那如果需要在class文件转换成dex之前,对class文件进行处理,例如:

  • (1)对全局的class进行字节码插桩:例如UI、内存、网络等的性能监控;埋点功能(如用户页面的访问、点击事件、每个方法的耗时统计)
  • (3)全局log日志:自动生成TAG,例如默认为当前类的名称;自动添加当前代码的行数

        那能不能有方法来解决这个问题呢?这次主要就是探索和了解下这个过程。像web开发中的ORM框架(如MyBatis)都是对.class文件的字节码进行修改。

        遗留问题:结合前期看的MyBatis的相关内容,在去详细了解下。

一 APP打包流程

1.概要

        一个Android项目从代码到安装到手机上,大致经历编译、打包、apk生成以及签名的过程。通过Android Studio的Build的Analyze APK来查看一个APK的内容如下:

        

  • (1)一个或多个dex文件
  • (2)resources.arsc
  • (3)未编译的资源文件
  • (4)AndroidManifest.xml文件 

        生成APK文件之后,再通过ADB将签名之后的APK安装到手机上。这里又让自己明白了平时在build.gradle中的signingConfigs进行配置,那么通过Android Studio进行运行之后的APK就会带着签名信息。

    signingConfigs 
        debug 
            storeFile file("debug.keystore")
            storePassword "xxxx"
            keyAlias "xxxx"
            keyPassword "xxxx"
      
        release 
            storeFile file("release.keystore")
            storePassword "xxxx"
            keyAlias "xxxx"
            keyPassword "xxxx"
        
    

         遗留问题:再详细的分析下(2)和(3)

2.详细的打包流程

        根据官网提供的打包流程图来具体分析一下前面的过程。

        

  • (1) res目录下的可被编译的资源文件(如layout、values等)和AndroidManifest.xml经过aapt工具生成对应的R.java文件和resouce.arsc文件。
    • R.java文件是在应用层可以直接通过resource id进行访问;而resouce.arsc是apk在运行时,dalivk虚拟机用来识别的资源表;
    • R.java文件只是resource id列表,而resource.arsc文件会将这些resource id进行组装,在apk运行的时候,根据设备的情况采用不同的资源;
    • 另一部分不可编译的需要直接通过文件名进行访问的资源文件(assets、res/raw)就会直接通过和.dex文件打包到apk中
  • (2)aidl文件通过aidl工具生成对应的java文件。对应的任务队列中的“Task :app:compileDebugAidl NO-SOURCE”;
  • (3)将R.java文件、aidl文件以及项目的源代码经过Java编译器编译成.class文件;
  • (4)通过dx工具(主要将Java字节码转换成Dalivk字节码、压缩常量池、消除冗余信息)生成运行apk的环境dalivk虚拟机可以执行的.dex文件,这里包括第三方的libraries以及.class文件。
  • (5)不可编译的资源文件、.dex文件通过apkbuilder工具直接打包到apk中
  • (6)通过jarsinger工具对apk进行签名,通常可根据debug还是release设置两种签名的keystore
  • (7)使用zipalign工具对apk中的所有资源文件对齐处理(遗留问题:这个需要了解下

        上面这些过程都是在Android Studio的build窗口中通过task任务来完成对应的工作。而这次探索的Transform就是在第(4)过程将.class文件转换成.dex文件的过程。

二 Android Transform的几个重要方法

        Android Gradle提供了Transform来对由.class文件转换成.dex文件进行字节码查找、代码注入等操作。每个Transform都是一个task,TaskManager会将每个Transform在处理完之后会交给下一个Transform。先大体了解一点Transform。

        Transform是一个抽象类。所以要想自定义一个Transform,就要继承该类。

public abstract class Transform 

         主要了解几个重要方法的含义:

1.getName()

        返回的内容即为指定该Transform的名字

    @Override
    public String getName() 
        return "HotTransformTask";
    

        在任务队列输出的时候如下:

> Task :app:transformClassesWithHotTransformTaskForDebug

        而这个Task的名字组成是由“transform+ContentType+With+transform名字+TaskFor+buildType+productFlavor”组成的。 这个transform名字就是我们通过getName()设置的名字。ContentType这个就是下面要介绍的一个方法设置的内容。

2.getInputTypes()

        返回的内容即为指定该Transform要处理的数据类型,即该Transform的输入文件的类型

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() 
        return TransformManager.CONTENT_CLASS;
    

        在TransformManager中有下面几种类型:

    /**要处理的数据类型是java class文件,并且包含jar文件*/
    public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
    /**要处理的数据类型是jar文件*/   
     public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
    /**要处理的数据类型是java resource文件*/
    public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
     /**要处理的数据类型是dex文件*/
    public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
    /**要处理的数据类型是dex文件、java resource文件*/
    public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
            ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);

        一般getInputTypes()返回的是CONTENT_CLASS。 

3.getScopes()

         返回的内容即指定该Transform的修改input文件的作用域

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() 
        return TransformManager.SCOPE_FULL_PROJECT;
    

         在TransformManager中有下面几种类型:

    /**仅仅当前工程*/    
    public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
    /**整个项目工程+外部library库*/   
     public static final Set<ScopeType> SCOPE_FULL_PROJECT =
            ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
    /***/
    public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES =
            new ImmutableSet.Builder<ScopeType>()
                    .addAll(SCOPE_FULL_PROJECT)
                    .add(InternalScope.FEATURES)
                    .build();
    public static final Set<ScopeType> SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
    public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
            ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
    public static final Set<ScopeType> SCOPE_FULL_PROJECT_WITH_LOCAL_JARS =
            new ImmutableSet.Builder<ScopeType>()
                    .addAll(SCOPE_FULL_PROJECT)
                    .add(InternalScope.LOCAL_DEPS)
                    .build();

        一般使用的是 SCOPE_FULL_PROJECT。

        通过getInputTypes() 和getScopes()就设置好了需要处理的设置为输入的对应的class的字节码,当复写transform()的时候,如果不进行任何处理,那么将无法生成.dex文件,在最后打包之后的apk文件中无.dex文件,如图:

          

4.getReferencedScopes()

        返回的内容即指定该Transform的查看input文件的作用域

    @Override
    public Set<? super QualifiedContent.Scope> getReferencedScopes() 
        return TransformManager.SCOPE_FULL_PROJECT;
    
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() 
        return TransformManager.EMPTY_SCOPES;
    

        getReferencedScopes()区别于getScopes(),复写transform()并不会覆盖Android原来的.class文件转换成dex文件的过程。该方法主要用来该自定义的Transform并不想处理任何input文件的内容,仅仅只是想查看input文件的内容的作用域范围。 

        从源码注解中可以发现:

        

         所以要实现只查看input文件的内容,设置getReferencedScopes()的作用域范围,同时需要将getScopes()返回一个空集合,如上代码所示。这样在transform()可以查看该.class文件转换成dex文件的过程,不改变原来Android的打包apk的逻辑。

5.isIncremental()

        返回的内容即指定该Transform的是否进行增量编译

    @Override
    public boolean isIncremental() 
        return false;
    

        当开启增量编译之后,对应着input的四种状态(changed/added/removed/notchanged)要完成不同的操作。当然返回false,则进行全量编译,每次删除上一次编译的内容。

6.transform()

        完成字节码的修改、处理等操作。

        先了解下怎么查看input的相关内容。后续在详细研究下,下面只是在transform()中输出了input的内容。

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException 
       //如果不带super,就不会生成dex文件
        super.transform(transformInvocation);
        Collection<TransformInput> inputs = transformInvocation.getReferencedInputs();
        for (TransformInput input : inputs) 
            //返回的是ImmutableJarInput
            for (JarInput jar : input.getJarInputs()) 
                SystemOutPrint.println("jar file = " + jar.getFile());
            
            //返回的是ImmutableDirectoryInput
            for (DirectoryInput directory : input.getDirectoryInputs()) 
                SystemOutPrint.println("directory file = " + directory.getFile());
            
        
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        SystemOutPrint.println("output  = " + outputProvider);

    

              Android Gradle已经将input和output打包成一个TransformInvocation对象,其中可以通过下面的方法获取对应的内容:

public interface TransformInvocation 
    /**
     * 返回正在处理哪个Context,该Context包含项目名称、路径等信息
     */
    Context getContext();
    /**
     * 返回通过getScope()设置的所有的input
     */
    Collection<TransformInput> getInputs();
    /**
     * 返回通过getReferencedScopes()设置的referenced-only 的input
     */
    Collection<TransformInput> getReferencedInputs();
    /**
     * 返回secondaryInputs
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();
    /**
     * 可以设置output的相关内容
     */
    TransformOutputProvider getOutputProvider();
    /**
     * 返回是否是增量编译
     */
    boolean isIncremental();

  • (1)getInputs()和 getReferencedInputs()就是返回的是input的相关内容,被封装为TransformInput,里面包含jar相关的JarInput集合以及和其他文件相关的DirectoryInput集合;
  • (2)通过getOutputProvider()来操作output相关内容。output的内容不是任意指定,而是根据input的内容和设置的scopes等有TransformOutputProvider生成。

        这样就可以获取到需要处理的.class文件,然后对这些文件进行自定义处理。

 7.registerTransform()

        注册Transform。

        在前面的多次提到,当我们通过继承DefaultTask添加的一个task的时候,都需要通过依赖已有task的方式将task队列中,那么对于自定义Transform同样也是需要进行注册,Android Gradle提供了相应的API进行注册,注册之后自动会插在Transform队列的最前面。

class FirstPluginProject implements Plugin<Project> 

    @Override
    void apply(Project project) 
    ......
        project.extensions.findByType(BaseExtension.class).registerTransform(new HotTransform())
    

       这里findByType()既可以是BaseExtension.class,也可以是AppExtension.class,这个AppExtension继承BaseExtension。

        在Android Studio中运行之后,就可以在Build的输出窗口中,输出相应的内容:

> Task :app:validateSigningDebug
> Task :app:mergeDebugResources
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:processDebugManifestForPackage
> Task :app:mergeDebugNativeLibs
> Task :app:mergeDebugJavaResource
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:processDebugResources
> Task :app:compileDebugJavaWithJavac
> Task :app:compileDebugSources
> Task :app:dexBuilderDebug
> Task :app:mergeLibDexDebug

> Task :app:transformClassesWithHotTransformTaskForDebug
%%%%%%%%% FirstPluginProject %%%%%%%%% context  project name = appcontext  project name = :app:transformClassesWithHotTransformTaskForDebug , isIncremental = false
%%%%%%%%% FirstPluginProject %%%%%%%%% jar file = /Users/j1/.gradle/caches/transforms-2/files-2.1/acfe271f2f2894a7773f8e05b54882dc/material-1.2.1-runtime.jar
//.......输出内容比较多,省略
%%%%%%%%% FirstPluginProject %%%%%%%%% directory file = /Users/j1/Documents/android/code/AndroidPlugin/app/build/intermediates/javac/debug/classes
> Task :app:mergeProjectDexDebug
> Task :app:mergeExtDexDebug

         具体的代码已经上传到至https://github.com/wenjing-bonnie/AndroidPlugin.git的firstplugin目录的相关内容。因为逐渐在这基础上进行迭代,可回退到Gradle_6.0.1该tag下可以查看相关内容。

三 Android Transform高级应用

        前面也提到了每个Transform都是一个gradle中task,而Android Gradle中的TaskMananger将每个Transform串联到一起。有两种方式的Transform:

  • (1)消费型:当前Transform需要将消费型输出给下一个Transform,每个Transform节点都可以对class进行处理之后在传递给下一个Transform。通过getScopes()设置的就是消费型输入,需要将输出给下一个任务,此时获取的outputProvider不为null;
  • (2)引用型:当前Transform可以读取这些输入,而不需要输出给下一个Transform。 通过getReferencedScopes()设置的为引用型输入,此时获取的outputProvider不为null。

        前面在二 Android Transform的几个重要方法中提到的例子,仅仅是一个引用型的Transform,只是简单的利用Transform输出了input的内容,那么消费型的Transform才用来进行对.class文件进行修改,那修改之后怎么生成对应的.dex文件,然后打包到apk包里面呢?

        对于消费型的Transform,最重要的就是需要将input内容通过FileUtils.copyFile()或FileUtils.copyDirectory()拷贝到output目录中,否则生成的apk会无法安装到手机。

        相比较于二 Android Transform的几个重要方法中提到的例子,修改内容如下:

  • (1)设置消费型input的作用域
 /**
     * 需要操作的内容范围
     *
     * @return
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() 
        //仅仅用来查看input文件
        //return TransformManager.EMPTY_SCOPES;
        return TransformManager.SCOPE_FULL_PROJECT;
    

    /**
     * 仅仅用来设置查看input文件的作用域
     */
//    @Override
//    public Set<? super QualifiedContent.Scope> getReferencedScopes() 
//        return TransformManager.SCOPE_FULL_PROJECT;
//    

        若不复写 getReferencedScopes(),在父类中默认的已经返回TransformManager.EMPTY_SCOPES。

  • (2)重写transform()
  @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException 
        //如果不带super,就不会生成dex文件
        super.transform(transformInvocation);

        SystemOutPrint.println("context  project name = " + transformInvocation.getContext().getProjectName()
                + "context  project name = " + transformInvocation.getContext().getPath()
                + " , isIncremental = " + transformInvocation.isIncremental());
        //现在进行处理.class文件:消费型输入,需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //仅仅用来查看input文件:引用型输入,无需输出,此时outputProvider为null
        //Collection<TransformInput> inputs = transformInvocation.getReferencedInputs();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        for (TransformInput input : inputs) 
            //返回的是ImmutableJarInput。
            for (JarInput jar : input.getJarInputs()) 
                SystemOutPrint.println("jar file = " + jar.getFile());
                //TODO 在这里增加处理.jar文件的代码

                //获取Transforms的输出目录
                File dest = outputProvider.getContentLocation(jar.getFile().getAbsolutePath(), jar.getContentTypes(), jar.getScopes(), Format.JAR);
                //将修改之后的文件拷贝到对应outputProvider的目录中
                FileUtils.copyFile(jar.getFile(), dest);
            
            //返回的是ImmutableDirectoryInput
            for (DirectoryInput directory : input.getDirectoryInputs()) 
                SystemOutPrint.println("directory file = " + directory.getFile());
                //TODO 在这里增加处理.class文件的代码

                //获取Transforms的输出目录
                File dest = outputProvider.getContentLocation(directory.getName(), directory.getContentTypes(), directory.getScopes(), Format.DIRECTORY);
                //将修改之后的文件拷贝到对应outputProvider的目录中
                FileUtils.copyDirectory(directory.getFile(), dest);
            
        
    

       这样就剩下只需要在TODO备注的地方增加对.jar文件或.class文件进行修改 的代码,最后将修改之后的文件拷贝到对应的output的目录中即可。

        这里要注意拷贝directory一定要用FileUtils.copyDirectory(),拷贝File一定要用FileUtils.copyFile(),一开始由于将FileUtils.copyDirectory()误用成了FileUtils.copyFile()导致抛出下面异常:

        

         将插件进行打包发布,通过Android Studio 运行项目,发现该apk已经成功运行在手机上。具体的代码已经上传到至https://github.com/wenjing-bonnie/AndroidPlugin.git的firstplugin目录的相关内容。因为逐渐在这基础上进行迭代,可回退到Gradle_6.0.2该tag下可以查看相关内容。

四 总结

        这两天终于解决了一开始自己在复写transform(),最后生成的apk无法运行在手机上面的问题了,太开心了。总结下

  • 1.一个apk从源码工程到安装到手机,大体会经过编译、打包、apk生成以及签名、adb安装过程;
  • 2.一个apk的详细打包流程简单总结:
    • (1)资源文件(除去assets以及res/raw)经过aapt工具被成R.java文件,放到resources.arsc文件中,供Dalivk虚拟机识别;
    • (2)剩余的资源文件(即assets以及res/raw)会直接最后和.dex文件以及其他java resouce文件被打包到apk中;
    • (3).aidl文件会经过aidl工具生成对应的java文件
    • (4)R.java文件、源代码以及aidl生成的java文件经过java编译器生成.class文件;
    • (5).class文件和第三方的library库以及.class文件经过dex工具被打包成.dex文件,而这个Android Transform的过程就发生在这里;
    • (6).dex文件、不可编译的资源文件以及其他资源文件经过apkBuilder被打包到.apk文件中,供Dalivk虚拟机识别运行;
    • (7).apk文件经过jarsinger签名、以及zipalign对齐最后生成一个可以运行在手机的apk
  • 3.Android Transform其实就是一个task,TransformManger会把所有的Transform串成一个链,一般自定义的Transform会自动添加到Transform链的最前面;
  • 4.Transform分为消费型的Transform和引用型的Transform;
  • 5.消费型的Transform必须通过getScopes()设置input的作用域以及将对应的input拷贝到output目录中;
  • 6.引用型的Transform通过getReferencedScopes()设置的引用input的作用域,并且还得设置getScopes()的返回集合为空,此时output为空,无需执行拷贝操作。

         后面尝试下对input文件进行修改,然后给每个页面增加打点事件,加油!!!越来越有意思了

以上是关于Android Gradle 中的Transform的主要内容,如果未能解决你的问题,请参考以下文章

gradle build 未检测到 build.gradle 中的 android ndkVersion

Android Gradle 插件Gradle 依赖管理 ② ( build.gradle 中的 dependencies 依赖配置 | DependencyHandler#add 方法介绍 )

Gradle,Gradle 包装器在 Android Studio 中的不同实例 by ionic cordova

Android Gradle 插件Gradle 构建机制 ① ( 空白工程 Gradle 构建文件 | IntelliJ IDEA 工程构建文件 | Android Studio 工程构建文件 )

android/flutter 中的 settings_aar.gradle 有啥用?

Android Gradle 插件组件化中的 Gradle 构建脚本实现 ⑤ ( 优化 Gradle 构建脚本 | 构建脚本结构 | 闭包定义及用法 | 依赖配置 | android 块配置 )