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

本文原创于微信公众号「fireantzhang」,如需转载请联系作者!

前言背景

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

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

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

  1. 第一点是由于在 gradle.properties 中加入了 android.useDeprecatedNdk=true,这个主要是由于项目中用到的一个自研 SO 包用的是低版本的 ndk-bundle 构建出来的,由于历史久远,改动工作量大,所以这个短期只能先这样了;
  2. 第二点则是由于下面一段处理逻辑引发的,这段逻辑主要是用于动态替换清单文件中定义的一些第三方库用到的 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}

这个简单插件相关的示例代码,可以直接访问:https://github.com/fireantzhang/gradle-plugin-sample

了解 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 工具从源码仓库:https://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 版本我当时升级的时候就找不到相关的介绍:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容