当前位置:首页 > 实时新闻 > 正文

Android Studio项目Gradle构建实践

摘要: AndroidStudio项目Gradle构建实践最佳答案53678位专家为你答疑解惑AndroidStudio项目Gradle...

Android Studio项目Gradle构建实践

最佳答案 53678位专家为你答疑解惑

Android Studio项目Gradle构建实践

参考

中文官网构建指南:https://developer.android.google.cn/studio/build/index.html#

配置构建:https://developer.android.google.cn/studio/build/index.html

Mainfest合并规则:https://developer.android.google.cn/studio/build/manifest-merge.html

多变体(特定版本)构建指南:https://developer.android.google.cn/studio/build/build-variants.html

裁剪未使用代码和资源:https://developer.android.google.cn/studio/build/shrink-code.html

配置依赖项:https://developer.android.google.cn/studio/build/dependencies.html

名词构建类型(BuildType),编译时的类型,如debug, release产品风味(ProductFlavor),不同的产品特征,可以有不同的包名等等。构建变体(BuildVariant),每一个特定唯一确定版本apk都是一个构建变体的产物,其由构建类型和产品风味组成。APG,全称是Android Plugin for Gradle,google为使用gradle构建而开发的插件。1 一个典型的Android Studio 项目1.1 项目结构

一个新建的Android Stuido项目结构如下:

项目结构

包含三个.gradle文件:

settings.gradle 文件对应脚本执行时的setting对象,该文件最先被解析和执行,一些通用的初始化操作可以放在这里执行,项目包含多个子工程或者模块时,必须在该文件中include,这也是其最重要的功能之一。新建项目默认的settings.gradle
include ':app'

这里我想在脚本刚执行时打印项目的存放路径操作:

String projectDir=rootProject.projectDir.getAbsolutePath();println projectDirinclude ':app'
项目根目录下的build.gradle,对应脚本执行时的rootProject对象,一般不做具体的模块构建操作,用于指定项目所依赖的远程仓库和使用的Gradle plugin 插件版本,适用与所有的子工程或者模块。
// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {    //jcenter一个著名的远程代码仓库    repositories {        jcenter()    }            dependencies {        classpath 'com.android.tools.build:gradle:2.2.3'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}allprojects {    repositories {        jcenter()    }}task clean(type: Delete) {    delete rootProject.buildDir}
上面指定远程仓库的作用就是在需要依赖的库在本地找不到时,会到该仓库中去寻找并自动下载。依赖的构建插件,注意该插件不是Gradle的版本,是插件的版本,由google开发每个子项目或模块下单独的build.gradle脚本文件,在这里指定各自依赖的SDK,库属性等等,这也是我们编译的脚本的主体。
apply plugin: 'com.android.application'android {    compileSdkVersion 25    buildToolsVersion "25.0.1"    defaultConfig {        applicationId "com.inpor.fmcdevicedemon"        minSdkVersion 14        targetSdkVersion 25        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }}dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {        exclude group: 'com.android.support', module: 'support-annotations'    })    compile 'com.android.support:appcompat-v7:25.0.1'    testCompile 'junit:junit:4.12'}

apply plugin 指定要加载的插件,这里是一个应用,所以加载com.android.application插件,注意这个插件是我们上面使用的google开发的com.android.tools.build:gradle:2.2.3中携带的。

android闭包来自于google插件,这里查看其DSL文档:http://google.github.io/android-gradle-dsl/。

1.2 Gradle Project

我们的AS项目对于Gradle而言就是一个个Gradle项目,而Gradle项目对于我们而言就是一个个task构成的,我们可以点击Android Studio的最右边的Gradle工具栏,可以查看其项目结构。

gradle project

比如上面我们点开other目录,双击第一个任务,此时就可以直接这个任务,生成一个apk。

gradle project22 配置基本编译参数2.1 基本使用

这里主要是指设置编译时指定的SDK、bulidtools版本,包名,应用版本号等等,注意这里面定义的属性会覆盖AndroidMainfest.xml文件中定义的。

//编译时的SDK版本号compileSdkVersion 25//编译时指定的buildtools版本buildToolsVersion "25.0.1"defaultConfig {    //应用的包名    applicationId "com.inpor.fmcdevicedemon"        //指定应用可以安装到的系统的最低版本,这里14对应的是android 4.0    minSdkVersion 14        //运行时指定使用的sdk的版本,主要针对当存在多个sdk版本时,优先使用的SDK版本    targetSdkVersion 25        //应用版本号    versionCode 1    versionName "1.0"        //执行单元测试时指定的Runner,在正式打包时并不会使用到    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}
2.2 提取公用字段定义到其他文件中

前面说过我们可以把一个通用的属性存放到项目根目录下的build.gradle中。

//使用ext表示导出ext {    compileSdk=25    buildTools="25.0.1"    targetSdk=25    minSdk=14}

然后在app的build.gradle文件中使用定义的通用属性

compileSdkVersion rootProject.ext.compileSdkbuildToolsVersion rootProject.ext.buildToolsdefaultConfig {    applicationId "com.inpor.fmcdevicedemon"    minSdkVersion rootProject.ext.minSdk    targetSdkVersion rootProject.ext.targetSdk    versionCode 1    versionName "1.0"    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}

如果是多项目关联,把一些共有的属性提取出来就很有用了。

2.3 使用resConfigs只打包需要的资源

只打包我们需要的资源。我们知道google给我们的apk提供了国际化支持,如适应不同的屏幕分辨率的drawable资源,还有适应不同语言的字符串资源等等,但是在很多情况下我们只需要一些指定分辨率和语言的资源就可以了,这个时候我们可以使用resConfigs方法来配置。

defaultConfig中添加如下配置之后

defaultConfig {    .....    // 过滤,对于国际化支持只打包中文资源,和"xxhdpi"    // 注意如果在这里指定了dpi,则flavor中不能指定的dpi与这里必须一致否则会报错    resConfigs "zh-rCN", "xhdpi"}

在添加resConfigs之前,反编译的res目录截图:

未过滤图片

在添加上述resConfigs配置之后,反编译res目录:

过滤后的图片

注意:

使用resConfigs并不会过滤默认的drawable, values目录,这是为了保证App在任何时候都有一个默认的可选值。resConfigs也可以在后面要讲到的productFlavor中也可以使用。3 signingConfigs(Apk签名配置)3.1 配置不同的签名

在默认情况下,AS中编译apk使用的是SDK中的Debug签名,不需要显式的指定签名配置项,在signingConfigs的闭包中我们可以自定义多个签名配置,一个典型的签名配置:

signingConfigs {        //debug签名    debug {        //签名秘钥库文件的存放的位置,这里使用的是相对路径        storeFile file('sign/debug.keystore')                //秘钥库的访问密码        storePassword 'android'                //别名,因为一个密码库可以供多个项目使用,所以别名不同,最后的签名也是不同的。        keyAlias 'androidreleasekey'                //别名的私钥密码        keyPassword 'android'    }            release {        storeFile file('sign/platform.keystore')        storePassword 'android'        keyAlias 'androidreleasekey'        keyPassword 'android'    }}
3.2 从指定文件加载签名和秘钥

如果希望不在build.gradle中暴露自己的签名秘钥,可以将这些参数放到一个专门的文件中,比如在项目的根目录下添加一个keystore.properties文件。

//testdebugStoreFile=sign/debug.keystoredebugStorePassword=androiddebugKeyAlias=androidreleasekeydebugKeyPassword=android//releasereleaseStoreFile=sign/platform.keystorereleaseStorePassword=androidreleaseKeyAlias=androidreleasekeyreleaseKeyPassword=android

在app模块的build.gradle中,解析这个文件

// Create a variable called keystorePropertiesFile, and initialize it to your// keystore.properties file, in the rootProject folder.def keystorePropertiesFile=rootProject.file("keystore.properties")// Initialize a new Properties() object called keystoreProperties.def keystoreProperties=new Properties()// Load your keystore.properties file into the keystoreProperties object.keystoreProperties.load(new FileInputStream(keystorePropertiesFile))android{    .....}

修改signConfigs闭包,引用文件中定义的属性

signingConfigs {    debug {        keyAlias keystoreProperties['debugKeyAlias']        keyPassword keystoreProperties['debugKeyPassword']        storeFile file(keystoreProperties['debugStoreFile'])        storePassword keystoreProperties['debugStorePassword']    }        release{        keyAlias keystoreProperties['releaseKeyAlias']        keyPassword keystoreProperties['releaseKeyPassword']        storeFile file(keystoreProperties['releaseStoreFile'])        storePassword keystoreProperties['releaseStorePassword']    }}
4 编译类型(buildTypes

在Android studio中我们可以自定义不同的编译类型,如调试版本,发行版本,在不同的版本中可以配置不同的参数与添加属性,工程自带有一个debug编译类型,另外用户无法自定义为testtype,它已经被单元测试占用。

比如下面我定义了三个不同的buildType,分别设置不同的属性值

buildTypes {    debug {        //指定签名文件的配置,不指定则使用SDK中默认的debug签名        signingConfig signingConfigs.debug                //压缩对齐,提高运行时的效率,也可以使用zipAlignEnabled true        setZipAlignEnabled(true)                //可以调试        debuggable true                //jni可调试        jniDebuggable true                //渲染脚本可调试        renderscriptDebuggable true    }    //在发行版本中,不允许调试,并且添加代码混淆    release {        setZipAlignEnabled(true)        debuggable false        jniDebuggable false        renderscriptDebuggable false                //指定签名文件为release签名,注意非debug,如果不指定签名,则打出来的包不会签名        signingConfig signingConfigs.release                //minifyEnabled表示代码是否可以压缩,裁剪优化,需要配合其他的工具一起使用,如proguard        //添加代码混淆,注意添加混淆时,必须将minifyEnabled 置为true,否则混淆不生效        //同样如果没有使用代码混淆必须置为false,否则编译失败        minifyEnabled true        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'    }        //自定义一个type,不使用代码混淆,并且添加一个string资源到xml资源文件中    custom{        zipAlignEnabled true        debuggable false        jniDebuggable false        renderscriptDebuggable false        //指定签名文件为release签名        signingConfig signingConfigs.release        //添加一个字符资源到values/strings.xml文件中,目前无法指定资源的语言类别        resValue "string", "custom_name", "测试用户"    }}

上面的minifyEnabled还可以配合shrinkResources属性使用,移除没有使用到的资源文件。

buildTypes {    custom {        ......        minifyEnabled true        shrinkResources true        ......    }}

在实际测试中发现,上述裁剪可以剪裁布局、图片、菜单,但是不会移除values。

注意shrinkResources优化并不一定会删除没有用到的文件,在我的实际测试中,它会图片、布局变成最小,没有删除它们。

当我们需要动态加载资源时,需要在不要使用该优化,否则可能会出现运行时报错或者显示效果不正确的问题,如果要使用该优化可以在res/raw/keep.xml中进行特定资源的保持或优化,如下例子不优化layout/test_layout

<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"    tools:keep="@layout/test_layout"/>

参考(官网持有资源一章 ):https://developer.android.google.cn/studio/build/shrink-code.html#keep-resources

5 splits(拆分只包含某些需要属性的apk)splits其作用是将当前配置的版本的apk分裂生成多个只包含指定属性的Apk,目前在google给我们提供根据language, abi, density进行拆分。
//过滤只打包英文和简体中文的资源splits{    //设置根据language拆分测试未通过,应该是字符串的表现形式不对//        language{//            enable true//            include "values-zh-rCN"//            include "zh-rCN"//        }    density{        enable true        reset()  // Clears the default list from all densities to no densities.        include "mdpi", "xxhdpi" // Specifies the two densities we want to generate APKs for.    }}

上面的配置编译之后会生成3个Apk:

app-mdpi-custom.apk  //裁剪掉大部分非mdpi资源的apkapp-xxhdpi-custom.apk  //裁减掉大部分非xxdpi资源的apkapp-universal-custom.apk //未做任何裁剪的apk              

参考Android apk splits 官方文档:https://developer.android.google.cn/studio/build/configure-apk-splits.html

6 PackagingOptions(指定添加/移除某些文件到最终的apk中)

首先看其DSL结构图。

PackagingOptions不同于resConfigs,后者过滤某些资源目录,前者是在打包Apk的时候(已经执行过编译了)排除一些文件,在实际测试中并不能用于过滤资源文件等等,更多是用于过滤一些与工程没有直接关系的文件(声明、版本控制等等)。

First-pick,如果要添加的文件已经存在于Apk中,则会被忽略,如果有多个路径于指定的pattern,只添加第一个。Merge,合并的意思,如果文件不存在,则添加,如果文件已经存在,则合并内容。Exclude,不包含的内容,默认以下的内容不会被打包到Apk中:
/META-INF/LICENCE/META-INF/LICENCE.txt/META-INF/NOTICE/META-INF/NOTICE.txt/LICENCE/LICENCE.txt/NOTICE/NOTICE.txt**/.svn/** (all .svn directory contents)**/CVS/** (all CVS directory contents)**/SCCS/** (all SCCS directory contents)**/.* (all UNIX hidden files)**/.*/** (all contents of UNIX hidden directories)**/*~ (temporary files)**/thumbs.db**/picasa.ini**/about.html**/package.html**/overview.html**/_***/_*/**

PackagingOptions更多的用于去除编译时依赖不同的包时,含有相同的文件时,去除编译时的重复错误中。如:

//在打包时,移除一些许可,注意文档packagingOptions {    exclude 'META-INF/DEPENDENCIES.txt'    exclude 'META-INF/NOTICE'    exclude 'META-INF/NOTICE.txt'    exclude 'META-INF/LICENSE'    exclude 'META-INF/LICENSE.txt'}

实际测试中还没有发现有其他的作用,待补充。

官方DSL文档链接:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html

7 lintOptions

lint检查工具是google开发的一款代码扫描工具,其主要用于扫描布局,未使用资源,国际化等等问题,其作用在这里不是我们关注的重点,其使用和配置方法请查看官方文档:https://developer.android.google.cn/studio/write/lint.html。

这里我们要考虑的是link选项对我们打包的影响,要注意link检查抛出来的错误,并不会导致编译时候的错误,可能会导致运行时的错误,以是lintOptions的属性截图:

lintOptions

在Android Studio中默认下,link检查报错会导致编译中断,为了避免这个问题,我们可以在android闭包中添加如下代码:

android {    ......    lintOptions {        //关闭编译release版本的lint检查        checkReleaseBuilds false                //关闭link检查报错中断编译选项        abortOnError false    }    ......}

在日常研发中,我们应当频繁执行lint检查,以优化代码和提前暴露一些可能运行时报错的代码。

8 productFlavor(产品风味)8.1 基本属性与方法

productFlavor与其说是产品风味还不如说是产品工厂,我们可以根据不同的需要,最终生成不同的apk。多渠道打包是productFlavor的最常用的功能之一。上面说到的defaultConfig我们可以认为一种简略的默认productFlavor,所以我们完全可以在自定义的productFlavor中覆盖defaultConfig中的任意配置项。

基本属性perpties

方法与闭包

method

参考:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.ProductFlavor.html

从上面的属性与方法中我们发现可以设置包名,版本,混淆文件, NDK等等

8.2 示例

现在我创建多个productFlavor,它们具有不同的包名,不同的版本号

productFlavors{    sky{        //直接在原来的包名后加后缀        applicationIdSuffix ".sky"                //指定不同的版本        versionName '1.2.0'        versionCode 120    }    gavin{        //重新命名包名        applicationId "com.gavin.gradlebuilddemo"                //指定不同的最小编译版本        minSdkVersion 17        targetSdkVersion 21    }    smith{                applicationId "com.smith.gradlebuilddemo"                //指定不同的resConfig        resConfigs "zh-rHK",        //添加resValue        resValue 'string', 'smith', 'this is smith'    }}

考虑到一种情况,有时候我们有些公共的资源和配置是某些productFlavors公用的,我们希望把它们提取出来,减少重复,这个时候我们可以使用flavorDimensions来实现我们的需求。

//使用dimensions将一些公共的修改独立出来,可以重复使用,减小代码的重复flavorDimensions 'type', 'common'    productFlavors{    sky{        dimension 'type'                //直接在原来的包名后加后缀        applicationIdSuffix ".sky"                //指定不同的版本        versionName '1.2.0'        versionCode 120    }    gavin{        dimension 'type'                //重新命名包名        applicationId "com.gavin.gradlebuilddemo"                //指定不同的最小编译版本        minSdkVersion 17        targetSdkVersion 21    }    smith{        dimension 'type'                applicationId "com.smith.gradlebuilddemo"                //指定不同的resConfig        resConfigs "en", "hdpi"        //添加resValue        resValue 'string', 'smith', 'this is smith'    }        commonClient{        dimension 'common'                //添加resValue        resValue 'string', 'common_client', 'this is common_client'    }        commonPrivate{        dimension 'common'                //指定一个私有的混淆规则        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-private.pro'    }}

采用上述实现中,会将两两不同的dimension进行组合,最终生成我们需要的apk。比如app-sky-commonClient-debug.apk。

现在我们来计算一下最终可以生成的apk的数目,我们将flavorDimensions看成数组的话,最终可以生成的apk的数目为:

count=BuildType.size * flavorDimensions[0].size * ... flavorDimensions[n].size

这里的flavorDimensions[n].size是指每个DimensionsproductFlavors中的个数,比如上面的最终能够生成的apk个数就是:3 * 3(type)* 2(common)=18。

8.3 资源替换

同样的我们也可以在src\main的同级目录下给每个productFlavors建立目录存放我们的特定资源。

srcFlavorDir

替换res资源

替换res资源采用的是合并的原则,即最终的资源是所有进行合并的资源的并集,出现资源ID重复的资源,采用优先级最高的那个,具体的优先级后面会讲到。

main/res/values/strings.xml中定义了这样的资源。

<resources>    <string name="app_name">GradleBuildDemo</string>    <string name="hello">hello world</string>    <string name="enter_button_str">enter</string>    <string name="cancel_button_str">cancel</string>    <string name="input_tips">Please input the word you want</string>    <string name="cancel_tips_msg">button is clicked</string></resources>

sky/res/values/strings.xml中重新定义了如下资源。

<resources>    <string name="app_name">GradleBuildDemo_Sky</string>    <string name="hello">hello world, sky</string>    <string name="enter_button_str">enter sky help</string>    <string name="cancel_button_str">cancel sky</string></resources>

最终合并的资源是这样的。

<resources>    <string name="app_name">GradleBuildDemo_Sky</string>    <string name="hello">hello world, sky</string>    <string name="enter_button_str">enter sky help</string>    <string name="cancel_button_str">cancel sky</string>    <string name="input_tips">Please input the word you want</string>    <string name="cancel_tips_msg">button is clicked</string></resources>

注意,layout资源是以整个文件覆盖的方式合并的。

assets目录

assets目录中的文件是以整个文件覆盖的方式进行合并的。

java原代码目录

源码文件的合并与其他的不同,如果我们想在不同的变体中对一个类做不同的实现,那么我们就不能在main/java目录下定义这个类,只能在每个变体中单独定义,并且路径要一致,而且对一些变体进行组合时,同时也只能存在一份代码。

以下示例中,我分别在sky, gavin两个flavors中定义了HelpTestActivity类。

sky版本的HelpTestActivity类。

package com.sky.gradlebuilddemo.activity;import android.graphics.Color;import android.os.Bundle;import android.support.annotation.Nullable;import android.support.design.widget.Snackbar;import android.support.v7.app.AppCompatActivity;import android.view.View;import android.widget.Button;import com.sky.gradlebuilddemo.R;/** * PACKAGE_NAME * [function] * [detail] * Created by Sky on 2016/10/24. * modify by */public class HelpTestActivity extends AppCompatActivity {    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_help_test);        Button skyButton=(Button) findViewById(R.id.skyButton);        //点击按钮弹出提示文案,        skyButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                final Snackbar snackbar=Snackbar.make(HelpTestActivity.this.getWindow().getDecorView(),                        "hello snackbar", Snackbar.LENGTH_LONG);                snackbar.setAction("Change Color", new View.OnClickListener() {                    @Override                    public void onClick(View v) {                        snackbar.getView().setBackgroundResource(R.color.colorPrimary);                    }                }).show();            }        });    }}

gavin版本的HelpTestActivity

package com.sky.gradlebuilddemo.activity;import android.content.Context;import android.os.Bundle;import android.support.annotation.Nullable;import android.support.v7.app.AppCompatActivity;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.Adapter;import android.widget.BaseAdapter;import android.widget.ListAdapter;import android.widget.ListView;import android.widget.SimpleAdapter;import android.widget.TextView;import com.sky.gradlebuilddemo.R;/** * PACKAGE_NAME * [function] * [detail] * Created by Sky on 2016/10/24. * modify by */public class HelpTestActivity extends AppCompatActivity {    private ListView listView;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_help_test);        listView=(ListView) findViewById(R.id.msgListView);        String[] msgs=getResources().getStringArray(R.array.listMsg);        listView.setAdapter(new SimpleListAdapter(this, msgs));    }    private static class SimpleListAdapter extends BaseAdapter{        private String[] data;        private Context context;        SimpleListAdapter(Context context, String[]data){            this.data=data;            this.context=context;        }        @Override        public int getCount() {            return data.length;        }        @Override        public Object getItem(int position) {            return data[position];        }        @Override        public long getItemId(int position) {            return 0;        }        @Override        public View getView(int position, View convertView, ViewGroup parent) {            if(convertView==null){                convertView=LayoutInflater.from(context).inflate(R.layout.list_item_layout, null);            }            TextView  textView=(TextView) convertView.findViewById(R.id.itemTextView);            textView.setText(data[position]);            return convertView;        }    }}

MainActivity中调用它。

package com.sky.gradlebuilddemo;import android.content.Intent;import android.support.design.widget.Snackbar;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import com.sky.gradlebuilddemo.activity.HelpTestActivity;public class MainActivity extends AppCompatActivity {    private static final String TAG="MainActivity";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button enterButton=(Button) findViewById(R.id.enter_button);        enterButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                MainActivity.this.startActivity(new Intent(MainActivity.this, HelpTestActivity.class));            }        });        .......    }}
9 sourceSets(资源集合的集合)9.1 sourceSet与优先级

sourceSet就是所谓的源集,包含特定源代码和所需资源,每一个源集不是任意命名的,每一个源集对应一个BuildTypeProductFlavorBuildVariant,看官方的文档描述。

sourceSet参考:https://developer.android.google.cn/studio/build/index.html#sourcesets

Android Studio在编译某个构建变体的时候,并不是单独的使用某个源集,而是merge不同的源集,比如有一个SkyCommonClientCustom的构建变体,并且定义了skyCommonClientCustom, custom, sky, commonClient这四个源集,那么在构建的时候就会合并上述的四个源集和默认的main源集的源代码和资源。

上面提到了merge合并资源,那么合并的优先级是怎样的呢?

SourceSetPority

需要补充一点,如果在ProductFlavor使用了flavorDimensions,比如:

flavorDimensions 'type', 'common'sourceSets{    sky{       .....    }        commonClient{       .....    }}productFlavors{    sky{        dimension 'type'        //直接在原有包名后面添加        applicationIdSuffix ".sky"    }    commonClient{        dimension 'common'    }}

那么源集sky的优先级高于commonClient,所以如果把flavorDimensions看做一个数组的话,最终的优先级是:

BuildVariant > BuidlType > flavorDimensions[0] > ... > flavorDimensions[x] > main > 内容库依赖项(aar等)

9.2 sourceSet的基本属性

sourceSet简单来说就是指定在编译指定了某些特定源代码和资源的集合,在Android中使用的是googleAndroidSourceSet

android闭包中的sourceSets就是由上面用户定义的一系列的AndroidSourceSet的集合。

先看AndroidSourceSet的DSL属性结构图:

AndroidSourceSet

从上面的图中我们可以看出,针对每个AndroidSourceSet可以配置不同的:

//跨进程通信声明文件(.aidl)aidl//assets文件下文件assets//.java文件目录java//.c, .cpp文件位置jni //.so文件路径,注意该路径只需要指定到所包含平台的外层,不需要指定到具体的平台如`armeabi`,否则无法找到SOjnilibs//mainfest文件manifest//Android resourceres//(java resource)resource//渲染脚本rendersript

最终根据这些不同的资源集合生成不同的apk

以下四个属性为只读属性:

//sourceSet的名称,如customname //编译时的配置名称,如customCompile,与后面要讲的dependencies的配置项compile相对应。compileConfigurationName//如customApk,与后面要讲的dependencies的配置项apk相对应。packageConfigurationName//如customProvided,与后面要讲的dependencies的配置项provided相对应。 providedConfigurationName
9.3 示例
sourceSets {    main{        jniLibs.srcDirs=['libs']    }    custom{        //指定一个新的jnilibs为根目录下的jnilibs        jniLibs.srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']        //指定一个新的assets为根目录下的skyTestAssets目录        assets.srcDirs=[rootProject.projectDir.absolutePath + '/assets/skyTestAssets']    }}

上面的例子中我们给custom这个源集指定了新的jniLibs和Assets目录,上面的源集也可以采用闭包的形式。

custom{        jniLibs{        //指定一个新的jnilibs为根目录下的jnilibs        srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']    }        assets{        //指定一个新的assets为根目录下的skyTestAssets目录        srcDirs=[rootProject.projectDir.absolutePath + '/assets/skyTestAssets']    }}

两点注意:

srcDirsrcDirs的区别,当使用srcDir指定文件目录时,不允许将要合并的源集的同一目录下有一样名称的资源,提示重复资源异常,而srcDirs则会根据前面所说的优先级进行覆盖。

如果我们在src/main的同级目录下,也建立一个如下的文件目录:

buildTypeCustonDir

当在源集中指定了asserts的目录时,custom/assets目录会直接失效。

9.4 未解决的问题

在上面的源集中,Android官方构建指南没有提及一点,就是如何过滤源集目录下的一些文件不编译或打包到最后的apk中,我使用如下方式,希望过滤掉src/main/assets/mainIngoreTest.txt文件不打包到最终的apk中。

sourceSets{    .....    custom{        //指定一个新的jnilibs为根目录下的jnilibs        jniLibs.srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']        //指定一个新的assets为根目录下的skyTestAssets目录        assets.srcDirs=[rootProject.projectDir.absolutePath + '/assets/skyTestAssets']                //意图过滤掉`src/main/assets/mainIngoreTest.txt`文件        assets.exclude (project.projectDir.absolutePath + "\\src\\main\\assets\\mainIngoreTest.txt")                //或者采用闭包        assets.exclude{            File f=it.file            println f.absolutePath            f.absolutePath.endsWith("mainIngoreTest.txt")        }            }    .....}

采用上述方式并不能成功过滤掉文件,并且上面的闭包中的代码也没有执行,目前还没有找到原因。

10 Dependencies(依赖内容库)

依赖内容库指的是不是当前工程的代码或资源,它们存在于其他的项目中,它们可以以源码库,jar,arr形式存在。

10.1 声明依赖项的三种方式

先看声明方式。

android {...}...dependencies {    // 添加含有源码的模块依赖    compile project(":mylibrary")    // 远程二进制库依赖    compile 'com.android.support:appcompat-v7:25.1.0'    // 本地库(jar)依赖    compile fileTree(dir: 'libs', include: ['*.jar'])}

下面逐一介绍,以下来自官方文档。

模块依赖项

compile project(':mylibrary')行声明了一个名为mylibrary的本地Android库模块作为依赖项,这样的库可能是另外某个工程的一部分,此时是具有源代码的,注意依赖本地库模块时,必须在根目录下的settings.gradle文件中include它。

远程二进制依赖项

compile 'com.android.support:appcompat-v7:25.1.0' 行通过指定其 JCenter 远程仓库中的标识,当本地不存在该依赖库时,则自动从远程下载,默认存放在sdk/extras/目录下,当然我们也可以在 SDK 管理器下载和安装特定的依赖项。

本地二进制依赖项

简单来说就是依赖已经打包好的jar库,compile fileTree(dir: 'libs', include: ['*.jar'])的意思就是依赖app/libs目录下的所有的以.jar结尾的文件。10.2 配置依赖项

当我们希望对依赖项在编译和打包时做一些特殊处理的时候,通过使用不同的关键词,google给我们提供三种配置方式:

compile,最常见的配置项,编译时依赖,Gradle将此配置依赖项添加到类路径和最终的apk中,其实就是说在编译时和最终的apk中都需要。

apk,其指定的依赖项只在打包最终的apk的时候才需要,此配置只能和JAR二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。

provided,其指定的依赖项,此配置依赖项将添加到类路径中,只在编译的时候需要,不打包到最终的apk中(也就是说运行时无须该依赖项),比如我们编译时使用的SDK就属于这一类,同样的此配置只能和JAR二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。

示例:

dependencies {     .....    // 依赖app/apklib下的jar文件,只在apk打包的时候依赖    apk fileTree(dir: 'apklib', include: ['*.jar'])        // 依赖app/rovidedlib下的jar文件,只在编译的时候依赖    provided fileTree(dir: 'providedlib', include: ['*.jar'])}
10.3 指定特定的构建变体的依赖项

在实际构建中,我们常常遇到有这样的需求,我们希望某些依赖项只有在某些特定构建变体编译时才被依赖,在Android Studio中我们可以指定依赖项在以下特定的构建类型下才依赖:

BuildTypesBuildVariantProductFlavors

也就是说我们可以以上述任意一种方式指定特定的依赖项,我们知道每一个buildType, flavor都有一个与之名字相同的的sourceSet,所以我们要指定依赖项为某特定类型的方式为:

sourceSet.compileConfigurationName,如skyCompilesourceSet.packageConfigurationName,如skyApksourceSet.providedConfigurationName,如skyProvided

以下是具体示例。

dependencies {    // BuildTypes为AndroidTest,做单元测试时才编译    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {        exclude group: 'com.android.support', module: 'support-annotations'    })            // BuildTypes为test的 依赖项junit    testCompile 'junit:junit:4.12'    // BuildTypes为debug的添加含有源码的模块依赖    debugCompile project(":mylibrary")        // flavor为sky的指定jar库,且不使用`okhttp-3.2.0.jar`库,使用skyCommon目录下的okhttp-3.3.1.jar    skyCompile fileTree(include: ['*.jar'], dir: 'jar/sky', excludes: ['okhttp-3.2.0.jar'])        // 构建变体依赖项指定    skyCommonClientCustomCompile fileTree(include: ['*.jar'], dir: 'jar/skyCommonClientCustom')    //以下是正常的依赖    // 远程二进制库依赖    compile 'com.android.support:appcompat-v7:25.1.0'    // 本地库(jar)依赖    compile fileTree(dir: 'libs', include: ['*.jar'])        compile 'com.android.support:design:24.2.1'}

注意,在上述使用xxxCompile时,有时会提示找不到对应的xxxCompile方法的错误:

Error:(180, 0) Could not find method skyCommonClientCustomCompile() for arguments [directory 'jar/skyCommonClientCustom'] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.<a href="openFile:E:\Source\GitHub\GradleBuildDemo\app\build.gradle">Open File</a>

解决办法为,我们在sourceSets闭包下,新建一个空的对应的soruceSet就可以了。

sourceSets{    .....    // 此处增加一个空的sourceSet,是为了解决在dependencies中使用    // skyCommonClientCustomCompile 指定依赖项时提示找不到方法的错误    skyCommonClientCustom{    }}

资源合并的规则同样适用与依赖合并,所以在指定特定依赖后,构建某个特定变体时(flavorType),其编译时最终的依赖项(不考虑provided)就变为:

flavorTypeCompile + typeCompile + flavorCompile + compile

指定依赖项的构建类型

我们可以直接指定依赖项的构建类型,这里的构建类型可以是BuildVariant, buildType, flavor

dependencies {    ...    // relase构建时指定依赖的`library`也是release    releaseCompile project(path: ':library', configuration: 'release')        // debug构建时指定依赖的`library`的构建也是`debug`    debugCompile project(path: ':library', configuration: 'debug')    ......}
10.4 transitive, force, exclude的使用与依赖冲突解决

通过gradle命令查看依赖树,在模块所在的目录(app目录),执行gradle dependencies,执行结果如图(以androidTest为例)。

android_test_dependenice

transitive

transitive用于自动处理子依赖项。默认为true,gradle自动添加子依赖项,形成一个多层树形结构;设置为false,则需要手动添加每个依赖项。

为所有的配置指定自动添加子依赖项为false
configurations.all {   transitive=false}
为单独的某个依赖项指定字典添加子依赖项为false
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {       transitive=false     })

force

即强制设置某个模块的版本。

configurations.all {   resolutionStrategy {       force 'com.android.support.test:runner:0.2'   }}

以上设置之后所有对com.android.support.test:runner模块有依赖的其他库都被强制使用0.2版本。

exclude

排除依赖项中的某些子依赖项,这在解决依赖库版本冲突或者重复时特别有用,我们可以通过如下两种方式进行排除:

groupmaven项目的GroupId,GroupID是项目组织唯一的标识符,对于小型的项目,常常对应JAVA的包的结构,是main目录里java的目录结构,但是也可以很多个项目共用一个GroupID,如com.android.support下就有很多个子项目。

module, maven项目的ArtifactID,ArtifactID就是具体某个项目的唯一的标识符,实际常常对应项目的名称,就是项目根目录的名称,如support-annotations

groupmodule可以配合一起使用也可以单独使用。

配合使用
//移除所有依赖项中,组织为`com.android.support`项目为`support-annotations`的子依赖项configurations {   all*.exclude group: 'com.android.support', module: 'support-annotations'}//移除单个依赖项中,组织为`com.android.support`项目为`support-annotations`的子依赖项androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {    exclude group: 'com.android.support', module: 'support-annotations'})
单独使用groupmodule
//移除所有依赖项中,组织为`com.android.support`的子依赖项configurations {   all*.exclude group: 'com.android.support'}//移除单个依赖项中,组织为`com.android.support`的子依赖项androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {    exclude group: 'com.android.support'})
单独使用module
//移除所有依赖项中名为`support-annotations`的子依赖项configurations {   all*.exclude module: 'support-annotations'}//移除单个依赖项中名为`support-annotations`的子依赖项androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {    exclude module: 'support-annotations'})

依赖项的版本冲突

gradle在同一个配置下(例如androidTestCompile),某个模块的不同版本同时被依赖时,默认使用最新版,gradle同步时不会报错,例如:

dependencies {   androidTestCompile('com.android.support.test:runner:0.4')   androidTestCompile('com.android.support.test:rules:0.2')   androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')}

上面espresso:espresso-core依赖runner不同与上面我们上面指定的版本的runner,此时gradle会自动同步最新版本。

对于不同的配置下,出现的依赖项不一样,gradle就会直接报错,这时我们就可以使用上面提到的force, exclude来解决。

10.5 参考

Google官网依赖项说明:

https://developer.android.google.cn/studio/build/dependencies.htmlhttps://developer.android.google.cn/studio/build/build-variants.html#dependencies

Gradle官方说明文档:

https://docs.gradle.org/current/userguide/artifact_dependencies_tutorial.htmlhttps://docs.gradle.org/current/userguide/dependency_management.html#sec:how_to_declare_your_dependencies

依赖的dependencies、transitive、force、exclude及版本冲突处理

http://www.paincker.com/gradle-dependencies11 综合11.1 过滤不需要生成的apk

当我们添加自定义的BuildType, flavor时,必然会组合出很多我们不需要的apk,google也给我们提供了解决方案。

核心就是使用variantFilter这个方法过滤满足我们特定条件的所有构建变体。

// 移除不需要打包的apkvariantFilter { variant ->    String buildTypeName=variant.buildType.name    String flavors0Name=variant.getFlavors().get(0).name    //对于编译类型为`release 或者 custom` 并且 flavors0类型为`smith 或 gavin`的构建类型直接忽略,不用编译    if((buildTypeName.equals('release') || buildTypeName.equals('custom'))            && (flavors0Name.equals('smith') || flavors0Name.equals('gavin'))) {        variant.setIgnore(true);    }}
11.2 apk名称修改

Android studio 构建生成的apk的默认名称为app-flavor[0]-...-flavor[n]-buildType.apk,google给我们提供了修改Apk名称的方法。

applicationVariants包含了所有可能构建变体的集合,我们使用闭包遍历所有的输出,修改我们想修改的apk的名称,以下是一个示例。

//修改生成的apk的名称,命名为demo-flavorsName-buildType-versionName.apkapplicationVariants.all { variant ->    //遍历所有的输出文件    variant.outputs.each { output ->        File tempFile=output.outputFile        //对于包含`commonClient` flavor的我们在名称中去掉它        if (variant.productFlavors[1].name.contains("commonClient")) {            output.outputFile=new File(tempFile.parent, tempFile.name.replace(tempFile.name,                    "demo" + variant.productFlavors[0].name + "_" + variant.buildType.name + "_${variant.versionName}.apk"))        } else {            output.outputFile=new File(tempFile.parent, tempFile.name.replace(tempFile.name,                    "demo" + variant.productFlavors[0].name + "_" + variant.productFlavors[1].name + "_" + variant.buildType.name + "_${variant.versionName}.apk"))        }    }}
11.3 mainfest文件添加属性

当我们打多渠道包或需要给不同的构建变体加入的不同的属性时,此时我们就需要修改mainfest文件,gradle给我们提供了两种方式动态的修改mainfest文件。

在变体对应src目录下添加一个mainfest文件

在添加的mainfest文件中添加/修改配置项,此种方式与动态合并res/values/strings.xml的方式一致,其遵守的规则也和它们保持一致,这种方式不仅能添加属性,还能添加四大组件等等,具体可以参考:

Mainfest合并规则:https://developer.android.google.cn/studio/build/manifest-merge.html

使用APG中的manifestPlaceholders属性

manifestPlaceholdersbuild.gradle中以键值对的方式给mainfest中对应键设置相应的值的方式来实现,看如下示例,我在mainfest文件中添加以下需要动态设置的属性。

    <application        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">                .....        <!--渠道配置信息AppKey  -->        <meta-data            android:name="APP_KEY"            android:value="${APP_KEY_VALUE}"/>        <!-- 产品ID -->        <meta-data            android:name="APP_ID"            android:value="${APP_ID_VALUE}"/>            .....   </application>

接下来我分别在defaultCofig, sky, gavin三个变体中从不同路径的配置文件config.xml中读取属性,这里我把解析xml文件放在根目录下自定义的utils.gradle文件中。

/** * 解析XML文件 * */def parseXml(String path) {    println ("parseXml called, path=" + path)    return new XmlParser().parse(path)}ext {    parseXml=this.&parseXml}

然后在模块的build.gradle文件中引用它。

// 加载自定义的utils.gradleapply from: rootProject.projectDir.getAbsolutePath() + File.separator + "utils.gradle"

最后在三个变体中解析相应的配置文件。

defaultConfig {        ......    def defaultConfig=parseXml("app/config/main/config.xml")    manifestPlaceholders=[            APP_KEY_VALUE : defaultConfig.appKey[0].text(),            APP_ID_VALUE  : defaultConfig.id[0].text()    ]}flavor{    sky{        ......        def skyConfig=parseXml("app/config/sky/config.xml")        manifestPlaceholders=[            APP_KEY_VALUE : skyConfig.appKey[0].text(),            APP_ID_VALUE  : skyConfig.id[0].text()        ]    }        gavin{        ......        def gavinConfig=parseXml("app/config/gavin/config.xml")        manifestPlaceholders=[                APP_KEY_VALUE : gavinConfig.appKey[0].text(),                APP_ID_VALUE  : gavinConfig.id[0].text()        ]            }        ......}

这样最后构建出来的apk使用的就是上面配置的不同的值。

11.4 APG动态生成的BuildConfig类的使用

使用BuildConfig

使用APG构建apk,会动态自动生成一个BuildConfig类,里面会包含当前构建变体的一些基本属性,如版本号等等,一下是一个默认下的示例(当前构建变体是skyCommonClientDebug)。

/** * Automatically generated file. DO NOT MODIFY */package com.sky.gradlebuilddemo;public final class BuildConfig {  public static final boolean DEBUG=Boolean.parseBoolean("true");  public static final String APPLICATION_ID="com.inpor.fmcdevicedemon.sky";  public static final String BUILD_TYPE="debug";  public static final String FLAVOR="skyCommonClient";  public static final int VERSION_CODE=120;  public static final String VERSION_NAME="1.2.0";  public static final String FLAVOR_type="sky";  public static final String FLAVOR_common="commonClient";}

一种最典型的用法,就是在源代码中判断当前是不是debug版本,然后做某些操作,如果是其他版本又做什么操作等等。

一个示例,在MainActivity中添加如下代码。

public class MainActivity extends AppCompatActivity {    private static final String TAG="MainActivity";        .....    private void checkBuildConfig(){        if(BuildConfig.DEBUG){            Log.i(TAG, "now this is debug build");        }    }}

自定义BuildConfig属性

APG给我们提供了自定义BuildConfig属性的方法buildConfigField,注意,添加Field时,最好在defaultConfig中给要添加的Field设置一个默认值,否则当编译其他没有设置该Field的变体时,会编译报错,我们可以在buildTypes, flavors中复写它。

在下面的示例中我添加一个字段。

defaultConfig {        ......    buildConfigField 'int', 'ID', '0'}flavor{    sky{        ......        buildConfigField 'int', 'ID', '1'    }        ......}

最终生成的BuildConfig是这样的。

/** * Automatically generated file. DO NOT MODIFY */package com.sky.gradlebuilddemo;public final class BuildConfig {  public static final boolean DEBUG=Boolean.parseBoolean("true");  public static final String APPLICATION_ID="com.inpor.fmcdevicedemon.sky";  public static final String BUILD_TYPE="debug";  public static final String FLAVOR="skyCommonClient";  public static final int VERSION_CODE=120;  public static final String VERSION_NAME="1.2.0";  public static final String FLAVOR_type="sky";  public static final String FLAVOR_common="commonClient";  // Fields from product flavor: sky  public static final int ID=1;  // Fields from default config.}

使用新 Android Gradle 插件加速您的应用构建

自 2020 年底,Android Gradle 插件 (AGP) 已经开始使用新的版本号规则,其版本号将与 Gradle 主要版本号保持一致,因此 AGP 4.2 之后的版本为 7.0 (目前最新的版本为 7.2)。在更新 Android Studio 时,您可能会收到一并将 Gradle 更新为最新可用版本的提示。为了获得最佳性能,建议您使用 Gradle 和 Android Gradle 插件这两者的最新版本。Android Gradle 插件的 7.0 版本更新带来了许多实用的特性,本文将着重为您介绍其中的 Gradle 性能改进、配置缓存和插件扩展等方面的内容。

如果您更喜欢通过视频了解此内容,请在 点击此处[1] 查看。

Gradle 的性能改进

Kotlin 符号处理优化

Kotlin[2]) 是 kapt (Kotlin annotation processing tool) 的替代品,它为 Kotlin 语言带来了一流的注解处理能力,处理速度最快可以达到 kapt 的两倍。目前已经有不少知名的软件库提供了兼容 KSP 的注解处理器,比如 Room、Moshi、Kotishi 等等。因此我们建议,当您的应用中所用到的各种注解处理器都支持 KSP 时,应该尽快从 kapt 迁移到 KSP。

非传递性 R 类

启用非传递性 R 类 (non-transitive R-class) 后,您应用中的 R 类将只会包含在子项目中声明的资源,依赖项中的资源会被排除在外。这样一来,子项目中的 R 类大小将会显著减少。

这一改动可以在您向运行时依赖项中添加新资源时,避免重新编译下游模块。在这种场景下,可以给您的应用带来 40% 的性能提升。另外,在清理构建产物时,我们发现性能有 5% 到 10% 的改善。

您可以在 gradle.properties 文件中添加下面的标记:

android.nonTransitiveRClass=true

△ 在 gradle.properties 中开启非传递性 R 类功能

您也可以在 Android Studio Arctic Fox[3] 及以上版本使用重构工具来启用非传递性 R 类,具体需要您运行 Android Studio 菜单栏的 Refactor --> Migrate to Non-transitive R Classes。这种方法还可以在必要时帮助您修改相关源代码。目前,AndroidX 库已经启用此特性,因此 AAR 阶段的产物中将不再包含来自传递性依赖项的资源。

Lint 性能优化

从 Android Gradle 插件 7.0 版本开始,Lint 任务可以显示为 "UP-TO-DATE[4]",即如果模块的源代码和资源没有更改,那么就不需要对该模块进行 Lint 分析任务。您需要在 build.gradle 中添加选项:

// build.gradleandroid {  ...  lintOptions {    checkDependencies true  }}

△ 在 build.gradle 中开启 lint 性能优化

如此一来,Lint 分析任务就可以在各个模块中并行执行,从而显著提升 Lint 任务运行的速度。

从 Android Gradle 插件的 7.1.0-alpha 13 版本开始,Lint 分析任务兼容了 Gradle 构建缓存 (Gradle build cache),它可以通过 复用其他构建的结果来减少新构建的时间[5]:

△ 不同 AGP 版本中 Lint 时间比较

我们在一个演示项目中开启了 Gradle 构建缓存并设置 checkDependencies 为 true,然后分别使用 AGP 4.2、7.0 和 7.1 进行构建。从上图中可看出,7.0 版本的构建速度是 4.2 的两倍;并且在使用 AGP 7.1 时,由于所有 Lint 分析任务都命中了缓存而带来了更加显著的速度提升。

您不但可以直接通过更新 Android Gradle 插件版本获得更好的 Lint 性能,还能通过一些配置来进一步提升效率。其中一种方法是使用可缓存的 Lint 分析任务。要启用 Gradle 的构建缓存,您需要在 gradle.properties 文件中开启下面的标记 (参见 Build Cache[6]):

org.gradle.caching=true

△ 在 gradle.properties 中开启 Gradle 构建缓存

另一种可改进 Lint 分析任务性能的方法是,在您条件允许的情况下给 Lint 分配更多的内存。

同时,我们建议您在 应用模块 的 Gradle 配置中为 lintOptions 块添加:

checkDependencies true

△ 在模块的 build.gradle 中添加 checkDependencies 标记

虽然这样不能让 Lint 分析任务更快执行,但能够让 Lint 在分析您指定应用时捕捉到更多问题,并且为整个项目生成一份 Lint 报告。

Gradle 配置缓存

△ Gradle 构建过程和阶段划分

每当 Gradle 开始构建时,它都会创建一个任务图用于执行构建操作。我们称这个过程为配置阶段 (configuration phase),它通常会持续几秒到数十秒。Gradle 配置缓存可以将配置阶段的输出进行缓存,并且在后续构建中复用这些缓存。当配置缓存命中,Gradle 会并行执行所有需要构建的任务。再加上依赖解析的结果也被缓存了,整个 Gradle 构建的过程变得更加快速。

这里需要说明,Gradle 配置缓存和构建缓存是不同的,后者缓存的是构建任务的产物。

△ Build 配置的输入内容

在构建过程中,您的构建设置决定了构建阶段的结果。所以配置缓存会将诸如 gradle.properties、构建文件等输入捕获,放入缓存中。这些内容同您请求构建的任务一起,唯一地确定了在构建中要执行的任务。

△ 配置缓存带来的性能提

上图展示包含 24 个子项目的 Gradle 构建示例,这组构建使用了最新版本的 Kotlin、Gradle 和 Android Gradle 插件。我们分别记录全量构建、有 ABI 变动和无 ABI 变动增量构建场景下启用配置缓存前后的对比。这里用添加新公有方法的方式进行增量构建,对应了 "有 ABI 变动" 的数据;用修改既有方法的实现来进行增量构建,对应了 "无 ABI 变动" 的数据。显而易见,所有三个构建场景都出现了 20% 的速度提升。

接下来,结合代码,一探配置缓存的工作原理:

project.tasks.register("mytask", MyTask).configure {  it.classes.from(project.configurations.getByName("compileClasspath"))  it.name.set(project.name)}

△ 配置缓存工作原理示例

在 Gradle 计算任务执行图之前,我们尚处于配置阶段。此时可以使用 Gradle 提供的 project、task 容器、configuration 容器等全局对象来创建包含声明的输入和输出的任务。如上代码中,我们注册了一个任务并进行相应配置。您可以在其中看到全局对象的多种用法,比如 project.tasks 和 project.configurations。

△ 存储配置缓存的过程

当所有任务都配置完成后,Gradle 可以根据我们的配置计算出最终的任务执行图。随后配置缓存会将这个任务执行图缓存起来,并将各个任务的执行状态进行序列化,再放入缓存中。从上图可以看到,所有的任务输入也会被存储到缓存中,因此它们必须是特定的 Gradle 类型,或是可以序列化的数据。

△ 加载配置缓存的过程

最终,当某个配置缓存被命中时,Gradle 会使用缓存条目来创建任务实例。所以只有先前已经被序列化的状态才会在新实例化的任务执行时被引用,这个阶段也不允许使用对全局状态的引用。

△ 新的 Build Analyzer 工具面板

我们在 Android Studio 的 Arctic Fox 版本添加了 Build Analyzer 工具来帮助您检查构建是否兼容配置缓存。当您的构建任务完成后,打开 Build Analyzer 面板,可以看到刚才构建配置过程花费的时间。如上图所示,配置构建过程总共使用了 9.8 秒。点击 Optimize this 链接,新面板中会显示更多信息,如下图所示:

△ Build Analyzer 提供的兼容性报告

如图,构建用到的所有插件都兼容配置缓存功能。点击 "Try Configuration cache in a build",IDE 会更新您的 gradle.properties 文件,在其中启用配置缓存。在不完全兼容的情况下,Build Analyzer 也可能会建议您将某些插件更新到与配置缓存兼容的新版本。如果您的构建与配置缓存不兼容,那么构建任务会失败,Build Analyzer 会提供相应的调试信息供您参考。

一个不兼容配置缓存的例子:

abstract class GetGitShaTask extends DefaultTask {  @OutputFile File getOutputFile() { return new File(project.buildDir, "sha.txt") }  @TaskAction void process() {    def stdout=new ByteArrayOutputStream()    project.exec {      it.commandLine("git", "rev-parse", "HEAD")      standardOutput=stdout    }    getOutputFile().write(stdout.toString())  }}project.tasks.register("myTask", GetGitShaTask)

我们有一个计算当前的 Git SHA 并将结果写入输出文件的任务。它会运行一个 git 命令,然后将输出内容写入给定文件中。我们在启用配置缓存的情况下执行这个构建任务,会出现两个与配置缓存相关的问题:

△ 配置缓存报告的内容

当您的构建任务与配置缓存不兼容时,Gradle 会生成一个包含了问题列表和详细信息的 HTML 文件。在我们的例子中,这个 HTML 文件会包含图中的内容:

△ 配置缓存错误报告

您可以从这些内容中找到各个出错点对应的堆栈跟踪信息。如示例中构建脚本的第 5 和第 11 行导致了这些问题。回看源文件,您会发现第一个问题是因为返回输出文件位置的函数中使用了 project.buildDir 方法;第二个问题是因为 TaskAction 中使用了 project 变量,这是由于启用配置缓存后,我们无法在运行时访问全局状态。

我们可以对上面的代码进行一些修改。为了在运行时调用 project.buildDir 方法,我们可以在任务属性中存储必要的信息,这样就可以一起被存入配置缓存中了。另外,我们可以使用 Gradle 服务注入来执行外部进程并获取输出信息。下面是修改后的代码供您参考:

abstract class GetGitShaTask extends DefaultTask {  @OutputFile abstract RegularFileProperty getOutputFile()  @javax.inject.Inject abstract ExecOperations getExecOperations()  @TaskAction void process() {    def stdout=new ByteArrayOutputStream()    getExecOperations().exec {      // ...    }    getOutputFile().get().asFile.write(stdout.toString())  }}project.tasks.register("myTask", GetGitShaTask) {  getOutputFile().set(    project.layout.buildDirectory.file("sha.txt")  )}

△ 使用 Gradle 服务注入来执行外部进程 (与配置缓存兼容的构建任务例子)

您可以从新代码发现,我们在任务注册期间,将输出文件的位置捕获并存入了某个属性中,然后通过注入的 Gradle 服务来执行 git 命令并获得命令的输出信息。这段代码还有另外一个好处,由于 Gradle 的延迟属性是实际使用时才计算的,所以 buildDirectory 发生的变动会自动反映在任务的输出文件位置上。

关于 Gradle 配置缓存和如何迁移您的构建任务的更多信息,请参阅:

Gradle 文档[7]深入探索 Android Gradle 插件的缓存配置[8]

扩展 Android Gradle 插件

不少开发者都发现在自己的构建任务中,有一些操作是无法通过 Android Gradle 插件直接实现的。所以接下来我们会着重探讨如何通过 AGP 新增的 Variant 和 Artifact API 来实现这些功能。

△ Android Gradle 插件的执行结构

build 类型 (buildTypes) 和产品变种 (productFlavors) 都是您项目的 build.gradle 文件中的概念。Android Gradle 插件会根据您的这些定义生成不同的变体对象,并对应各自的构建任务。这些构建任务的输出会被注册为与任务对应的工件 (artifact),并且根据需要被分为公有工件和私有工件。早期版本的 AGP API 允许您访问这些构建任务,但是这些 API 并不稳健,因为每个任务的具体实现细节是会发生改变的。Android Gradle 插件在 7.0 版本中引入了新的 API,让您可以访问到这些变体对象和一些中间工件。这样一来,开发者就可以在不操作构建任务的前提下改变构建行为。

修改构建时产生的工件

在这个部分,我们要通过修改 asset 的工件来向 APK 添加额外的 asset,代码如下:

// buildSrc/src/main/kotlin/AddAssetTask.ktabstract class AddAssetTask: DefaultTask() {  @get:Input  abstract val content: Property<String>   @get:OutputDirectory  abstract val outputDir: DirectoryProperty   @TaskAction  fun taskAction() {    File(outputDir.asFile.get(), "extra.txt").writeText(content.get())    }}

△ 向 APK 添加额外的 asset

上面的代码定义了一个名为 AddAssetTask 的任务,它只有一个字符串输入内容属性和一个输出目录属性 (DirectoryProperty 类型)。这个任务的作用是将输入字符串写入输出目录中的文件。随后我们需要在 ToyPlugin.kt 中编写一个插件,利用 Variant 和 Artifact API 来将 AddAssetTask 的实例连接到对应的工件:

// buildSrc/src/main/kotlin/ToyPlugin.ktabstract class ToyPlugin: Plugin<Project> {  override fun apply(project: Project) {    val androidComponents=project.extensions.getByType(AndroidComponentsExtension::class.java)     androidComponents.onVariants { variant ->      val taskProvider=project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {          it.content.set("foo")        }       // 核心部分      variant.artifacts        .use(taskProvider)        .wireWith(AddAssetTask::outputDir)        .toAppendTo(MultipleArtifact.ASSETS)    }  }}

△ 将 AddAssetTask 实例连接到对应的工件

上述代码中的核心部分会将任务的输出目录添加到 asset 目录的集合中,并正确连接任务依赖项。这段代码中我们将额外 asset 的内容硬编码为 "foo",但后面的步骤我们会对这里进行更改,还请您阅读时留意。

△ 可供开发者操作的中间工件举例

上图中展示了您可以访问到的几种中间工件,我们的 Toy 示例中就用到了其中的 ASSETS 工件。Android Gradle 插件为不同工件提供了额外的访问方式,比如当您想要校验某个工件的内容时,可以通过下面的代码来获得 AAR 工件:

androidComponents.onVariants { variant ->  val aar: RegularFileProperty=variant.artifacts.get(AAR)}

△ 获取 AAR 工件

请参阅 Android 开发者文档 Variant API、工件和任务[9] 获取关于 Android Gradle 插件新 Variants 和 Artifact API 的资料,这些资料可以帮助您更深入了解如何与中间工件进行交互。

修改和扩展 DSL

接下来我们需要修改 Android Gradle 插件的 DSL,从而允许我们设置额外 asset 的内容。新版本的 Android Gradle 插件允许您为自定义插件编写额外的 DSL 内容,所以我们会用这种方式来编辑每个构建类型的额外 asset。下面的代码展示了我们对模块的 build.gradle 文件的修改。

// app/build.gradle android {  ...  buildTypes {    release {      toy         content="Hello World"      }    }  }}

△ 在 build.gradle 中添加自定义 DSL

另外,为了能够扩展 Android Gradle 插件的 DSL,我们需要创建一个简单的接口。您可以参照下面一段代码:

// buildSrc/src/main/kotlin/ToyExtension.kt interface ToyExtension {  var content: String?}

△ 定义 toyExtension 接口

定义好接口之后,我们需要为每一个 build 类型添加新定义的扩展:

// buildSrc/src/main/kotlin/ToyPlugin.kt abstract class ToyPlugin: Plugin<Project> {  override fun apply(project: Project) {    val android=project.extensions.getByType(ApplicationExtension::class.java)     android.buildTypes.forEach {      it.extensions.add("toy", ToyExtension::class.java)    }    // ...  }}

△ 为所有 build 类型添加新定义的扩展

您也可以使用自定义接口扩展产品变种,不过在这个例子中我们不需要这样做。我们还需要对 ToyPlugin.kt 作进一步修改,让插件可以获取到我们在 DSL 中为每个变体定义的 asset 内容:

// buildSrc/src/main/kotlin/ToyPlugin.ktabstract class ToyPlugin: Plugin<Project> {  override fun apply(project: Project) {    // ...    // 注意这里省略了上一段代码增加的内容    val androidComponents=project.extensions.getByType(AndroidComponentsExtension::class.java)     androidComponents.onVariants { variant ->      val buildType=android.buildTypes.getByName(variant.buildType)      val toyExtension=buildType.extensions.findByName("toy") as? ToyExtension       val content=toyExtension?.content ?: "foo"      val taskProvider=project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {          it.content.set(content)        }       // 注意这里省略了修改工件的部分      // ...    }  }}

△ 在产品变体中使用自定义 DSL

上述代码中,我们增加了一段代码用于获取新增的 toyExtension 定义的内容,也就是刚才修改 DSL 时为每个 build 类型定义的额外 asset。需要您注意,我们这里定义了备选 asset 内容,也就是当您没有为某个 build 类型定义 asset 时,会默认使用的值。

使用 Variant API 添加自定义属性

您还可以用类似扩展 DSL 的方法来扩展 Variant API,具体来说就是向 Android Gradle 插件的 Variant 对象中添加您自己的 Gradle 属性或某种 Gradle Provider。相比仅扩展 DSL,扩展 Variant API 有这样一些优势:

DSL 值是固定的,但自定义变体属性可以使用构建任务的输出,Gradle 会自动处理所有构建任务的依赖项。您可以很方便地为每个变体的自定义变体属性设置独立的值。与自定义 DSL 相比,自定义变体属性能提供与其他插件之间更简单、稳健的交互。

当我们需要添加自定义变体属性时,首先要创建一个简单的接口:

// buildSrc/src/main/kotlin/ToyVariantExtension.kt interface ToyVariantExtension {  val content: Property<String>} // 比较之前的 ToyExtension (您不需要在代码中包括这部分) interface ToyExtension {  val content: String?}

△ 定义带有自定义变体属性的扩展 (对比普通扩展)

通过与先前的 ToyExtension 定义对比,您会注意到我们使用了 Property 而不是可空字符串类型。这样做是为了与 Android Gradle 插件内部的代码习惯保持一致,既能支持您将任务的输出作为自定义属性的值,又避免您再去考虑复杂的插件排序过程。其他插件也可以设置属性值,至于发生在 Toy 插件之前还是之后都没有影响。下面的代码展示了使用自定义属性的方式:

// app/build.gradleandroidComponents {  onVariants(    selector().all(),    { variant ->      variant.getExtension(ToyVariantExtension.class)        ?.content        ?.set("Hello ${variant.name}")    }  )}

△ 在 build.gradle 中使用带有自定义变体属性的扩展

虽然这样的写法没有直接扩展 DSL 那样简单,但它可以很方便地为每个变体设置自定义属性的值。相应的,还需要修改 ToyPlugin.kt 文件:

// buildSrc/src/main/kotlin/ToyPlugin.kt abstract class ToyPlugin: Plugin<Project> {  override fun apply(project: Project) {    // ...    // 注意这里省略了部分内容    val androidComponents=project.extensions.getByType(AndroidComponentsExtension::class.java)     androidComponents.beforeVariants { variantBuilder ->      val buildType=android.buildTypes.getByName(variantBuilder.buildType)      val toyExtension=buildType.extensions.findByName("toy") as? ToyExtension       val variantExtension=project.objects.newInstance(ToyVariantExtension::class.java)      variantExtension.content.set(toyExtension?.content ?: "foo")      variantBuilder.registerExtension(ToyVariantExtension::class.java, variantExtension)       // 注意这里省略了部分内容      // ...    }  }}

△ 注册带有自定义变体属性的 AGP 扩展

在这段代码里,我们创建了 ToyVariantExtension 的实例,首先用 toy DSL 中的值作为自定义变体属性对应的 Property 的默认值,随后将这个实例注册到变体对象上。您会发现我们使用了 beforeVariants 而不是 onVariants,这是由于变体扩展必须在 beforeVariants 块中注册,只有这样,onVariants 块中的其他插件才可以使用新注册的扩展。另外需要您注意,我们在 beforeVariants 块中获取了自定义 toy DSL 中的值,这个操作其实是安全的。因为当调用 beforeVariants 回调时,DSL 的值会被当作最终结果并锁定,也就不会产生额外的安全问题。获取到 toy DSL 中的值后,我们将它赋值给自定义变体属性,并最终在变体上注册新的扩展 (ToyVariantExtension)。

完成 beforeVariants 块的各项操作后,我们可以继续在 onVariants 块将自定义变体属性赋值给任务输入了。这个过程很简单,请参考下面的代码:

// buildSrc/src/main/kotlin/ToyPlugin.kt abstract class ToyPlugin: Plugin<Project> {  override fun apply(project: Project) {    // ...    // 注意这里省略了上一段展示内容     androidComponents.onVariants { variant ->      val content=variant.getExtension(VariantExtension::class.java)?.content      val taskProvider=project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {          it.content.set(content)        }       // 注意这里省略了修改工件的部分      // ...    }  }}

△ 使用自定义变体属性

上面这段代码很好地展示了使用自定义变体属性的优势,特别是当您有多个需要以变体专用的方式进行交互的插件时更是如此。如果其他插件也想设置您的自定义变体属性,或者将属性用于它们的构建任务,也只需要使用类似上述 onVariants 代码块的方式。

如果您想要了解更多关于扩展 Android Gradle 插件的内容,敬请关注我们的 Gradle 与 AGP 构建 API 系列文章[10]。您也可以阅读 Android 开发者 文档: 扩展 Android Gradle 插件[11] 或者研读 GitHub 上的 AGP Cookbook[12]。在不久的将来,我们还会推出更多构建和同步方面的改进,敬请关注。

下一步工作

Project Isolation

Gradle Project Isolation[13] 是基于配置缓存的一个新特性,旨在提供更快地构建和同步速度。每个项目的配置都是彼此隔离的,不允许跨项目的引用,于是 Gradle 可以缓存每个项目的同步 (sync) 结果,每当构建文件发生变化,只有受影响的项目会被重新配置。目前这个功能还在开发中,您可以在 gradle.properties 文件中添加 org.gradle.unsafe.isolated-projects=true 开关来尝试这个特性 (需要 Gradle 7.2 及以上版本) 。

改进 Kotlin 增量编译

我们还和 JetBrains 一起合作改进 Kotlin 的增量编译,目标是支持所有的增量编译场景,比如修改 Android 资源、添加外部依赖项或修改非 Kotlin 的上游子项目。

感谢所有开发者们的支持,感谢大家试用我们的预览版工具并提供问题反馈。请您持续关注我们的进展,也欢迎您遇到问题时与我们沟通。

欢迎您 点击这里[14] 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

参考资料

[1] 点击此处: https://www.bilibili.com/video/BV1Tq4y1e77K/

[2] Kotlin: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzk0NDIwMTExNw==&action=getalbum&album_id=1806104078662352898#wechat_redirect

[3] Android Studio Arctic Fox: https://zhuanlan.zhihu.com/p/398604810

[4] UP-TO-DATE: https://developer.android.google.cn/studio/releases/gradle-plugin#lint_tasks_can_now_be_up-to-date

[5] 复用其他构建的结果来减少新构建的时间: https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_intro

[6] Build Cache: https://docs.gradle.org/current/userguide/build_cache.html

[7] Gradle 文档: https://docs.gradle.org/current/userguide/configuration_cache.html

[8] 深入探索 Android Gradle 插件的缓存配置: https://zhuanlan.zhihu.com/p/281084168

[9] Variant API、工件和任务: https://developer.android.google.cn/studio/build/extend-agp#variant-api-artifacts-tasks

[10] Gradle 与 AGP 构建 API 系列文章: https://zhuanlan.zhihu.com/p/453892242

[11] 文档: 扩展 Android Gradle 插件: https://developer.android.google.cn/studio/build/extend-agp

[12] AGP Cookbook: https://github.com/android/gradle-recipes

[13] Project Isolation: https://gradle.github.io/configuration-cache/#project_isolation

[14] 点击这里: https://go2.gdsub.com/androiddevfb

发表评论