了解如何从源码层寻找消除 Android Build Gradle 过期 API 的方案

2,326 阅读6分钟

前言背景

前段时间把公司一个项目中用到的 Gradle 版本从 com.android.tools.build:gradle:3.2.1 升级到了 com.android.tools.build:gradle:3.5.2 版本,这个项目由于起步晚,所以对于 build.gradle 文件中的用法基本都符合新版本的要求,不过有两个 Warning 点:

  • The following project options are deprecated and have been removed: android.useDeprecatedNdk.
  • API 'variantOutput.getProcessManifest()' is obsolete and has been replaced with 'variantOutput.getProcessManifestProvider()'. It will be removed at the end of 2019.

这两个 Warning 点是怎么产生的呢,我们分别来看一下:

第一点是由于在 gradle.properties 中加入了 android.useDeprecatedNdk=true,这个主要是由于项目中用到的一个自研 SO 包用的是低版本的 ndk-bundle 构建出来的,由于历史久远,改动工作量大,所以这个短期只能先这样了;

第二点则是由于下面一段处理逻辑引发的,这段逻辑主要是用于动态替换清单文件中定义的一些第三方库用到的 meta-data 配置信息,原理挺简单的就是拿到清单文件路径,然后对相应的 KEY 进行文本内容的替换

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifest.doLast {

            def manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
            // 省略后面对清单文件的替换处理逻辑
            ...
            ...
        }
    }
}

比如这个是配置的 FB SDK 用到的应用 ID

// 比如这个
<!-- facebook sdk -->
<meta-data
        android:name="com.facebook.sdk.ApplicationId"
        android:value="FACEBOOK_APPID" />

如何消除第二个 Warning

以往升级 Gradle 版本的时候,遇到有 Warning,一般的消除流程都是直接先网络进行一番搜索,看是否有前人已经输出对应的消除策略,然后就会有一堆的解决方案出现在我们眼前,接着就会欣喜若狂地逐个去试,直接 Warning 消除为止,然后继续陷入搬砖 Coding 码需求。

然而这次无比尴尬,搜了一大圈,发现相关的内容少之又少,而且试了一圈之后还无法消除,场面一度尴尬,伤心之余,痛定思痛,决定还是从 Android 的 Gradle 插件源码入手寻找如何消除这个 Warning。

经过一番努力,逻辑调整如下,即可顺利消除这个 Warning:

// output.processManifest.doLast ==> output.processManifestProvider.get().doLast
// manifestOutputDirectory ==> {manifestOutputDirectory.get()}
android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifestProvider.get().doLast {

            def manifestPath = "${manifestOutputDirectory.get()}/AndroidManifest.xml"
        }
    }
}

OK,到这里 Warning 已经完美的消除了,如果你只是寻求一个消除方案,那后续的内容其实可以忽略,而如果你是想以后可以从容面对,这种由于 Gradle 版本升级带来的过期 API 调整方案,那么很高兴我们可以一起接着往下探索。

先了解如何开发一个简单的 Gradle 插件

要想知道为何这么调整可以消除 Warning,以及编译时提示这个 Warning 的根源在哪里,我们需要先缓一缓,买个关子,需要先了解一下:如果开发一个简单的 Gradle 插件。

开发 Gradle 前需要先确认机器是否已经安装好 Gradle 环境,本人环境情况如下

------------------------------------------------------------
Gradle 6.0
------------------------------------------------------------

Kotlin:       1.3.50
Groovy:       2.5.8
JVM:          1.8.0_45 (Oracle Corporation 25.45-b02)
OS:           Mac OS X 10.15.1 x86_64

用 Gradle 的 init 命令基本初始化一个自定义插件项目,

» gradle init

// 选择类型,4 即为插件
Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 4

// 选择开发插件用到的语言
Select implementation language:
  1: Groovy
  2: Java
  3: Kotlin
Enter selection (default: Java) [1..3] 2

// 选择 DSL 的语言,也就是 build.gradle 里面用到那些配置项
Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

// 插件代码包名路径
Project name (default: plugin): fireantPlugin
Source package (default: fireantPlugin): com.fireantzhang.plugin

> Task :init
Get more help with your project: https://guides.gradle.org?q=Plugin%20Development

BUILD SUCCESSFUL in 1m 34s
2 actionable tasks: 2 executed

生成初始项目目录结构如下:

.
├── build.gradle
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
├── local.properties
├── plugin.iml
├── settings.gradle
└── src
    ├── functionalTest
    ├── main
    └── test

插件代码类,增加了我们自己插件的逻辑代码之后,内容如下:

public class FireantPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Register a task,Gradle init 时自动生成的任务,可以自行删除
        project.getTasks().register("greeting", task -> {
            task.doLast(s -> System.out.println("Hello from plugin 'com.fireantzhang.plugin.greeting'"));
        });

        // 通过 project 的 extensions 创建自己开发插件的配置参数
        MyAndroidInfo myAndroidInfo = project.getExtensions().create("myAndroidInfo", MyAndroidInfo.class);

        // 接下来创建我们自己的任务,这个任务可以读取 build.gradle 中配置的自定义参数,如下:
        project.task("myAndroidTask", task -> {
            // 为了方便找到我们插件的任务,给添加分组
            task.setGroup("fireantzhang");

            task.doLast(action -> {
                System.out.println("自定义插件中执行任务:myAndroidTask,获取到的参数为:" + myAndroidInfo.toString());
            });
        });
    }
}
// 配置信息
public class MyAndroidInfo {

    public String devName;
    public int devAge;
}

经过这么一番折腾,我们自定义开发的插件有一个 Task: myAndroidTask,并且支持在 build.gradle 中配置信息,引入之后如下:

apply plugin: 'com.fireantzhang.plugin.greeting'

myAndroidInfo {
    devName="fireantzhang"
    devAge=18
}

// gradle 任务项
app
  >Tasks
    >android
    >build
    >cleanup
    >fireantzhang
        myAndroidTask

运行插件的自定义任务:./gradlew myAndroidTask,输出内容如下,也代表着配置项和任务读取配置项都是成功的:

» ./gradlew myAndroidTask

> Task :app:myAndroidTask
自定义插件中执行任务:myAndroidTask,获取到的参数为:MyAndroidInfo{devName='fireantzhang', devAge=18}

这个简单插件相关的示例代码,可以直接访问:github.com/fireantzhan…

了解 Android 官方提供的插件

有了前面了解的基本的 Gradle 插件开发知识之后,其实开发 Android 项目的时候,项目引入的就是 Android 官方开发的 Gradle 插件。

Android 项目里面最常见的就是官方提供的两个插件:com.android.applicationcom.android.library

  1. com.android.application 用于构建可用的 Android 应用程序;
  2. com.android.library 作为 module,用于生成 aar 包,可以引入到项目中;

而 Android 项目里面的 build.gradle 文件中常见的这些配置逻辑,其实就是这两个主要插件提供中的配置信息

android {

    compileSdkVersion 28
    buildToolsVersion "28.0.3"

    defaultConfig {
        applicationId = "com.fireantzhang.pluginsample"

        minSdkVersion 19
        targetSdkVersion 28
    }
}

文章开头的这些处理逻辑,实际上就是这些插件中相关类提供的 API 方法:

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifest.doLast {

            def manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
            // 省略后面对清单文件的替换处理逻辑
            ...
            ...
        }
    }
}

Android 官方开发的这两个插件地址也是开源的,源码访问方式有:

  • 第一种是用 Google 提供的 repo 工具从源码仓库:android.googlesource.com 直接克隆到本地查看,编译等,不过整个源码非常大,30G 以上,所以本文不推荐用这个方式,操作方式本文不作展开,可以自行了解细节;
  • 第二种是利用已有的 Android 项目,直接在 Android Studio 中可以查看,也是本文推荐的方式;

这里着重介绍通过第二种方式,如何查看 Android 官方提供的 Gradle 插件的源码,从而可以得知官方提供了那些配置项和 API

首先我们在一个已有的 Android 项目的应用级 build.gradle 中引入我们想要查看的插件版本,如下引入的是 3.5.2 版本(因为文章开头我们的项目就是升级到这个版本):

dependencies {
    ...
    ...
    
    implementation 'com.android.tools.build:gradle:3.5.2'
    ...
}

接着点击 Sync Now,在 External Libraries 下即可看到引入的 Android 插件代码结构

Android插件代码结构

开始寻找消费第二个 Warning 的方案

前面提到了几点:

  1. 如何开发一个简单的 Gradle 插件,并且可以支持配置信息,旨在说明 Android 中常见的这些配置项是怎么来的;
  2. 简单介绍 Android 插件,以及如何查看对应插件版本的源码;

有了这些前提条件,就可以回过头来跟大家介绍该如何寻找 Warning 解决方案了,Warning 产品的配置逻辑如下:

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifest.doLast {

            def manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
            // 省略后面对清单文件的替换处理逻辑
            ...
            ...
        }
    }
}

然后 Warning 内容如下:

API 'variantOutput.getProcessManifest()' is obsolete and has been replaced with 'variantOutput.getProcessManifestProvider()'. It will be removed at the end of 2019.

从 Warning 内容,可以很清楚知道 output.processManifest 这个方法已经被废弃,不建议继续使用,并且 2019 年末会进行移除,所以可以对这段逻辑调整如下,打印 output 的类信息,方便定位到 output 这个类的具体位置:

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        println("output 实现类信息:${output.getClass()}")
        output.processManifest.doLast {

            def manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
            // 省略后面对清单文件的替换处理逻辑
            ...
            ...
        }
    }
}

运行 ./gradle clean assembleDebug 之后得到:

output 实现类信息:class com.android.build.gradle.internal.api.ApkVariantOutputImpl_Decorated

接着直接访问 ApkVariantOutputImpl 这个类,发现没有类似 processManifest 的方法,别灰心,他有一个继承类,接着往父类找,可以看到下面一段逻辑:

@Override
@NonNull
public ManifestProcessorTask getProcessManifest() {
    deprecationReporter.reportDeprecatedApi(
            "variantOutput.getProcessManifestProvider()",
            "variantOutput.getProcessManifest()",
            TASK_ACCESS_DEPRECATION_URL,
            DeprecationReporter.DeprecationTarget.TASK_ACCESS_VIA_VARIANT);
    return taskContainer.getProcessManifestTask().get();
}

@NonNull
@Override
public TaskProvider<ManifestProcessorTask> getProcessManifestProvider() {
    //noinspection unchecked
    return (TaskProvider<ManifestProcessorTask>) taskContainer.getProcessManifestTask();
}

看到这里调整逻辑已经开始呼之欲出了,Warning 提议使用 variantOutput.getProcessManifestProvider(),而这个方法返回的是一个 TaskProvider,所以可以接着查看 TaskProvider 如何拿到我们需要的 ManifestProcessorTask,一直往上追溯,可以得知调用 get() 方法即可拿到:

TaskProvider<T> --> NamedDomainObjectProvider<T> --> Provider<T>

@NonExtensible
public interface Provider<T> {
    T get();
    ...
    ...
}

另外 manifestOutputDirectory 参数也是有变化,可以通过查看 ManifestProcessorTask 类,最终发现这个参数也是 Provider<T> 类型的,所以也是通过 get() 方法来获取到文件夹路径:

public abstract class ManifestProcessorTask extends IncrementalTask {
    ...
    ...

    @SuppressWarnings("unused")
    @Nonnull
    private final DirectoryProperty manifestOutputDirectory;
}

DirectoryProperty --> FileSystemLocationProperty<Directory> --> Property<T> --> Provider<T>

所以综合上面的信息,最终的逻辑调整方案如下即可消除 Warning:

// output.processManifest.doLast ==> output.processManifestProvider.get().doLast
// manifestOutputDirectory ==> {manifestOutputDirectory.get()}
android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.processManifestProvider.get().doLast {

            def manifestPath = "${manifestOutputDirectory.get()}/AndroidManifest.xml"
        }
    }
}

结语

升级 Aroid Gradle 编译插件版本时,面对 Warning 提示时,如果无法快速从前人的踩坑经验中找到靠谱的解决方案时,该如何从源码层面找到可靠的消除方案,毕竟自己动手,丰衣足食。

好了,今天的文章就先分享这么多,你的关注与留言是我输出分享内容的最大源动力,动动小手关注,才不会漏掉下一次的分享内容。

最后附上两个 Android 官方关于 Android Gradle 插件的说明文档地址,不过发现其实更新不是很及时,像这次升级的 3.5.2 版本我当时升级的时候就找不到相关的介绍: