Gradle 构建(二)

4. 构建变体

其实上面多多少少也提到了,构建变体是构建类型与产品风味的交叉产物,是 Gradle 在构建应用时使用的配置。可以利用构建变体在开发时构建产品风味的调试版本,或者构建已签名的产品风味发布版本进行分发。我们并不直接配置构建变体,而是配置组成变体的构建类型和产品风味。创建附加构建类型或产品风味也会创建附加构建变体。

每个构建变体都代表您可以为应用构建的一个不同版本,例如,有时可能希望构建应用的免费版本和付费版本。同时还可以针对不同的设备、根据 API 级别或其他设备变体构建应用的不同版本。

注意,如果我们希望根据设备 ABI 或屏幕密度构建不同的版本,那么应该使用 APK 拆分(下面介绍)。

5. 源集

Android Studio 按逻辑关系将每个模块的源代码和资源分组为源集。模块的 main/ 源集包括其所有构建变体共用的代码和资源。其他源集目录为可选项,在配置新的构建变体时,Android Studio 不会自动创建这些目录。不过,创建类似于 main/ 的源集有助于让 Gradle 只应在构建特定应用版本时使用的文件和资源井然有序:

src/main/
 此源集包括所有构建变体共用的代码和资源。

src/<buildType>/
 创建此源集可加入特定构建类型专用的代码和资源。

src/<productFlavor>/
 创建此源集可加入特定产品风味专用的代码和资源。

src/<productFlavorBuildType>/
 创建此源集可加入特定构建变体专用的代码和资源。

比如,要生成应用的“freeDebug” 版本,构建系统需要合并来自以下源集的代码、设置和资源:

  • src/freeDebug/(构建变体源集)

  • src/debug/(构建类型源集)

  • src/free/(产品风味源集)

  • src/main/(主源集)

如果不同源集包含同一文件,Gradle 将按以下优先顺序决定使用哪一个文件(左侧源集替换右侧源集的文件和设置):

 构建变体 > 构建类型 > 产品风味 > 主源集 > 库依赖项

这个优先级顺序保证了既可以使用专用的文件,也可以复用通用文件。

(1)创建源集

默认情况下,Android Studio 会创建 main/ 源集和目录,用于存储我们要在所有构建变体之间共享的一切资源。当然,我们可以创建新的源集来控制 Gradle 要为特定的构建类型、产品风味(以及使用 风味维度 时的产品风味组合)和构建变体编译和打包的确切文件。

创建一个源集也比较简单,只需要和main/ 源集处于同一目录即可,比如要创建一个debug 构建类型的源集,可以在src 目录下创建debug 文件夹,形如 src/debug/ ,当然这个源集下面的组织结构也要和main/ 相同。

Android plugin有一个Task,可以向我们展示如何针对每种构建类型、产品风味和构建变体组织文件,Task 位置如下图


image.png

双击sourceSets,可以在Gradle Console 里生成报告,如图


image.png

我们可以根据报告来创建相关源集,如果需要的话。

(2)更改源集配置

如果一些源码或资源的文件路径和gradle 所期望的不一致,这时可以通过sourceSets{} 块,更改 Gradle 希望为源集的每个组件收集文件的位置,不需要重新定位文件;只需要为 Gradle 提供相对于模块级 build.gradle 文件的路径,Gradle 应当可以在此路径下为每个源集组件找到文件。

那么sourceSets 可以为哪些组件组织文件路径呢,如下所示

//跨进程通信声明文件(.aidl)
aidl

//assets文件下文件
assets

//.java文件目录
java

//.c, .cpp文件位置
jni 

//.so文件路径,注意该路径只需要指定到所包含平台的外层,不需要指定到具体的平台如`armeabi`,否则无法找到SO
jnilibs

//mainfest文件
manifest

//Android resource
res

//(java resource)
resource

//渲染脚本
rendersript

示例

sourceSets {
    // 设置了main 源集的配置
    main {
      // 默认是'src/main/java'
      java.srcDirs = ['other/java']

      // 这里如果列举了多个目录,gradle 会全部使用,所以如果两个目录有同名文件,
      // 就会报错sources,这个的默认目录是 'src/main/res'.
      res.srcDirs = ['other/res1', 'other/res2']

      // 注意,要避免目录有包含情况,比如:
      // res.srcDirs = ['other/res1', 'other/res1/layouts', 'other/res1/strings']
      // 应该要么使用根目录 'other/res1' ,要么使用两个子目录
      // 'other/res1/layouts' 和 'other/res1/strings'

      // 对于每个源集,只能指定一个manifest 文件路径,
      // 默认情况下会为main 源集创建一个manifest 文件,在src/main/ 目录下
      manifest.srcFile 'other/AndroidManifest.xml'
      ...
}
(3)使用源集构建

我们可以使用源集目录包含我们希望仅针对某些配置打包的代码和资源,当构建时,要使用的源集及其优先级上面已经说过了。

注:对于给定的构建变体,如果找到两个或两个以上定义同一 Java 类的源集目录,Gradle 就会引发一个构建错误。例如,在构建debug APK 时,不能同时定义 src/debug/Utility.java 和 src/main/Utility.java。这是因为 Gradle 会在构建过程中检查这两个目录并引发“duplicate class”错误。如果针对不同的构建类型需要不同版本的 Utility.java,可以让每个构建类型定义其自己的文件版本,而不将其包含在 main/ 源集中。

不仅Java 代码,values、res、assets 等目录的文件也会合并打包,如果有同名文件,采用的顺序和上面说的优先级顺序一致。

同时,所有的manifest 文件也会合并,因为一个APK 中只能包含一个AndroidManifest.xml 文件。关于manifest 的合并还有一套规则。

6. 清单条目

我们可以为构建变体配置中manifest 文件的一些属性指定值。这些构建值会替换清单文件中的现有值。如果想为应用生成多个 APK,让每一个 APK 文件都具有不同的应用名称、最低 SDK 版本或目标 SDK 版本,便可运用这一技巧。当存在多个清单时,Gradle 会对manifest 进行合并,合并的规则可以参考这篇 合并manifest 文件

7. 过滤变体

之前我们介绍了很多怎样构建不同种类的应用,同时我们也看到,当我们定义了构建类型、产品风味等之后,gradle 会组合出很多的构建变体,有的时候这些构建变体我们并不是都需要,这个时候就可以对构建变体进行筛选过滤。

我们可以在模块级gradle 文件的android{} 块下通过variantFilter{} 代码块来进行过滤,例如

// 配置过滤,接收一个lambda 表达式
variantFilter { variant ->
    // variant.flavors 获取所有的flavor,是一个list,这里有一个*,涉及到groovy 语法
    def flavorNames = variant.flavors*.name
    // 获取buildType 的名称
    def typeNames = variant.buildType.name

    // 设定相关条件
    if (flavorNames.contains("what") && flavorNames.contains("full")) {
        // 如果要过滤掉的话,调用这个函数,并传true
        setIgnore(true)
    }

    if (typeNames == "myType") {
        setIgnore(true)
    }
}

在向构建配置添加变体过滤器并点击通知栏中的 Sync Now 后,Gradle 将忽略满足指定的条件的任何构建变体,在点击菜单栏中的 Build > Select Build Variant(或工具窗口栏中的 Build Variants)时,这些构建变体将不会再显示在下拉菜单中。

8. 依赖项

我们编写一个项目,有时看到前人造好的华丽的轮子想要使用,或者自己在其他项目写的一个好的工具想用到本项目,这时要想使用这些库的内容,就要对他们进行依赖的配置。我们可以通过gradle 来管理项目依赖,这样,我们就不必手动搜索、下载依赖项的二进制文件包以及将它们复制到项目目录内。

我们可以在模块级别的build.gradle 中dependencies{} 块中声明依赖,一共可以声明三种类型的直接依赖项

dependencies {
    // 'compile' 属性告诉gradle 将依赖库添加到编译路径,并包含在最终的包内

    // Dependency on the "mylibrary" module from this project
    compile project(":mylibrary")

    // Remote binary dependency
    compile 'com.android.support:appcompat-v7:27.0.2'

    // Local binary dependency
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

模块依赖项
  compile project(':mylibrary') 行声明了一个名为“mylibrary”的本地 Android 库模块作为依赖项,并要求构建系统在构建应用时编译并包含该本地模块。

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

本地二进制依赖项
  compile fileTree(dir: 'libs', include: ['*.jar']) 行告诉构建系统在编译类路径和最终的应用软件包中包含 app/libs/ 目录内的任何 JAR 文件。如果模块需要本地二进制依赖项,那么需要将这些依赖项的 JAR 文件复制到项目内部的 <moduleName>/libs 中。

模块配置的这些依赖项,有些可能会有自己的依赖项,这称为模块的传递依赖项。Gradle 将会自动为您收集并添加这些传递依赖项,而不必手动逐一加以声明。Android plugin 包含一个有用的Task,可为每个构建变体和测试源集生成依赖项树,这样我们就可以轻松地可视化模块的直接和传递依赖项。这个Task 名叫androidDependencies,位置如图


image.png

双击,可在gradle console 里生成依赖树的报告


image.png
(1)配置依赖项

我们可以通过指定的关键字来告诉gradle 什么时候、怎样使用某个依赖项,比如刚才提到的compile 关键字,构建系统提供了三种关键字:

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

不过,在gradle 3.0 及以上 implementation, api, compileOnly, 和 runtimeOnly,其中implementation 和 api 对应3.0以下的compile,compileOnly 对应provided,runtimeOnly 对应apk。使用这些关键字会比3.0 以下的关键字效率更高一些,具体的区别,可以参考 升级gradle 到3.0

此外,我们可以通过将构建变体或测试源集的名称应用于配置关键字,为特定的 构建变体 或 测试源集 配置依赖项,如下例

dependencies {
    // BuildTypes为debug的添加含有源码的模块依赖
    debugCompile project(":mylibrary")

    // 正常的依赖
    compile 'com.android.support:appcompat-v7:25.1.0'

    // 我们可以直接指定依赖项的构建类型,如下

    // relase构建时指定依赖的`library`也是release
    releaseCompile project(path: ':library', configuration: 'release')
    // debug构建时指定依赖的`library`的构建也是`debug`
    debugCompile project(path: ':library', configuration: 'debug')
    ...
}

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

flavorTypeCompile + typeCompile + flavorCompile + compile

(2)解决依赖冲突

刚才我们也提到了,一个模块除了有直接依赖项外,还有传递依赖项,通过androidDependencies 这个Task 可以查看依赖树,针对多种依赖我们以下关键字,即transitive、force 和exclude,来完成对多依赖项的操作,下面一一介绍他们的功能。

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

configurations.all {
   // 为所有的配置指定自动添加子依赖项为false
   transitive = false
}

dependencies {
   // 为单独的某个依赖项指定字典添加传递依赖项为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
排除依赖项中的某些传递依赖项,这在解决依赖库版本冲突或者重复时特别有用,一般情况下,我们可以用以下两种方式来完成排除。

  • group,maven 项目的GroupId,GroupID是项目组织唯一的标识符,对于小型的项目,常常对应JAVA的包的结构,是main目录里java的目录结构,但是也可以很多个项目共用一个GroupID,如com.android.support下就有很多个子项目。
  • module, maven项目的ArtifactID, ArtifactID 就是具体某个项目的唯一的标识符,实际常常对应项目的名称,就是项目根目录的名称,如support-annotations。

完整的声明一个依赖项其实是这样:

compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.7.Final'

但是我们通常简写为:

compile 'org.hibernate:hibernate-core:3.6.7.Final '

简写的格式为 compile 'group:name:version' ,所以我们也就知道了group 是哪部分,module 是哪部分。

group和module可以配合一起使用也可以单独使用。

//移除所有依赖项中,组织为`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'
})

// 移除所有依赖项中名为`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'
})

// 上面是单独使用针对group 和module 进行排除,两者还可以结合在一起使用,如下

// 移除所有依赖项中,组织为`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'
})

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

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

上面espresso:espresso-core依赖runner不同与上面我们上面指定的版本的runner,此时gradle会自动同步最新版本。对于不同的配置下,出现的依赖项不一样,gradle就会直接报错,这时我们就可以使用上面提到的force, exclude来解决。

(3)依赖仓库

上面介绍了三种依赖类型,最常用的就是依赖远程的库,那么gradle 从哪获取到依赖的代码呢,就是从远程仓库,即repositories{} 块中定义的仓库。一般常用的远程仓库有以下几种:

repositories {
    mavenCentral()      // Maven center 仓库

    jcenter()           // jcenter 仓库

    maven {             // 远程自定义maven 仓库
        url "http://repo.mycompany.com/maven2"
    }

    ivy {               // ivy 仓库
        url "http://repo.mycompany.com/repo"
    }

    ivy {               // ivy 本地仓库,URL 可以指向本地目录
        url "../local-repo"
    }
}

我们可以声明多个仓库,声明的顺序就是gradle 检索依赖项的顺序,gradle 一旦在某个仓库检索到依赖项就会下载文件。

9. 签名配置

构建系统让我们能够在构建配置中指定签名设置,并可在构建过程中自动签名 APK。构建系统通过使用已知凭据的默认密钥和证书签署调试版本,以避免在构建时提示密码。我们可以使用signingConfigs 创建多个签名配置。

android{
    signingConfigs {
        // release 包签名配置
        release {
            // 签名密钥库文件的存放位置,此处为相对路径
            storeFile file("myreleasekey.keystore")
            // 密钥库的访问密码
            storePassword "password"
            // 别名,因为一个密码库可以供多个项目使用,所以别名不同,最后的签名也是不同的。
            keyAlias "MyReleaseKey"
            // 别名的私钥密码
            keyPassword "password"
        }
    }
}

将发布密钥和密钥库的密码放在build.gradle 文件中并不安全,所以我们可以将此构建文件配置为通过环境变量获取这些密码,或让构建流程提示输入这些密码。

// 通过环境变量获得密码
storePassword System.getenv("KSTOREPWD")
keyPassword System.getenv("KEYPWD")

// 让构建流程在您要从命令行调用此构建时提示输入这些密码:
storePassword System.console().readLine("\nKeystore password: ")
keyPassword System.console().readLine("\nKey password: ")

此外,我们还可以从指定的文件中获取密码,比如我们在根目录下创建一个password.properties 文件,比如内容如下

//release
releaseStoreFile=sign/platform.keystore
releaseStorePassword=123456
releaseKeyAlias=androidreleasekey
releaseKeyPassword=123456

然后在build.gradle 文件中解析它

def keystorePropertiesFile = rootProject.file("keystore.properties")

// 初始化一个Properties 实例
def keystoreProperties = new Properties()

// 使用Properties 实例加载 keystore.properties 文件
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android{
    signingConfigs {
        release{
            keyAlias keystoreProperties['releaseKeyAlias']
            keyPassword keystoreProperties['releaseKeyPassword']
            storeFile file(keystoreProperties['releaseStoreFile'])
            storePassword keystoreProperties['releaseStorePassword']
        }
    }
}

在声明了不同签名配置之后,怎么使用这个配置呢,还记得前面的 构建类型产品风味 吗,里面有个signingConfig 属性,所以只要给这个属性赋值就可以了。

android {
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release    // 使用之前定义的签名配置
        }
    }
}

关于为不同类型定义不同签名配置就介绍完了,至于如何签名发布一个应用,可以参考这篇 签名应用

10. apk 拆分

有时我们可能想让apk 只包含针对特定屏幕密度 和 CPU指令集的代码,这时我们可以对apk 进行拆分。使用splits{} 块进行apk 的拆分配置,splits{} 块在android{} 块之下。目前支持对三种情况进行拆分,分别是ABI、density 和language 。

android {
    splits {
        // 根据不同的abi 进行拆分
        abi {
            // 设置为true,gradle 将根据我们配置的ABI 来产生多个apk,默认值是false
            enable = true

            // 清空默认的ABI 列表,只有在使用include 属性时,才使用reset
            reset()

            // 下例只为ABI 为x86 和mips 产生apk,和reset 联合使用,产生明确的ABI 列表
            include "x86", "mips"

            // 为不包含某些ABI 之外的配置产生apk,比如下例表示为除了x86之外的所有abi 生成apk
            // exclude "x86"

            // 表示是否生成适用所有ABI 的apk,默认是false,这个属性值在abi{}块内有效,在density{} 块默认是生成支持所有屏幕密度的apk
            universalApk = true
        }
        // 根据配置的屏幕密度拆分apk
        density {
            // 下面这四个属性,作用和abi{} 中的一样
            // include/exclude 可选的值是["ldpi" | "mdpi" | "hdpi" | "xhdpi" | "280" | "360" | "420" | "480" | "560" ]
            enable = true
            exclude "ldpi", "mdpi"
            reset()
            include "hdpi", "xhdpi"

            // 这个值会改变manifest 文件中 <compatible-screens> 节点的值,作用是为不同屏幕大小(注意上面说的是密度,这个是大小)产生适配的apk
            // 取值 'small', 'normal', 'large', 'xlarge'
            compatibleScreens "small", "normal"
        }

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

推荐阅读更多精彩内容