背景
目前有这样的一个情景:在开发过程中我们需要在整个应用启动的前面选择合适的环境,因此我们会将整个应用的启动界面替换成HostChooseActivity,将WelcomeActivity相应的启动intent-filter注释掉。而在上线时又需要将HostChooseActivity在Manifest文件里注释掉,将WelcomeActivity的intent-filter打开。本地还好,但是在jenkins上就很麻烦,比如当目前的代码不包含可选环境时,QA需要一个可选环境的包来进行测试,这时就得本地重新更改代码然后push、merge、Jenkins重新打包,流程很复杂。所以希望能够根据不同的build type来选择合适的Manifest,最后在官方文档里找到了解决方案,在这之前先简单介绍下预备知识。
Build Variants
我们知道在Android Studio中有一个Build Variants,它是根据我们在gradle文件里的buildTypes和productFlavors生成的,通过这个我们可以对我们的项目创建不同的版本。例如:可以针对不同的渠道替换一些资源文件,或者更改min Api levels。下面介绍下buildTypes和productFlavors。
配置Build Types
当你创建一个Module时,AS会自动的创建debug和release这两种build types,其中debug并不会在gradle文件里显示,但其实是存在的,我们点击AS的Build Variants时会看见,平时开发时默认的就是这个模式,而且它的debuggable是为true的。下面是一个例子:
develop和stage是自己定义的,我们可以通过initWith来通过前面定义的进行配置,也可以自己配置,自己配置时需要指定signingConfig。具体的参考下面的链接:
http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.BuildType.html
针对每一个build type,会有一个对应的sourceSet,默认是src/buildtype的名字,需要自己手动创建,但是也可以指定:
配置Product Flavors
Product Flavors的配置和buildTypes很相似,每个Product Flavors的配置都会先由defaultConfig提供,自己也可以重写覆盖。下面是我们的配置,和buildTypes一样,会在src/name 下针对不同的渠道做一些差异化的处理,productFlavors和buildTypes不能相同,否则会报错
通过source sets进行构建
当我们配置好buildTypes和productFlavors后,点击syn,AS会自动做一个笛卡尔积,这个就是build variant了。当我们选择了某个build variant进行Build时,它会分别在main的source set、buildType的source sets、、product flavors的source set以及build variant的source set进行合并。优先级由低到高。我们可以用gradle sourcesets来查看如何管理你的目录:
这个是我设置develop的sourceSets为develop2的:
最终的版本遵循下面的规则:
- 所有source set在java/目录下的都会被一起编译,如果有相同的则会报错
- Manifest会被合到单独的一个Manifest文件中,优先级如上。之后会详细介绍
- 在res/和asset目录下的资源文件会被一起打包,如果在不同的source set里有相同的,优先级如上。
- 在构建最终的APK文件时,也会带上依赖第三方库的资源和manifest文件,它们的优先级是最低的。
同时我们可以用gradle androidDependencies
来查看依赖,这个很有用,经常用来查看包的冲突。
合并Manifest文件
通过合并Manifest文件可以实现我最开始提到的需求。我们的apk最终只会包含一个AndroidManifest.xml,但是因为我们的main source set,build variants和引入的第三方依赖都可能存在Manifest文件,这时候就需要做合并。
如上图所示,第三方库会合并进main的manifest,然后main的manifest会合并进build variant的manifest,具体优先级如下:
- build variant的Manifest
- build variant 的manifest(例如:src/demoDebug/)
- build type 的manifest(例如:src/debug)
- product flavor 的manifest(例如:src/demo)
- app moudle下的主manifest
- 第三方库里的manifest
note1:build.gradle会覆盖Manifest里的一致的属性,所以为了避免冲突尽量在build.gradle里设置和Manifest一致的属性,例如:minSdkVersion等。
解决Merge冲突
合并时会对XML的结点进行匹配,如果满足了则会认为是同一个结点,然后就会存在冲突。具体的匹配规则如下:
如果一个低优先级的结点不能匹配任何高优先级的结点就会被加入到高优先级的Manifest文件里,如果匹配上了则会进行合并,如果该结点下的存在相同属性在不同文件里具有不同的值时则会报错需要在较高优先级的manifest文件里添加合并规则标识(Merge rule markers)。
note2:不要依赖默认的属性值。因为所有单独的属性都会被合并到同一个结点,如果高优先级依赖默认的属性,而如上图低优先级设置了一个属性,这时最终的结点下的属性就是低优先级的,会存在问题。例如:Activity结点的launchMode属性。
合并规则标识(Merge rule markers)
标识是用来告诉合并工具如何的去解决冲突,所有的标识都属于Android tools namespace,你需要在<manifest>结点上写上,如下:
markers包含:Node markers、Attribute markers,其中Node markers是用于整个结点,而Attribute markers是用于结点里的属性。
结点标识(node markers)
对匹配到的结点设置合并规则:如
具体规则如下:
- merge: 合并一个结点下的全部属性和子结点,这也是默认的,不过会产生冲突
- merge-only-attributes:只合并整个结点的属性,不合并这个结点下面的子结点
- remove: 在合并后的manifest文件中将该结点移除,为了防止较低优先级的manifest存在该结点而高优先级的不存在导致的问题
- removeAll: 移除匹配该结点的所有结点
- replace: 完全替代较低优先级的结点
- strict: 严格模式,两个Manifest文件匹配的结点必须完全相同,如果有不相同的就会报错
属性标识(Attribute markers)
只对一个结点下的某些属性设置合并规则,对一个结点可以设置多个属性,用”,”分开:如tools:remove="attr, ..."
包含:remove
、replace
、strict
,规则和结点标记一致。
Marker selector
如果目前有多个library的Manifest需要合并,而你只想让合并规则作用于某个指定的library,对其他的library不生效时,可以通过selector来指定目录,例如,下面的remove规则只对来自com.example.lib1
文件下的Manifest文件生效:
通过工具查看合并
我们可以通过AS自带的工具来查看Manifest文件的合并,点击manifest文件下的Merged Manifest
文件如下图,左边是合并后的代码,右边是合并的哪些manifest文件。
解决问题
针对开头提到的问题,可以通过如下方式解决:
在main里:
在develop时:
因为<intent-filter>
不能匹配,所以只能在develop下通过replace来进行替换,如果在之后对main的MainActivity进行修改时,需要同时对develop下的进行修改,不过我们很少对Manifest下的MainActivity结点进行修改