编译时区分不同的manifest

很多Android项目都会区分debug和release的manifest文件,以便调试,一些组件化的项目甚至有多个manifest文件来调试不同的组件。举个简单的例子,在app的build.gradle文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android {
defaultConfig {
applicationId "com.xxx.xxx"
}
sourceSets {
main {
if(是否为debug打包) {
manifest.srcFile "${projectDir}/src/main/debug/AndroidManifest.xml"
} else {
manifest.srcFile "${projectDir}/src/main/release/AndroidManifest.xml"
}
}
}
}

这个地方的if条件,一般可以是一个变量,或者一个方法:

1
2
3
4
5
6
7
8
9
// 这样
def isDebug = true
// 或者
def isDebug() {
// 一些逻辑
return true
}
// 以上均遵循Groovy语法规则,Android项目中的Gradle脚本都是用Groovy语言(一门JVM动态语言,很容易上手)编写的。
android { ... }

然后此处的 ${projectDir} 变量对应的是当前模块所在的路径,这里就是app的路径,不是整个工程的路径。这样一来,我们就能编译不同的manifest文件了。

自动判断编译类型

但是,这样每次编译都需要手动改那个debug变量,挺麻烦的,尤其是一些技术团队可能是用的公司服务器在线编译,每次都要提交代码到仓库。其实我们可以用gradle插件的特性,这样来判断:

1
2
3
4
5
6
// 这里app是当前模块名,assembleDebug是gradle编译debug包时的默认task
if(gradle.startParameter.taskNames.contains(":app:assembleDebug")) {
manifest.srcFile "${projectDir}/src/main/debug/AndroidManifest.xml"
} else {
manifest.srcFile "${projectDir}/src/main/release/AndroidManifest.xml"
}

不用改代码,打release包和debug包就自动区分了,当然你也可以增加新的条件,去对应不同的编译task。

解析并自动生成manifest文件

这部分是本文重点啦!从上面的步骤来看,我们显然是准备了2份manifest文件,平时维护时也需要一起修改。实际业务中很可能debug和release的manifest内容差不多,可能只是某些组件节点的属性不同,手动改也挺麻烦的。

能不能通过gradle脚本动态地来修改并生成manifest文件呢?当然可以,本质上就是处理XML文件。AndroidManifest是标准的XML文件。正好,Groovy处理XML又非常的简单。这里我们还是以一个实际例子来讲。

比如我们的需求是,在debug测试时,应用正常显示桌面图标,在release发布时,应用需要隐藏图标。那么,两个manifest文件就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ...>
<!--App入口页面-->
<activity
...
android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...

release版本的manifest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ...>
<!--App入口页面-->
<activity
...
android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!--在入口Activity添加如下data节点即可隐藏桌面图标-->
<data
android:host="localhost"
android:scheme="${applicationId}" />
</intent-filter>
</activity>
...

然后平时我们开发过程中,如果新增了一些四大组件,2个文件都要同时增加,而区别却只有入口Activity。我们想要的是release版本的manifest是自动生成的(在生成时插入那个data节点),平时只需要开发改动debug版本的文件即可。

主要思路比较简单:

  1. 通过Groovy的XML解析库读取debug的manifest文件,遍历节点找到入口Activity;
  2. 将data节点插入到入口Activity下面;
  3. 把新的内容写入release版本的文件当中。

先直接看build.gradle脚本源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import groovy.xml.XmlUtil

def getManifestPath(buildType) {
return "${projectDir}/src/main/$buildType/AndroidManifest.xml"
}

def handleManifestXml(manifest) {
if (gradle.startParameter.taskNames.contains(":app:assembleDebug")) {
// debug build
def debugPath = getManifestPath('debug')
manifest.srcFile debugPath
println("Manifest path: $debugPath")
} else {
// release build
def releasePath = getManifestPath('release')
println("Manifest path: $releasePath")
manifest.srcFile releasePath

def debugPath = getManifestPath('debug')
def debugFile = new File(debugPath)
def releaseFile = new File(releasePath)
def debugXml = new XmlParser(false, false).parse(debugFile)
// 修改debug的manifest,自动生成release版本
debugXml.application[0].each { comp ->
if (comp.name() == 'activity') {
comp.each { filter ->
// 搜索入口Activity
if (filter.toString().contains('android.intent.category.LAUNCHER')) {
// 添加data节点,隐藏桌面icon
filter.appendNode('data', ['android:host': 'localhost', 'android:scheme': '${applicationId}'])
return true // break each闭包
}
}
}
}
releaseFile.write(XmlUtil.serialize(debugXml))
}
}

android {
defaultConfig {
applicationId "com.xxx.xxx"
}
sourceSets {
main {
// 此处会自动处理manifest文件
// 注意:平时开发时只需修改debug下的manifest即可,请勿手动修改release的
handleManifestXml(manifest)
}
}
...

关键逻辑从 def debugXml = new XmlParser(false, false).parse(debugFile) 开始,这里的XmlParser构造方法可以不传任何参数,我这里传false主要是为了让manifest根节点自动添加 xmlns ,这样最后生成文件内容会简洁一点。

然后就是 debugXml.application[0].each { comp -> ... } 循环块,Groovy的语法很神奇,这里可以直接通过节点名称来获取数组,比如application,取0当然就是第一个,一般我们的manifest里就一个application节点,所以也不会出现空指针异常。each是数组的函数,遍历数组的每一个节点,comp参数是我自定义的命名。能拿到application的子节点,后面就都是一些逻辑处理了,不赘述。

一些小问题

如果是Windows开发者,最后写文件时,需要注意换行符和编码的问题。

1、最后的write方法,加上参数:

1
releaseFile.write(xmlStr, "UTF-8")

或者直接改全局配置,修改整个工程下的gradle.properties文件,在gradle的JVM参数后面追加如下:

1
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

2、换行符问题,没有JVM参数可供修改,只能手动处理,还是最后write时:

1
releaseFile.write(XmlUtil.serialize(debugXml).replaceAll('\r\n', '\n'))

这里XmlUtil工具类的serialize方法返回类型实际上就是String,所以可以这样直接replace。