安卓开发多module打包aar

闲话专栏 专栏收录该内容
13 篇文章 0 订阅

需求背景

在SDK开发过程中,我们可能遇到过需要将多个Module的代码打包成一个jar或者aar的情况,由于安卓打包的时候不会将module依赖的其他module打包进aar中,所以通过AS打包出来的aar是不完整的。但是在组件化开发盛行的当下,这种诉求却越发明显。

解决方案

当然,针对上述问题,也有很多解决方案,比如利用Python脚本来对代码进行重组、利用Gradle来进行打包流程干预,还有直接使用单模块开发的方式。首先,使用单模块开发的方式是不会面临上述问题的,也就不存在解决,但是它会抛弃掉组件化开发或者模块化开发的巨大优势;其次,使用Python对代码进行重组,这种方式的成本比较低,利用Python脚本语言的拷贝等语法对多个模块的代码进行组合,然后对组合后的代码进行打包操作,可以实现多个模块打包成一个目标文件的目的;当然,我们还可以利用AS原生嵌入的Gradle来完成这项工作,通过在打包流程中添加自定义的操作来完成多模块打包成一个目标文件。

下面对比下这几种方式的优缺点:

方式优点缺点
单模块兼容性高,没有额外的流程不支持组件化开发,在项目较复杂时,开发维护成本偏高
Python处理思路简单,实现容易需要额外的脚本语言支持,需要操作文件,通用性较弱
Gradle没有额外流程,扩展性强,接近原生速度需要对原始流程进行干预,操作部分文件

事实上,在笔者的项目中,最开始是用的Python脚本的方式,打包时,调用Python命令来完成。在此过程中,通过执行Python脚本命令来完成打包的方式在每次版本迭代时,可能都需要修改打包脚本,在我们的jenkins服务器上也需要安装Python环境,因此,我们修改了打包方式,通过原生的Gradle来实现,这种方式让我们的项目的打包更简洁,直接通过AS的任务来实现,而且具备很强的通用性。

方案分析

针对Gradle这种方式,我们主要是利用Gradle的一些特性和Android标准打包流程来实现的。

Gradle生命周期

涉及到到gradle的构建干预,我们必须了解gradle的一些基本规则。gradle的构建流程有三个步骤(或者说它的生命周期包含三个主要节点),分别是:初始化、配置、执行。

官方给出的定义是:

A Gradle build has three distinct phases.

Initialization
Gradle supports single and multi-project builds. During the initialization phase, Gradle determines which projects are going to take part in the build, and creates a Project instance for each of these projects.

Configuration
During this phase the project objects are configured. The build scripts of all projects which are part of the build are executed.

Execution
Gradle determines the subset of the tasks, created and configured during the configuration phase, to be executed. The subset is determined by the task name arguments passed to the gradle command and the current directory. Gradle then executes each of the selected tasks.

事实上,每个build.gradle文件都会对应一个Project对象,在初始化完成之后,我们就可以获取到这个对象并进行一些操作。而配置阶段则是针对我们的build.gradle文件来进行一些配置操作,比如构建任务树等。在执行阶段,就会按照配置阶段确定的任务树来依次执行每个任务(不开启并发执行的情况下)。

而我们的切入点就是在执行接阶段来对构建流程进行干预。事实上,我们既可以通过项目内的gradle脚本来实现,也可以通过gradle的插件plugin来实现我们的功能。相对脚本,插件具备更加强的通用性和简洁性。但是这里,我们仅仅针对脚本来阐述下我们的实现思路。

生命周期监听器

在我们的实现中,我们没有采用自定义任务以及任务依赖的方式,因为这样我们需要添加额外的任务,并且需要通过额外的任务来完成我们的构建,这是我们不想看到的,也是我们抛弃掉Python构建方式的原因之一。我们采用的是通过对构建流程中项目以及各个任务的执行状态和流程来进行切入的。这里,我们可以使用gradle api 提供的各种监听器来实现。Gradle本身提供了很多监听器,使之具备极强的扩展性。具体包含:

  /**
     * <li>{@link org.gradle.BuildListener}
     * <li>{@link org.gradle.api.execution.TaskExecutionGraphListener}
     * <li>{@link org.gradle.api.ProjectEvaluationListener}
     * <li>{@link org.gradle.api.execution.TaskExecutionListener}
     * <li>{@link org.gradle.api.execution.TaskActionListener}
     * <li>{@link org.gradle.api.logging.StandardOutputListener}
     * <li>{@link org.gradle.api.tasks.testing.TestListener}
     * <li>{@link org.gradle.api.tasks.testing.TestOutputListener}
     * <li>{@link org.gradle.api.artifacts.DependencyResolutionListener}
     */
     

其中有比较重要的几个监听器接口是我们在实现多模块打包中可以使用的。

全局构建监听器:可以获取项目构建的一些关键性节点步骤,从而可以初始化一些全局变量。

public interface BuildListener {
    /**
     * <p>Called when the build is started.</p>
     *
     * @param gradle The build which is being started. Never null.
     */
    void buildStarted(Gradle gradle);

    /**
     * <p>Called when the build settings have been loaded and evaluated. The settings object is fully configured and is
     * ready to use to load the build projects.</p>
     *
     * @param settings The settings. Never null.
     */
    void settingsEvaluated(Settings settings);

    /**
     * <p>Called when the projects for the build have been created from the settings. None of the projects have been
     * evaluated.</p>
     *
     * @param gradle The build which has been loaded. Never null.
     */
    void projectsLoaded(Gradle gradle);

    /**
     * <p>Called when all projects for the build have been evaluated. The project objects are fully configured and are
     * ready to use to populate the task graph.</p>
     *
     * @param gradle The build which has been evaluated. Never null.
     */
    void projectsEvaluated(Gradle gradle);

    /**
     * <p>Called when the build is completed. All selected tasks have been executed.</p>
     *
     * @param result The result of the build. Never null.
     */
    void buildFinished(BuildResult result);
}

任务状态监听器:可以获取每个任务的执行入口和出口,实现对指定任务的输入输出进行干预

public interface TaskExecutionListener {
    /**
     * This method is called immediately before a task is executed.
     *
     * @param task The task about to be executed. Never null.
     */
    void beforeExecute(Task task);

    /**
     * This method is call immediately after a task has been executed. It is always called, regardless of whether the
     * task completed successfully, or failed with an exception.
     *
     * @param task The task which was executed. Never null.
     * @param state The task state. If the task failed with an exception, the exception is available in this
     * state. Never null.
     */
    void afterExecute(Task task, TaskState state);
}

任务树结果监听器:可以获知整个构建流程中的所有任务,实现对某些任务的移除,缩减构建时间等优化措施

public interface TaskExecutionGraphListener {
    /**
     * <p>This method is called when the {@link TaskExecutionGraph} has been populated, and before any tasks are
     * executed.
     *
     * @param graph The graph. Never null.
     */
    void graphPopulated(TaskExecutionGraph graph);
}

Project对象

上文提到过,每个build.gradle都对应一个Project对象,而且,需要指出的是,apply语法会将当前build.gradle的对象传递给引入的插件或者gradle对象。因此,我们可以通过需要构建的build.gradle对象,获取一些重要的信息。比如,配置列表、模块名称,模块属性等。同时还可以给当前的Project对象添加各种监听器。完成这些操作,我们只需要两行代码即可。。

引入构建的gradle(插件)

apply from: '../shell.gradle'

获取Project对象

/**以下语法是在shell.gradle文件中任意位置可用**/
project.gradle

其中project的gradle成员是非常核心的一个对象,它记录了构建流程的很多内容,并且通过它可以添加任务、行为(Action)、配置信息、监听器等。比如,我们添加监听器的方式为:

project.gradle.addListener(new TaskListener())

通过对这些基础信息的获取以及监听器的添加,我们就可以实现对构建流程的干预了。

流程干预

对构建流程的干预,我们主要是通过对构建任务的入口和出口进行干预的。我们通过
beforeExecute来修改任务的输入(Task.inputs),通过afterExecute来管理任务的输出(Task.outputs),从而实现对参与打包的文件的管理和打包结果的控制。当然,我们可以通过一些简单的配置来确定我们需要处理的模块。类似:

   /**
   * 是否merge内容:BuildConfig等
   */
  private boolean merge;

   /**
   * 主模块名
   */
  private String host = "mainModule";

   /**
   * 参与构建的子模块名集合
   */
  private Set<String> subs = new HashSet<>();

  public TaskListener() {
    subs.add("module1")
    subs.add("module2")
    subs.add("module3")
    subs.add("module4")
  }


其目的是确定参与构建的子模块、主模块以及某些任务的区分。当然,如果是插件的话,配置则更灵活。在这里,我们主要关心这几个任务:

  private final static String JAVA_WITH_JAVAC = "JavaWithJavac";
  private final static String BUILD_CONFIG = "BuildConfig";
  private final static String TRANSFORM = "transformClasses";
  private final static String AAR = "Aar";
  

Javac:将java文件编译成字节码文件

generateBuildConfig:确定是否抛弃,或者合并各个模块的构建配置类

transformClasses:正常情况下,字节码文件处理的最后步骤

bundleAar:将所有的构建产物压缩成aar的步骤,也是生成aar的最后步骤。

所以我们只需要通过任务名称来对任务输入和输出进行控制,就可以达到我们的目的。简单的逻辑是:

  1. 记录各个子模块的javac输出
  2. 确定子模块的BuildConfig的处理规则:禁用或者删除
  3. 收集各个模块的资源文件
  4. 合并各个模块的R文件
  5. 将收集到的数据作为输入,传递给主模块的transform或者bundle任务

举个例子:

记录所有的字节码文件是通过监听javac任务的输出来实现的

      // 收集子模块字节码文件
      if (task.name.contains(JAVA_WITH_JAVAC)) {
        Iterator<File> it = task.outputs.getFiles().iterator()
        while (it.hasNext()) {
          toMoveFilePaths.addAll(listF(it.next()))
        }
      }

确定子模块的BuildConfig处理规则是通过监听generateBuildConfig来实现的

        // 阻止子模块生成BuildConfig.java
        if (task.name.contains(BUILD_CONFIG)) {
          task.enabled(false)
        }
      }

后续任务分别是通过generateResourcesgenerateRFilebundleAar来实现的。在打包aar之前,我们先合并一些文件,比如字节码文件的合并,将子模块的字节码文件拷贝到主模块的字节码文件中间目录中:

        // move lib classes to main lib
        if (task.name.contains(TRANSFORM) && !isTransformed) {
          String[] path = task.inputs.files.head().absolutePath.split("classes/")
          String libDir = path[0] + "classes/"
          for (int i = 0; i < toMoveFilePaths.size(); i++) {
            File file = toMoveFilePaths.get(i)
            String[] classPaths = file.absolutePath.split("classes/")
            String classPath = classPaths[classPaths.length - 1]
            File dest = new File(libDir, classPath);
            CopyFile(file, dest)
          }
          isTransformed = true;
        }
        

这样,我们基本就完成了所有文件的处理,只需要bundleAar自身来合并就可以。

思路扩展

上述方案是通过自定义gradle监听来实现的,事实上利用Gradle自定义插件可以达到同样的效果,并且适用性和扩展性更强,参数的处理更灵活。以上方案主要是提供一种多模块打包AAR的解决思路,通过这种构建流程的监听和干预,我们可以在原生的构建体检下完成一些非常奇妙的操作。

  • 0
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值