Android Studio 自带了一个功能,叫做 Product Flavor,它可以让开发者更加便捷地切换代码源
集,不用再像之前一样做多渠道开发需要通过注释来实现,现在只需要在AS 界面上点击一下即可切换源代码集合。
一、Build Variants
新建工程后,点击 AS 左下角的 Build Variants(顾名思义,这个功能是用来设置编译变量以切换代码集合) 菜单按钮,在弹出的工具栏中点击右上角的小箭头,可以看到我们目前的编译可选变量为debug
和release
,且默认选中debug
变量,因为现在我们的app module
处于开发环境。
这两个编译变量是 AS 默认提供且无法删除因为在 AS 自带的关于编译的部分功能需要用到这两个值,比如Build部分相关指令。这两个变量又称为Build Type
,我们可以通过操作 AS Build -> Edit Build Types
查看当前 module 有哪些Build Type
值(默认就只有debug
和release
且无法删除),我们还可以自己增加新的编译类型,如图二所示
当然,我们也可以直接在 app 的 build.gradle
中新增新的编译类型,格式如下,在这里我们可以把buildTypes
下面的release
和debug
删除,但AS 还是会默认提供的,我们可以通过上一步操作进入edit BuildTypes
界面,会发现依然看得到这两个编译类型和新增的编译类型。
android {
defaultConfig {}
buildTypes {
release {}
//新增的编译类型
define {
debuggable false
minifyEnabled false
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
现在我们重新打开 AS 左下角的 Build Variants
界面,可以看到右上角的Build Variant
下拉菜单多出一个define
选项,我们在开发中就是通过切换Build Variant
的值来实现多渠道包的开发或者是测试环境和正式环境的切换。当然我们现在切换编译类型是看不出效果的,我们还需要进一步的配置。
二、Product Flavor
Product Flavor
其实和Build Type
很是类似。我们可以通过操作 AS 的Build -> Edit Flavor
查看app
对应的flavor
,界面如图四
通过对比图二和图四,我们发现两者大部分属性是比较相近的,但是Product Flavor
默认支持自定义完整的Application Id
和Target SDK Version
,而且从命名的角度来看,这个功能更多地是实现多样化开发,即多语言、多地区等多渠道开发。我们可以再图四界面上点击左下角的+
按钮添加新的 Product Flavor
,结果如图五
在AS 3.0之后,要求productFlavors
必须有flavorDimension
属性,否则编译器会报错,通过上步操作后我们打开app
对应的build.gradle
,可以看到在android
目录下多了一个productFlavors
代码块
android {
compileSdkVersion 26
defaultConfig {}
buildTypes {}
productFlavors {
china {
}
usa {
}
}
}
由于我们没有给productFlavors
设置flavorDimensions
属性,所以编译出错,接下来我们补充完整的属性, sync 之后没有报错
flavorDimensions "country"
productFlavors {
china {
dimension "country"
}
usa {
dimension "country"
}
}
通过这样的设置之后我们就可以在Build Variants
界面中切换不同的Build Variant
来达到切换不同的开发环境这一个目的,现在我们可以看到Build Variant
下拉列表中有6个值可以选择,如图六
之所以有6个值,是因为Gradle使用以下命名方案组合创建总共 6 个构建变体:
构建变体:[china, usa][Debug, Release, Define]
对应 APK:app-[china, usa]-[debug, release, Define].apk
我们可以通过以下步骤测试一下
- 选择
Build Variant
为chinaRelease
- 点击 AS 顶部菜单栏
Build -> Clean Project
//清除之前可能产生的脏文件 - 点击 AS 顶部菜单栏
Build -> Build APK(s)
切换工程的查看模式为Project
,在app--build--outputs--apk
目录下我们可以看到有一个china
文件夹,里面有一个release
子文件夹,里面有一个apk
,名字为app-china-release-unsigned.apk
,是不是和我们上面说的构造变体生成的规则是很相似的?只不过多了一个-unsigned
部分,因为我们没有选择Build -> Generated Signed APK
操作。
所以我们就可以通过上面的操作来实现发布6个渠道包,分别是china
和 usa
对应的 release
、 debug
、 define
渠道包,只需要切换Build Variant
即可,是不是很简单?
不过我们目前build
出来的 apk在功能上却是没有区别的,因为执行的代码都一样,这一点后面说,现在继续补充Product Falovr
的内部组合用法。
其实flavorDimensions
是list
类型,我们可以指定多个值,现在我们为其增加一个food
类型,并且为productFlavors
增加两个声明为food
的字段,代码如下
flavorDimensions "country", "food"
productFlavors {
china {
dimension "country"
}
usa {
dimension "country"
}
fruits {
dimension "food"
}
preservedegg { //皮蛋
dimension "food"
}
}
现在我们看到Build Variant
有12个编译类型,其实完整的构建变体方案如下
构建变体:[china, usa][fruits, preservedegg][Debug, Release, Define] //组合2 * 2 * 3 = 12
对应 APK:app-[china, usa]-[fruits, preservedegg]-[debug, release, define].apk
flavorDimensions
列表的属性是有优先级的,越先写的值优先级越高,表现在构件变体的时候命名的位置越前,所以构件的变体中,country
类型的字段永远排在food
类型前面。
我们也可以排除某些Product Flavor
的组合,以美国人不吃皮蛋为例,我们把美国皮蛋这一变体去掉,代码如下
variantFilter { variant ->
def names = variant.flavors*.name
//如果要判断是否包含某个 build type, 使用 variant.buildType.name == ""
if (names.contains("usa") && names.contains("preservedegg")) {
// Gradle 不会生成包含以上条件的变体
setIgnore(true)
}
}
现在我们再次查看Build Variants
,可以看到只剩下9个Build Variant
了,文章到这里,关于ProductFlavors
的相关使用就介绍完毕,接下去我们就需要写点代码来体现不同渠道包功能的差异了。
三、Source Set
当使用 AS 创建一个 Android
,工程的时候,app
源码是存放在app/src/main/java
目录下的,当我们创建了编译变体之后,以chinaFruitsDebug
为例,其源码的位置默认是app/src/chinaFruitsDebug/java
(我们可以通过点击 AS 右上角的 Gradle
菜单按钮,执行:app--Tasks--android--sourceSets
,然后点击AS 右下角的Gradle Console
查看chinaFruitsDebug
对应的Java sources
的值)。
以chinaFruitsDebug
编译变体为例,在未设置 sourceSets
的前提下,编译器寻找代码的顺序如下
1. 首先去`app/src/chinaFruitsDebug/java/`目录下寻找,如果找不到则往下走
2. 去`app/src/{case}/java/`目录下找,如果找不到则往下走,
3. 去`app/src/main/java`目录下找,如果找不到则提示找不到目标的错误
其中{case}就复杂了,这个组合关系我有点难以描述,以`chinaFruitsDebug`为例,case 可以是,china、fruits、debug、chinaFruits、chinaFruitsDebug。[flavor]的两个属性可以单独存在或者组合在一起,debug也可以单独存在,但是组合的话只能和完整的 flavor 组合,不能出现类似 chinaDebug之类的。
幸好我们可以选择不去记忆这种规则,在`Project`目录结构下,我们可以看到`src/china|fruits|chinaFruits/`目录下面的`java`文件夹的颜色为蓝色!如果我们这个时候新建一个`src/usa/java`的源集,可以看到其`java`文件夹为灰色,蓝色就表示在当前编译变体的环境下,编译器会去这些源集中寻找目标代码
以chinaFruitsDebug为例,我们创建app/src/chinaFruitsDebug/java
文件夹
切换工程目录格式为
Project
-
在
app--src
文件夹右键选择创建Java Folder
,如图七
-
在弹出的创建文件夹界面中进行如下操作,记得文件路径写
src/chinaFruitsDebug/java
,如图八所示,这里简单说明一下,通过勾选Change Folder Location
我们可以修改文件夹路径,文本输入框就是输入我们想要存放的路径,这里以编译变体名命名,方便理解和查看。至于第三个Target Source Set
,它的作用是指定编译变体对应的源代码目录,其结果就是在app
的build.gradle
中的android
代码块中生成一个sourceSets
代码块,这点待会介绍
在
src/chinaFruitsDebug/java/
目录下我们新建一个包,包名和app module
的包名保持一致(方便无缝切换),我的包名是com.mango.multiproduct
,在这个包下面创建一个类,代码如下
public class Descrption {
public static String desc() {
return "chinaFruitsDebug";
}
}
- 打开
Build Variants
界面,将app
对应的Build Variant
选择为chinaFruitsDebug
- 新建一个
test
目录下的单元测试类(可以看我前面写的单元测试相关文章),代码如下
public class DescTest {
@Test
public void onCreate() throws Exception {
System.out.println("fetch info:" + Descrption.desc());
}
}
执行单元测试,最后可以在控制台看到以下输入信息,表示我们在切换Build Variant
之后成功找到了目标代码
fetch info:chinaFruitsDebug
-
打开
Build Variants
界面,将app
对应的Build Variant
选择为chinaFruitsRelease
,这个时候我们就直接看到Descrption
变成红色字体了,如图九所示。
为了验证我上面所说的在
/src/chinaFruitsRelease/java
找不到Descrption
类的时候编译器最终会去/src/main/java/
目录下找(其它 {case}目录优先main
目录,这点大家可以去试试),我们把Descrption.java
复制到目标位置,并且让desc
方法返回return "MainDescrption";
,这个时候可以看到单元测试没有提示找不到Descrption
类了,这个时候我们Clean Project后再执行一次单元测试,可以看到控制台输出我们想要的信息了
fetch info:MainDescrption
回到本节的正题,我们查看app
的build.gradle
文件,可以看到多出了sourceSets
代码块,如下
android {
compileSdkVersion 26
defaultConfig {}
buildTypes {}
flavorDimensions "country", "food"
productFlavors {}
variantFilter {}
sourceSets {
chinaFruitsDebug {
java.srcDirs = ['src/chinaFruitsDebug/java', 'src/chinaFruitsDebug/java/']
}
}
}
sourceSets
中的字段就是Build Variant
值了,意思就是某个编译变体所对应的源集(源代码集合)位置,可以是java
代码,也可以是jni
源码、也可以是资源文件架。当然我们把上面的sourceSets
删除也是没关系的,因为我们给chinaFruitsDebug
设置的关联目录就是编译器默认去寻找的目录。来试一下,我们直接把sourceSets
代码块删除,然后打开Build Variants
界面,切换Build Varinant
为chinaFruitsDebug
,这个时候Descption.java
报错了,提示重复定义类了,说明编译器确实是去src/chinaFruitsDebug/java/
目录下找到了Descrption.java
,这个时候我们把main/java
目录下重复的类删除,然后clean一下工程(否则可能单元测试不过),然后执行单元测试便可以看到如上第6个步奏输出的结果。
sourceSets
已经解释完毕,需不需要用到,就看我们在创建编译变体源集的时候的路径是不是 AS 默认的路径了,一旦我们为某个编译变体明确指定了源集,那么编译器第一次尝试寻找的位置就变成我们指定的位置,如果找不到的话就继续往上层去寻找。
四、蓝色、绿色和灰色?
不同的源集在不同的编译变体环境中的java
文件夹的颜色是不同的,它们分别代表什么呢?
如图十所示,我们可以看到这几种颜色的java
文件夹
灰色我们已经说过了,灰色表示该源集在当前编译变体的环境下,编译器不会去该源集中寻找目标代码。
蓝色则表示表示该源集在当前编译变体的环境下,编译器会去该源集中寻找目标代码。
那么绿色呢?绿色表示的不是源集,而是测试集合,意味着这些代码是不能上传到服务器的。而如果是绿色并且右下角有一个android
机器人小图标,则表示该文件夹的单元测试是运行在Android
运行环境的
那么为什么编译器知道哪些是源集哪些是测试集合?因为它自己制定了一个命名规则:
那就是[test/Android + B + C],其中 B 是 Flavor 中的任一个值,允许为空;C 是 BuildType 中的任一个值,允许为空
我们进行一步操作来验证我们猜想的规则,这里可以跳过不看,看看编译器默认为module
设置的sourceSets
是什么,点击AS右上角的Gradle
菜单按钮,执行:app--Tasks--android--sourceSets
,然后点击AS 右下角的Gradle Console
我们可以看到如下内容,由于信息太多,所以我只展示部分关键信息
------------------------------------------------------------
Project :app
------------------------------------------------------------
androidTest
-----------
Java sources: [app/src/androidTest/java]
androidTestChina
----------------
Java sources: [app/src/androidTestChina/java]
china
-----
Java sources: [app/src/china/java]
test
----
Java sources: [app/src/test/java]
testChina
---------
Java sources: [app/src/testChina/java]
通过上面的输出可以看到,AS 默认的 sourceSet 中已经有这些测试源集的名称和源码位置了,以testChina
为例,我们可以看到其位置是app/src/testChina/java
,我们新建一个以test
开头的目录,以testChina
为例,结果我们看到确实java
文件夹确实是绿色的。如图十一
反之我们新建一个testABC
的目录,在其目录下新建一个java
目录,可以看到其颜色是灰色的,因为这样的命名不符合 AS 的规矩,所以 AS 不认可它作为测试源集。
在这里我们引出另外一条命名规矩:
BuildType 和 ProductFlavor 中的名字不能以 test、AndroidTest 开头
下面验证一下,可以不看我们在app
的build.gradle
中的builtTypes
代码块中新增一个字段,字段名为testMango
,如果我们猜测是对的,那么待会去创建对应的源集的时候java
文件夹应该是绿色的。不过 AS 很智能,我们的猜测也是对的,因为sync
之后,编译器报错了,错误信息如下:
Error:(25, 0) BuildType names cannot start with 'test'
同样的,我们在productFlavors
代码块中增加一个字段,字段名为testMango
,点击sync
,结果编译器也报错了,错误信息也类似,如下:
Error:(44, 0) ProductFlavor names cannot start with 'test'
同样的,我们也可以做同样的试验,以androidTest
开头添加字段到buildTypes
和productFlavors
代码块中,也同样报相似的错误信息。
至于怎么创建蓝色或者灰色的源集就不再赘述了,因为本章大部分时候都是在创建这类源集。
测试集合和其它源集有一个非常重要的区别,那就是前者里面的代码并不会在编译的时候打包到程序中,所以不会出现类重定义的编译异常。
五、废了这么大周章,搞这些有什么用?
文章一开始就简单说明了一些快速切换渠道包的好处,可以不通过修改注释直接简单配置不同渠道所使用的代码,降低这方面代码的耦合。
然后还有一种作用就是用来切换mock
和prod
,mock
表示数据仿真,prod
表示实际的生产环境。
举个例子,在开发网络这一块的时候我们在开发的时候可以切换到mock
分支,对后台数据进行模拟,这样可以快速验证我们业务模块的整体逻辑,等到逻辑理顺了,切换回prod
联调,确保聚焦当下,排除无用干扰。
同样的我们在单元测试的时候也常常需要去打桩(mock
),以覆盖分支为手段,快速验证某些与桩对象具体实现无关的逻辑,比如验证程序调用流程是否符合时序图的流程。那么需要打桩的单元测试放在testMock
中,不需要的单元测试放在test
中,也让代码目录更加清晰。
之所以写这篇文章,也是为了后面测试做准备,原本的想法很简单,测试代码多了,种类多了总不能都放在一个地方吧,这样显得多么臃肿。