引言
Android 构建系统非常灵活,可让您在不修改应用核心源代码文件的情况下执行自定义构建配置。Google 基于IntelliJIdea 开发了Android Studio (当然如果不使用 Android Studio,可以通过命令行执行指令来构建和运行您的应用。 ),为了降低开发难度和开发成本,提供了Android 插件,同时采用了Gradle 构建系统来编译应用资源和源文件,并将它们打包成可快速测试、部署、签署和分发的 APK,得益于Gradle 强大的构建机制,除了默认的配置之外,还支持根据需求去自定义构建配置信息,其中每个构建配置均可自行定义一组代码和资源(即源集SourceSet),同时对所有应用版本公有部分的重复利用。以后更多精选系列文章以高度完整的系列形式发布在公众号,真正的形成一套完整的技术栈,欢迎关注,目前第一个系列是关于Android系统启动系列的文章,大概有二十几篇干货:
一、Android Studio vs Gradle vs Android 插件
Android Studio 是基于IntelliJIdea 开发的一个IDE,默认采用Gradle 构建系统来自动编译应用资源和源代码文件。而Android 插件则属于Google 专门为Android 开发定制的Gradle 插件(在Gradle脚本下的android
节点就是用于配置Android 这个Gradle插件的),负责完成Android 相关的编译任务。
二、多渠道定制前需要掌握的重要术语
1、 源集SourceSet
Android Studio 按照逻辑关系将每个模块的源代码文件和资源(包含assets、JNI、清单文件),Android Studio 默认就为我们创建了main 源集,main源集包含的源代码文件和资源,可供其他源集共享使用。源集还和构建变体有关系,在您配置新的构建变体时,除了可以使用main源集还可以自己创建对应构建变体的源集,创建源集的操作见Gradle脚本语法详解
在源集下创建XML资源文件:
首先同一 Project 窗格中,右键点击
src
目录,然后依次选择 New > XML > Values XML File,输入 XML 文件的名称或保留默认名称,再从 Target Source Set 旁边的下拉菜单中,选择对应的源集。
-
src/main/——main源集包括所有构建变体所公有的代码和资源,main源集的默认配置:
android { sourceSets { //如果需要改变main源集的默认信息,也可以在`sourceSets`下配置名为`main`的子节点配置 main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] //setRoot 'src/main/java' } }
如果你希望Gradle 只在你指定的新的路径下加载源代码文件,你可以
sourceSets { main { java { srcDirs = ['src/java'] } } }
如果你希望Gradle 同时到指定的新路径和默认路径下加载源代码文件,你可以
sourceSets { main { if (isModuleRun.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { //release 时 debug 目录下文件不需要合并到主工程 exclude '**/debug/**' } } } }
-
src/buildType/——构建类型源集,创建此格式源集可加入特定构建类型专用的代码和资源。如
src/debug/
,通过执行Android Plugin for Gradle 对应的 Gradle 任务,可以看到“debug”构建类型的包含的所有文件,其他源集也是如此。------------------------------------------------------------ Project :app ------------------------------------------------------------ ... debug ---- Compile configuration: compile build.gradle name: android.sourceSets.debug Java sources: [app/src/debug/java] Manifest file: app/src/debug/AndroidManifest.xml Android resources: [app/src/debug/res] Assets: [app/src/debug/assets] AIDL sources: [app/src/debug/aidl] RenderScript sources: [app/src/debug/rs] JNI sources: [app/src/debug/jni] JNI libraries: [app/src/debug/jniLibs] Java-style resources: [app/src/debug/resources]
-
src/productFlavor/——创建此格式源集可加入特定产品风格专用的代码和资源,如果配置构建以组合多个产品风格,则可为风格维度间产品风格的各个组合创建源集目录: src/productFlavor1ProductFlavor2/,比如
src/m1 或者src /mpt
,如果将多个产品变种和维度组合在一起,这些产品变种对应源集的优先级由它们所属的变种维度(
android.flavorDimensions
属性)决定。列出的第一个变种维度的产品变种的优先级高于属于第二个变种维度的产品变种,依此类推。此外,为产品变种组合创建的源集的优先级高于属于各个产品变种的源集。 -
src/productFlavorBuildType/——创建此格式源集可加入特定构建变体专用的代码和资源,比如
src/demoDebug/
。
注意:当您在 Android Studio 中使用 File > New 菜单选项新建文件或目录时,可以针对特定源集进行创建, 可供您选择的源集取决于您的构建配置,如果所需目录尚不存在,Android Studio 会自动创建,而且以上的buildType、productFlavor、productFlavor1ProductFlavor2、productFlavorBuildType皆不是意味着目录名称,只是表示目录名称的格式。
当如果不同源集均包含同一文件的不同版本,Gradle 在进行编译时将按以下从低到高优先级顺序:
库依赖项
–>主源集
–>产品风格
–>构建类型
–>**构建变体
**进行处理,不同类型的文件处理策略有所不同,但同一遵循这一优先级顺序。
1.1、构建时Gradle 将java/
目录中的所有源代码将编译输出
所以在不同源集下,不能同时存在两个全类名或全路径名一模一样的文件,因为对于指定的构建变体,当Gradle 遇到两个或多个源集目录下都定义了同一个 Java 类时就会抛出构建错误。例如,在构建调试 APK 时,不能同时定义 src/debug/Utility.java
和 src/main/Utility.java
。因为Gradle 在构建过程中会在检查这两个目录时抛出“重复类”错误。如果希望不同的构建类型有不同版本的 Utility.java
,可以让每个构建类型定义各自的文件版本并存放在不同的包下确保全类名不一致即可。
src/main/Util.java 和src/m1/Util.java 就是属于同一个Java类。
1.2、构建时 Gradle 将所有源集的清单按优先级合并到一个清单文件
每个源集只能指定一个或者不指定清单文件,**所有清单文件都将合并为一个清单处理,在合并多个源集下的同一清单的不同版本时,Gradle 将使用与源代码同一优先顺序从低合并到高优先级,先将库清单合并到主清单中,然后再将主清单合并到构建变体清单中,**每个构建变体都能在最终清单中定义不同的组件或权限。
在构建 APK 时,Gradle 会为库模块依赖项随附的资源和清单指定最低优先级。
1.2.1、构建变体的清单文件
如果变体有多个源代码集,则其清单优先级(从低到高)如下:
src/demo/
(产品变种清单)src/demoDebug/
(构建变体清单)src/debug/
(构建类型源代清单)
1.2.2、应用模块的主清单文件
1.2.3、所包含的库中的清单文件
如果您有多个库,则其清单优先级与依赖顺序(与库出现在 Gradle dependencies
代码块中的顺序)一致。
build.gradle
文件中的构建配置将替换合并后的清单文件中的所有对应属性。例如,build.gradle
文件中的minSdkVersion
将替换 `` 清单元素中的匹配属性。
1.3、构建时Gradle 将values
下的资源文件合并在一起
若两个文件同名,则按照上面的优先级顺序,采用高优先级替换低优先级的同一文件。即在构建类型源代码集的文件中定义的值会替换在产品变种的同一文件中定义的值,依此类推,下同。
1.4、构建时Gradle 将res
和asset
下的资源文件打包到一起
res/
和 asset/
目录中的资源会打包在一起。如果在两个或更多个源代码集中定义了同名的资源,将按照上面顺序指定优先级。
1.5、sourceSets节点配置源集属性
Android Gradle插件通过在android 子节点sourceSets
来详细配置源集,支持的属性有:
属性 | 说明 |
---|---|
res | 该源集的Android源目录。 |
aidl | Android AIDL源目录为此源设置。 |
assets | 该源集的Android Assets目录。 |
java | Java源代码由Java编译器编译到类输出目录中。 |
jni | 该源码集的Android JNI源目录。 |
jniLibs | 该源码集的Android JNI libs目录。 |
manifest | 该源代码集的Android Manifest文件 |
compileConfigurationName | 该源集合的编译配置的名称 |
name | 该源集的名称。 |
packageConfigurationName | 此源集合的运行时配置的名称。 |
providedConfigurationName | 此源集合的仅编译配置的名称。 |
renderscript | 该源码集的Android RenderScript源目录。 |
resources | 要复制到javaResources输出目录的Java源。 |
还可以直接在Gradle 脚**本下的
sourceSets
节点通过.
来引用属性对应成员变量。**如java.src。
2、构建类型BuildType
构建类型定义 Gradle 在构建和打包您的应用时使用的某些属性(Android Studio 默认在build.gradle脚本中buildTypes节点下会创建子节点debug调试和发布release构建类型),针对开发生命周期的不同阶段进行配置,例如调试构建类型支持调试选项,使用调试密钥签署 APK;而发布构建类型则可压缩、混淆 APK 以及使用发布密钥签署要分发的 APK,必须至少定义一个构建类型才能构建应用。
3、产品风格productFlavors
产品风格代表可以向用户发布的不同版本的应用(例如免费和付费版本的应用) 也可以将产品风格自定义为使用不同的代码和资源,同时对所有应用版本共有的部分加以共享和重复利用。 不过产品风格是不是必选项,但是如果需要就必须手动创建。
android {
...
flavorDimensions('type')
//定义渠道productFlavors
productFlavors {
m1 {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "m1"]
applicationIdSuffix '.m1'
}
mpt320 {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "mpt320"]
applicationIdSuffix '.mpt320'
}
}
productFlavors 必须配合至少一个flavorDimensions。productFlavors除了可以影响项目的源集,还可以影响到dependencies节点中的依赖分组,默认情况下系统只提供了几个依赖分组如implementation、api等,但是我们配置了productFlavors 之后,就会自动生成对应productFlavors 的依赖分组(格式形如:productFlavors+系统依赖分组 或者 buildType + 系统依赖分组 ,但不存在组合型的:productFlavors+ buildType + 系统依赖分组),作用和机制和源集大同小异。
4、构建变体Build Variant
构建变体是构建类型与产品风格的交叉产物,是 Gradle 在构建应用时使用的配置。 您可以利用构建变体在开发时构建调试版本的产品风格,或者构建要分发的已签署发布版产品风格。 您不是直接配置构建变体,而是配置组成变体的构建类型和产品风格。 创建附加构建类型或产品风格也会创建附加构建变体。
只有在构建变体之下,对应的源集才有效。
简单理解就是通过构建变体我们就可以灵活选择对应的源集合并到main源集中再进行编译打包。
三、Gradle 多渠道定制方案的实现
1、渠道定制代码存放到独立源集方式
1.1、在清单文件下定义meta-data 配置渠道
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MultVolume">
<!--CHANNEL~CHANNEL_VALUE-->
<meta-data
android:name="CHANNEL"
android:value="m1" />
</application>
如果是配置在application节点下就可以在代码中通过PackageManager 获取对应的meta-data的值:
public static String getChannel(@NonNull Context context,@NonNull String metaKey) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
//return appInfo.metaData.getString("CHANNEL");
return appInfo.metaData.getString(metaKey);
} catch (PackageManager.NameNotFoundException ignored) {
}
return "";
}
1.2、定义productFlavors 和 flavorDimensions
- 定义flavorDimensions 产品纬度
- 配置productFlavors
android{
...
flavorDimensions('type')
//定义渠道productFlavors
productFlavors {
m1 {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "m1"]
applicationIdSuffix '.m1'
}
mpt320 {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "mpt320"]
applicationIdSuffix '.mpt320'
}
}
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (names.contains("m1") && names.contains("type")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
}
简单来说productFlavors是用于配置自定义的Build Variant,通过Build Variant就可以差异化依赖编译不同的版本的apk。未配置productFlavors采用的就是默认的defaultConfig进行编译的结果。
1.3、配置productFlavors对应的源集
主要是在sourceSets
节点下以productFlavors 为名称的子节点下配置信息
如果需要配置多个路径,需要注意多个路径之间不能出现“父子”依赖关系。比如可以配置
res.srcDirs ='src/m1/res1'
也可以配置res.srcDirs = ['src/m1/res1/strings','src/m1/res1/layouts']
配置,但不能res.srcDirs = ['src/m1/res1', 'src/m1/res1/layouts', 'src/m1/res1/strings']
,针对这种情况应该只需要配置:src/m1/res1
或者 `` res.srcDirs = [‘src/m1/res1/strings’,‘src/m1/res1/layouts’]`。
java.srcDirs
设置Java 代码源文件的存放目录,main源集默认的java.srcDirs 为'src/main/java'
,如果需要配置多个路径可以java.srcDirs = ['other/java','other2/java']
。res.srcDirs
设置Android资源的存放目录,如果配置了多个路径,那么Gradle使用所有的这些文件夹来加载资源,并赋予这些文件夹相同的优先级。所以如果不同的文件夹中定义了相同的资源,那么会产生资源合并错误。manifest.srcFile
设置Android 清单文件,每个源集只能有一个AndroidManifest.xml清单文件,如果在其他源集中配置了清单文件,则Gradle 会把其他源集下的清单文件内容合并到main源集中。main源集默认的配置改变后,Gradle 就不会到默认的路径下加载对应的清单文件,而是仅仅到我们配置的路径下加载。include
或者exclude
去添加到源集或者从源集排除文件。
sourceSets {
//对应productFlavors的名称
m1{
java.srcDirs = ['src/m1/java']
manifest.srcFile 'm1/AndroidManifest.xml'
}
mpt320{
java.srcDirs = ['src/mpt320/java']
manifest.srcFile 'mpt/AndroidManifest.xml'
}
}
存放到新建独立的源集下,但只有在当前构建变体下main源集才能访问对应的源集下的代码。
1.3.1、创建独立的源集
先配置好productFlavors之后才能开始通过Project模式下的src目录->右键new->Folder创建独立的源集。
然后选择要创建源集
然后就在与main同集目录下看到对应的源集了。
1.3.2、配置独立的源集
android {
...
sourceSets {
m1{
java.srcDirs = ['src/m1/java']
manifest.srcFile 'm1/AndroidManifest.xml'
}
mpt320{
java.srcDirs = ['src/mpt320/java']
manifest.srcFile 'mpt320/AndroidManifest.xml'
}
}
}
1.4、配置不同productFlavors对应的dependencies依赖
android{
....
}
dependencies {
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
//仅在构建变体是m1 下才会依赖
m1Implementation project(path: ':libm1')
mpt320Implementation project(path:':libmpt')
api project(path: ':basepal')
}
1.5、选择对应的Gradle Task 执行编译
2、渠道定制代码统一存放到main源集,再进行差异化定制
在清单文件下定义meta-data 配置渠道后,再把渠道定制代码统一存放到main源集方式就不需要新建独立源集了,所有代码都在main源集下,最后再通过exclude和include进行差异化配置
def chanel='m1';//如果是mpt 渠道的话就改成mpt
android{
sourceSets{
//main 源集的默认目录路径是'src/main/java',如果不做任何修改的话,打包APK会把main源集下所有的代码都打包进去
main {
java {
if(chanel == 'm1') {
exclude 'com/crazymo/app2/mptonly/**'
///setIncludes(new HashSet(['com/crazymo/app2/m1only/*.java']))
}else if(chanel == 'mpt'){
//exlude 的话就会把com/crazymo/app2/m1only目录下的所有代码文件排除,** 代表会递归遍历所有子目录及孙目录等等。
exclude 'com/crazymo/app2/m1only/**'
///setIncludes(new HashSet(['com/crazymo/app2/mptonly/*.java']))
}
}
}
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:26.1.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
if(chanel=='m1'){
}else if(chanel == 'mpt'){
}
}
[TODO]原本我是打算把所有的定制代码都存放到main源集,然后再定义productFlavor 和对应的sourceSets,首先是把新建的sourceSets 冲定向至main 源集,再exlude 但是不成功,有谁知道是什么原因的吗?如下所示。
flavorDimensions('type')
//定义渠道productFlavors
productFlavors {
m1 {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "m1"]
applicationIdSuffix '.m1'
}
mpt {
dimension 'type'
manifestPlaceholders = [CHANNEL_VALUE: "mpt"]
applicationIdSuffix '.mpt320'
}
}
sourceSets{
m1{
java {
srcDirs = ['src/main/java']
exclude '**/m1only/**'
///setExcludes(new HashSet(['com/crazymo/app2/m1only/*.java']))
}
}
mpt {
java {
srcDir "../../java/src"
setIncludes(new HashSet(['com/crazymo/app2/mptonly/*.java']))
}
}
}
configurations.all {
resolutionStrategy.force 'com.android.support:support-annotations:26.1.0'
}
}