Android项目构建Gradle脚本解析

3,606 阅读17分钟

Android项目构建Gradle脚本解析

1.项目整体目录

build.gradle

一个Android项目首先在根目录有一个build.gradle脚本,这个脚本负责 项目中所有模块的构建配置 ,主要配置 项目中所有模块共用的 Gradle 代码库和依赖项 。

// 引入自定义的配置脚本
apply from: "version.gradle" 
// buildscript定义项目构建的仓库和依赖项
buildscript {
    	// 依赖仓库,包含本地仓库和远程仓库
        repositories {
            google()
            jcenter()
        }
    	// 定义构建整个项目需要依赖的gradle插件
        dependencies {
            classpath 'com.android.tools.build:gradle:3.6.0'
        }
    }
}
// 配置项目全局的依赖仓库
allprojects {
   repositories {
       google()
       jcenter()
   }
}
// 定义全局的额外属性,属性名称可以自己随意定义
ext {
    	// 在ext中可以定义变量属性
        compileSdkVersion = 28
        supportLibVersion = "28.0.0"
    	// 也可以定义map属性
		android = [
         	     compileSdkVersion       : 28,
            	 buildToolsVersion       : "28.0.3",
              	 minSdkVersion           : 19,
              	 targetSdkVersion        : 25
    	]
}

ext 可以定义在顶层配置文件中,也可以定义在自定义的xxx.gradle脚本中,只需要在顶层配置文件中使用apply from: "xxx.gradle"即可引入。要在子模块中使用我们定义的额外属性,可以通过如下两种方式

android {
    // 在方法调用中充当参数使用,可以直接使用变量名称
      compileSdkVersion rootProject.ext.compileSdkVersion
    }
dependencies {
    // 在字符串中使用可以通过,groovy语法中字符串插值的方式,
        implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
    }

settings.gradle

文件位于项目的根目录下,用于指示 Gradle 在构建应用时应将哪些模块包含在内。对大多数项目而言,该文件很简单,只包含以下内容:

include ':app',':module1'

local.properties

为构建系统配置本地环境属性,其中包括:

  • ndk.dir - NDK 的路径。此属性已被弃用。NDK 的所有下载版本都将安装在 Android SDK 目录下的 ndk 目录中。
  • sdk.dir - SDK 的路径。
  • cmake.dir - CMake 的路径。
  • ndk.symlinkdir - 在 Android Studio 3.5 及更高版本中,创建指向 NDK 的符号链接,该链接可比 NDK 安装路径短。

gradle.properties

可以在其中配置项目全局 Gradle 设置,如 Gradle 守护进程的最大堆大小。

android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m

2.APP整体配置

variant、type 、flavor

在讲解app 的模块的构建之前需要了解下variant、type 、flavor 这三个概念,因为这涉及到app的包结构:

flavor 是风味的意思,可用于区分不同渠道、区分免费版和收费版。

type 是类型的意思,区分在开发过程中不同阶段使用的包比如debug、测试、灰度、生产

variant 是变体的意思,以上两个维度相交就会有多种变体,比如收费版的debug包、免费版测试包,每种变体都可以添加一些特殊配置。

app模块 build.gradle

app的配置文件是在app目录下的build.gradle,主要为特定的模块配置构建设置,

	// 指定Android插件
    apply plugin: 'com.android.application'
	// Android插件配置
    android {
      // 编译版本
      compileSdkVersion 28
	  // 构建工具版本,这个版本需要比编译的版本高
      buildToolsVersion "29.0.2"
	  // 所有构建变体共有的默认配置,可以覆盖部分在main/AndroidManifest.xml中的配置属性。
      // 在渠道中配置的相同属性会覆盖此处的默认属性。
      defaultConfig {
        applicationId 'com.example.myapp'
        // Defines the minimum API level required to run the app.
        minSdkVersion 15
        // Specifies the API level used to test the app.
        targetSdkVersion 28
        // Defines the version number of your app.
        versionCode 1
        // Defines a user-friendly version name for your app.
        versionName "1.0"
      }
	  // 构建类型,默认有debug和release两种类型
      buildTypes {
        // 配置了代码的压缩和混淆
        release {
            minifyEnabled true // Enables code shrinking for the release build type.
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
      }
	  // 定义风味所有的维度,这里暂时只用一个维度,下面每种风味都必须定一个维度。
      // 如果有多个维度,底下的风味会根据维度产生组合
      // 比如:"abi","tier"  最终的风味会根据这两个维度产生多种组合方案
      flavorDimensions "tier"
      productFlavors {
        free {
          dimension "tier"
          applicationId 'com.example.myapp.free'
        }

        paid {
          dimension "tier"
          applicationId 'com.example.myapp.paid'
        }
      }
	  // 基于屏幕分辨率,打不同的apk
      splits {
        // Settings to build multiple APKs based on screen density.
        density {
          // Enable or disable building multiple APKs.
          enable false
          // Exclude these densities when building multiple APKs.
          exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
        }
      }
    }
/*********************************************************************************************/
	// 定义模块依赖
    dependencies {
        implementation project(":lib")
        implementation 'com.android.support:appcompat-v7:28.0.0'
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    }
    

3.依赖管理

上面APP的脚本中依赖管理部分是独立于Android插件的,

apply plugin: 'com.android.application'
android { ... }
dependencies {
    // 本地library依赖
    implementation project(":mylibrary")
    // 本地二进制jar包依赖
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 也可以具体指定文件相对路径名
    implementation files('libs/foo.jar', 'libs/bar.jar')
    // 远程依赖
    implementation 'com.example.android:app-magic:12.3'
    implementation group: 'com.example.android', name: 'app-magic', version: '12.3'
}

常见依赖配置

新配置 已弃用配置 行为
implementation compile Gradle 会将依赖项添加到编译类路径,并将依赖项打包到构建输出。不过,当您的模块配置 implementation 依赖项时,会让 Gradle 了解您不希望该模块在编译时将该依赖项泄露给其他模块。也就是说,其他模块只有在运行时才能使用该依赖项。使用此依赖项配置代替 apicompile(已弃用)可以显著缩短构建时间,因为这样可以减少构建系统需要重新编译的模块数。例如,如果 implementation 依赖项更改了其 API,Gradle 只会重新编译该依赖项以及直接依赖于它的模块。大多数应用和测试模块都应使用此配置。
api compile Gradle 会将依赖项添加到编译类路径和构建输出。当一个模块包含 api 依赖项时,会让 Gradle 了解该模块要以传递方式将该依赖项导出到其他模块,以便这些模块在运行时和编译时都可以使用该依赖项。此配置的行为类似于 compile(现已弃用),但使用它时应格外小心,只能对您需要以传递方式导出到其他上游消费者的依赖项使用它。这是因为,如果 api 依赖项更改了其外部 API,Gradle 会在编译时重新编译所有有权访问该依赖项的模块。因此,拥有大量的 api 依赖项会显著增加构建时间。除非要将依赖项的 API 公开给单独的模块,否则库模块应改用 implementation 依赖项。
compileOnly provided Gradle 只会将依赖项添加到编译类路径(也就是说,不会将其添加到构建输出)。如果您创建 Android 模块时在编译期间需要相应依赖项,但它在运行时可有可无,此配置会很有用。如果您使用此配置,那么您的库模块必须包含一个运行时条件,用于检查是否提供了相应依赖项,然后适当地改变该模块的行为,以使该模块在未提供相应依赖项的情况下仍可正常运行。这样做不会添加不重要的瞬时依赖项,因而有助于减小最终 APK 的大小。此配置的行为类似于 provided(现已弃用)。
runtimeOnly apk Gradle 只会将依赖项添加到构建输出,以便在运行时使用。也就是说,不会将其添加到编译类路径。此配置的行为类似于 apk(现已弃用)。
annotationProcessor compile 如需添加对作为注释处理器的库的依赖关系,您必须使用 annotationProcessor 配置将其添加到注释处理器类路径。这是因为,使用此配置可以将编译类路径与注释处理器类路径分开,从而提高构建性能。如果 Gradle 在编译类路径上找到注释处理器,则会禁用避免编译功能,这样会对构建时间产生负面影响(Gradle 5.0 及更高版本会忽略在编译类路径上找到的注释处理器)。如果 JAR 文件包含以下文件,则 Android Gradle 插件会假定依赖项是注释处理器: META-INF/services/javax.annotation.processing.Processor。如果插件检测到编译类路径上包含注释处理器,则会生成构建错误。

进行依赖配置时最常用的是是implementation和api,这两个配置最明显的差异在于子模块的第三方依赖在编译期间对父模块是否可见。

implementation

A->B->C

比如子模块B使用implementation依赖了一个库C,A模块在编译时就不能直接使用库C种的方法。这个时候C中方法变动,因为对A模块不可见,所以编译的时候只会重新编译B和C模块,少编译一个模块意味着项目构建速度会更快。

api

在编译阶段就会传递依赖,所以如果C模块改动了,A模块也会重新编译。

特定变体依赖

当针对某些特定编译类型,比如测试、debug,或者特定flavor需要依赖的依赖项,我们可以使用构建类型+Implementation或者flavor名称+Implementation,构建类型+Api或flavor名称+Implementation。

dependencies {
    // 特定编译类型+Implementation
    freeImplementation 'com.google.firebase:firebase-ads:9.8.0'
    // 构建类型+Implementation
    debugImplementation 'com.google.firebase:firebase-ads:9.8.0'
}

注意:如果要针对构建类型+flavor两个维度构成的组合添加依赖,就必须在 configurations 代码块中初始化配置名称。以下示例向“freeDebug”构建变体添加了 runtimeOnly 依赖项(使用本地二进制文件依赖项):

configurations {
    // Initializes a placeholder for the freeDebugRuntimeOnly dependency configuration.
    freeDebugRuntimeOnly {}
}
dependencies {
    freeDebugRuntimeOnly fileTree(dir: 'libs', include: ['*.jar'])
}

本地测试和插桩测试添加 implementation 依赖项,请使用如下所示的代码

dependencies {
    // Adds a remote binary dependency only for local tests.
    testImplementation 'junit:junit:4.12'
    // Adds a remote binary dependency only for the instrumented test APK.
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

注解处理器依赖,

dependencies {
    // Adds libraries defining annotations to only the compile classpath.
    compileOnly 'com.google.dagger:dagger:version-number'
    // Adds the annotation processor dependency to the annotation processor classpath.
    annotationProcessor 'com.google.dagger:dagger-compiler:version-number'
}

排除传递依赖

随着应用的范围不断扩大,它可能会包含许多依赖项,包括直接依赖项和传递依赖项(应用中导入的库所依赖的库)。

dependencies {
    implementation('some-library') {
    	// 排除指定模块
        exclude group: 'com.example.imgtools', module: 'native'
    }
}

4.默认配置

defaultConfig是AndroidgradlePlugin需要配置的第一部分,为所有的变体属性配置默认值,常见的配置项如下:

    android {
        defaultConfig {
            applicationId "com.hahah.www"
        	minSdkVersion 15
        	targetSdkVersion 28
        	versionCode 1
        	versionName "1.0"
        }
    }
、

看起来配置项都是key-value形式的,貌似这些配置项都是属性,但其实不然;这里minSdkVersion其实是方法。这里需要说明下groovy中方法调用的括号是可以省略的,minSdkVersion 15实际上是minSdkVersion(15)。DefaultConfig到底有哪些属性和方法呢,可以在官方文档DefaultConfig中查看。

属性表

Property Description
applicationId 应用id,其实就是包名,当和manifest.xml冲突时以这个为准
applicationIdSuffix 包名后缀,配置后会附加到包名后面,自己会添加小点
consumerProguardFiles 只用于library,配置当前library生成arr时使用的混淆规则,多个文件,分割
dimension 指定当前defaultConfig所属的风味维度,在全局配置中没有意义
externalNativeBuild 用来设置ndk配置过程中的一些参数
generatedDensities 已标记为废弃
javaCompileOptions 用于配置java编译过程中的一些参数
manifestPlaceholders 用于替换AndroidManifest.xml中的一些属性,一般在渠道配置中修改
multiDexEnabled 是否开启分包,方法索引为2个字节,开启分包后可超越65536
multiDexKeepFile 将指定的类打进主包classs.dex ,此处配置一个文件,内部是对应类列表
multiDexKeepProguard 效果同上,使用混淆规则决定哪些类在主包
ndk ndk配置,常用来指定打哪些架构的包
proguardFiles 混淆配置,一般不在通用设置中配置
signingConfig 构建签名,也一般不再此处配置
testApplicationId Test application ID.
testFunctionalTest See instrumentation.
testHandleProfiling See instrumentation.
testInstrumentationRunner Test instrumentation runner class name.
testInstrumentationRunnerArguments Test instrumentation runner custom arguments.
vectorDrawables 配置项项目中矢量图参数
versionCode integer 版本号
versionName String 版本名称
versionNameSuffix 版本名称后缀
wearAppUnbundled Returns whether to enable unbundling mode for embedded wear app. If true, this enables the app to transition from an embedded wear app to one distributed by the play store directly.

方法表

Method Description
buildConfigField(type, name, value) 向生成的BuildConfig添加字段 buildConfigField('String', 'name', '"zinc"') 需要配置类型、名称和对应值。
consumerProguardFile(proguardFile) Adds a proguard rule file to be included in the published AAR.
consumerProguardFiles(proguardFiles) Adds proguard rule files to be included in the published AAR.
maxSdkVersion(maxSdkVersion) 设置应用的最高支持版本,通常不用,除非你代码中用到的系统api在高版本中已经屏蔽。
minSdkVersion(minSdkVersion) 设置应用的最低支持版本,
missingDimensionStrategy(dimension, requestedValue) 忽略所使用的Library中的维度和风味,避免同步出错
missingDimensionStrategy(dimension, requestedValues) 第一个参数是维度,第二个参数是风味列表
proguardFile(proguardFile) 指定library的混淆文件
proguardFiles(files) Specifies ProGuard configuration files that the plugin should use.
resConfig(config) 保留的资源配置,比如国际化当中的资源
resValue(type, name, value) 向资源文件中动态添加值
setConsumerProguardFiles(proguardFileIterable) Specifies a proguard rule file to be included in the published AAR.
setProguardFiles(proguardFileIterable) Sets the ProGuard configuration files.
setTestProguardFiles(files) Specifies proguard rule files to be used when processing test code.
targetSdkVersion(targetSdkVersion) 应用的目标版本
testInstrumentationRunnerArgument(key, value) Adds a custom argument to the test instrumentation runner, e.g:
testInstrumentationRunnerArguments(args) Adds custom arguments to the test instrumentation runner, e.g:
testProguardFile(proguardFile) Adds a proguard rule file to be used when processing test code.
testProguardFiles(proguardFiles) Adds proguard rule files to be used when processing test code.

todo 逐一解析使用案例

5.构建配置

Android构建配置描述了构建的类型,比如debug、测试、灰度、生产等不同环境下要用的包。

    android {
        defaultConfig {
            manifestPlaceholders = [hostName:"www.example.com"]
            ...
        }
        buildTypes {
            release {
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }

            debug {
                applicationIdSuffix ".debug"
                debuggable true
            }
            /**
             * The `initWith` property allows you to copy configurations from other build types,
             * then configure only the settings you want to change. This one copies the debug build
             * type, and then changes the manifest placeholder and application ID.
             */
            staging {
                initWith debug
                manifestPlaceholders = [hostName:"internal.example.com"]
                applicationIdSuffix ".debugStaging"
            }
        }
    }
    

这个buildTypes构建块其实是一个BaseExtension中的一个成员变量,NamedDomainObjectContainer buildTypes,这里我们逐一列举下BuildType包含的属性和方法:

属性

Property Description
applicationIdSuffix 这个属性和默认配置中的一样,因为BuildType和DefaultConfig都继承BaseConfigImpl,
consumerProguardFiles 负责Library被编译时的混淆规则.
crunchPngs boolean类型是否对png进行压缩,debug默认关闭,release默认打开
debuggable boolean类型,决定当前包是否可被debug,即是否可以打断点调试。
embedMicroApp Whether a linked Android Wear app should be embedded in variant using this build type.
javaCompileOptions 编译java时指定的一些参数
jniDebuggable boolean类型,是否可以进行native调试
manifestPlaceholders 配置AndroidManifest.xml文件的属性,比如调整样式,调整图标等功能
matchingFallbacks 为当前构建类型指定依赖库的备选类型,一般debug应用要依赖debug类型的library,release要依赖release类型的library,如果library没有release构建类型,就会报错。此时我们为APP的release构建类型中使用该字段指定一个备选类型列表,release就可以使用debug版本的library了。
minifyEnabled 是否开启混淆,移除代码中没用到的代码
multiDexEnabled 是否开启分包
multiDexKeepFile 使用指定的文本记录要把哪些类打进主包
multiDexKeepProguard Text file with additional ProGuard rules to be used to determine which classes are compiled into the main dex file.
name
postprocessing incubatingThis DSL is incubating and subject to change.
proguardFiles 配置混淆规则文件,混淆会更改代码中的变量和类名,会导致反射失败,需要排除相关类。
pseudoLocalesEnabled Specifies whether the plugin should generate resources for pseudolocales.
renderscriptDebuggable 是否对renderscript进行调试
renderscriptOptimLevel 设置渲染脚本的等级
shrinkResources 是否压缩资源,需要开启混淆,会移除不用的资源文件
signingConfig 为构建类型配置签名.
testCoverageEnabled 是否生成测试覆盖率报告
useProguard 是否总是开启混淆
versionNameSuffix 版本名称后缀
zipAlignEnabled 是否开启zipAlign。会对应用程序进行字节对齐,对齐后会减少了运行应用程序时消耗的内存。

方法

Method Description
buildConfigField(type, name, value) 向BuildConfig中添加字段
consumerProguardFile(proguardFile) 为library设置混淆文件
consumerProguardFiles(proguardFiles) 为library设置多个混淆文件
externalNativeBuild(action) 配置ndk编译过程中的参数
initWith(that) 拷贝指定类型的BuildType,类似于继承
proguardFile(proguardFile) Adds a new ProGuard configuration file.
proguardFiles(files) Adds new ProGuard configuration files.
resValue(type, name, value) 添加资源类型
setProguardFiles(proguardFileIterable) Sets the ProGuard configuration files.

6.产品配置

构建配置是针对开发过程,针对不同阶段的不同需求进行差异化的配置。产品配置是站在用户角度,针对不同用户进行差异化的配置。

产品配置第一步是要定义风味维度,就是定义有哪些产品组。比如按照是否收费,这个维度上至少可以分成收费和免费两个产品。按照不同颜色又可以分为多个产品。flavorDimensions定义产品组名称。productFlavors定义具体每个产品类型,并在每个产品中使用dimension定义具体产品类型所属的产品组。

值得注意的是这里的维度不是孤立的,不同产品组其实是有交织的,比如上面的定义,最终会有收费红色,收费蓝色,免费红色,免费蓝色,四中产品出来。这里和维度定义的先后顺序有关。

    android {
        ...
        defaultConfig {...}
        buildTypes {
            debug{...}
            release{...}
        }
        // 定义产品组名称
        flavorDimensions "version","isFree"
        productFlavors {
            demo {
                // 定义该产品所属成员的产品组
                dimension "version"
                applicationIdSuffix ".demo"
                versionNameSuffix "-demo"
            }
            full {
                dimension "version"
                applicationIdSuffix ".full"
                versionNameSuffix "-full"
            }
        }
    }
    

productFlavors块中的每个对象都是一个ProductFlavor,由于DefaultConfig和ProductFlavor都继承BaseFlavor,所以在DefaultConfig中使用的配置,在ProductFlavor中基本上都可以使用。

完成了构建配置和产品配置后,同步代码会根据

[维度1产品1,维度1产品2,维度1产品3,]-[维度2产品1,维度2产品2]-[构建类型1,构建类型2]

生成3X2X2=12种构建变体,每个变体对应一个apk。

7. 代码配置

Android项目中代码和资源的位置按照一定的约定配置,但目录结构是可变的,我们按照不同的变体,配置不同的代码目录。我们可以通过./gradlew sourceSets来查看约定的目录结构配置。

todo 这里需要再整理下应用场景

    android {
      ...
      sourceSets {
        // Encapsulates configurations for the main source set.
        main {
          // Changes the directory for Java sources. The default directory is
          // 'src/main/java'.
          java.srcDirs = ['other/java']
          // If you list multiple directories, Gradle uses all of them to collect
          // sources. Because Gradle gives these directories equal priority, if
          // you define the same resource in more than one directory, you get an
          // error when merging resources. The default directory is 'src/main/res'.
          res.srcDirs = ['other/res1', 'other/res2']
          // Note: You should avoid specifying a directory which is a parent to one
          // or more other directories you specify. For example, avoid the following:
          // res.srcDirs = ['other/res1', 'other/res1/layouts', 'other/res1/strings']
          // You should specify either only the root 'other/res1' directory, or only the
          // nested 'other/res1/layouts' and 'other/res1/strings' directories.
          // For each source set, you can specify only one Android manifest.
          // By default, Android Studio creates a manifest for your main source
          // set in the src/main/ directory.
          manifest.srcFile 'other/AndroidManifest.xml'
        }

        // Create additional blocks to configure other source sets.
        androidTest {
          // If all the files for a source set are located under a single root
          // directory, you can specify that directory using the setRoot property.
          // When gathering sources for the source set, Gradle looks only in locations
          // relative to the root directory you specify. For example, after applying the
          // configuration below for the androidTest source set, Gradle looks for Java
          // sources only in the src/tests/java/ directory.
          setRoot 'src/tests'
          ...
        }
      }
    } 
Property Description
aidl The Android AIDL source directory for this source set.
assets The Android Assets directory for this source set.
compileConfigurationName deprecatedThe name of the compile configuration for this source set.
java The Java source which is to be compiled by the Java compiler into the class output directory.
jni The Android JNI source directory for this source set.
jniLibs The Android JNI libs directory for this source set.
manifest The Android Manifest file for this source set.
name The name of this source set.
packageConfigurationName deprecatedThe name of the runtime configuration for this source set.
providedConfigurationName deprecatedThe name of the compiled-only configuration for this source set.
renderscript The Android RenderScript source directory for this source set.
res The Android Resources directory for this source set.
resources The Java resources which are to be copied into the javaResources output directory.
Method Description
setRoot(path) Sets the root of the source sets to a given path. All entries of the source set are located under this root directory.

8. 签名配置

签名在gradle中的配置相对简单,只需要设置密钥库和私钥。

	android {
        defaultConfig {...}
        signingConfigs {
            release {
                storeFile file("myreleasekey.keystore")
                storePassword "password"
                keyAlias "MyReleaseKey"
                keyPassword "password"
            }
        }
        buildTypes {
            release {
                ...
                signingConfig signingConfigs.release
            }
        }
    }

上面的配置将密钥库密码和密钥密码都写到了脚本中,这样不安全。可以通过下面两种方式从系统配置中读取

 storePassword System.getenv("KSTOREPWD")
 keyPassword System.getenv("KEYPWD")

或者通过命令构建时直接从终端中输入

storePassword System.console().readLine("\nKeystore password: ")
keyPassword System.console().readLine("\nKey password: ")
Property Description
keyAlias 签名别名
keyPassword 签名的密码
storeFile 签名库文件
storePassword 签名库密码
storeType 签名类型,默认是’jks‘
v1SigningEnabled 是否使用v1签名方案,默认为true,Android7.0之前可以使用
v2SigningEnabled 是否使用v2签名方案,默认为true,签名后只能在7.0之后的系统上使用

参考文档:

docs.gradle.org/5.6/dsl/

google.github.io/android-gra…

developer.android.google.cn/studio/buil…

juejin.cn/user/182044…

构建类型官方文档

gradle插件版本