我在编译时使用以下 Gradle 脚本对 AndroidManifest 进行一些修改.xml。在此示例中,我想注入一个<meta-data>
元素。代码基于此答案。
android {
// ...
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def manifestOutFile = output.processManifest.manifestOutputFile
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
当我在 Android Studio 中进行 Gradle 同步或从命令行进行干净的构建时,这可以按预期工作:元数据可以从应用程序内访问。
但是当我从Android Studio运行▶应用程序时,修改后的清单似乎被忽略了,因为插入的元数据不是APK中编译清单的一部分,并且应用程序本身在运行时也找不到它,元数据根本不存在。
在所有情况下,合并的中间 AndroidManifest.xml(在/build/intermediates/manifests/ 中)确实包含更改,但由于某种原因,如果我运行该应用程序,它看起来会被忽略。
为了使它更加明显,我尝试插入一些无效的XML: 在这种情况下,由于清单中的语法错误,Gradle 同步和干净构建按预期失败。但是我仍然能够从Android Studio运行该应用程序,因此修改实际上被忽略了。
重现此问题的最简单方法是先清理项目(在 Android Studio 中),这会导致重新处理清单(如果出现语法错误,我按预期失败),然后运行该应用程序,即使使用无效的清单也可以运行。
请注意,doLast
中的任务每次都会执行:打印任务中的println()
,中间清单包含更改。
就好像在我的任务执行之前,清单被编译到 APK 中一样。
问题出在哪里?
我正在使用带有Android Gradle Plugin 2.0.0的Android Studio 2.0。
我发现它与Android Studio 2.0中引入的即时运行功能有关。如果我关闭它,一切都按预期工作。但是由于我想使用即时运行,所以我进一步挖掘。
问题是,启用即时运行后,中间的AndroidManifest.xml文件将位于另一个位置,即/build/intermediates/bundles/myflavor/instant-run/。这意味着我有效地编辑了错误的文件。另一个清单文件可以使用属性 instantRunManifestOutputFile
访问,可以使用它代替 manifestOutputFile
。
为了使它在所有用例中都能正常工作,我检查两个临时清单文件是否存在,如果存在,请修改它们:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
[output.processManifest.manifestOutputFile,
output.processManifest.instantRunManifestOutputFile
].forEach({ File manifestOutFile ->
if (manifestOutFile.exists()) {
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
})
}
}
}
实际上没有instantRunManifestOutputFile
的文档.我得到的唯一谷歌搜索结果是Android Gradle插件源代码。但后来我还发现了第三个潜在的清单文件属性aaptFriendlyManifestOutputFile
,我也不知道它是关于什么的......
为这个问题添加一些额外的信息。@Floern的答案有点过时了。该代码适用于旧的 Gradle 版本。新版本的Gradle表示manifestOutputFile
已被弃用,并将很快被删除。 instantRunManifestOutputFile
根本不存在。因此,以下是新 Gradle 版本的示例:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def outputDirectory = output.processManifest.manifestOutputDirectory
File manifestOutFile = file(new File(outputDirectory, 'AndroidManifest.xml'))
if(manifestOutFile.exists()){
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
编辑:以下是 Gradle 5.4.1 和 Grudle 插件 3.5.1 的较新变体:
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File outputDirectory
if (outputDir instanceof File) {
outputDirectory = outputDir
} else {
outputDirectory = outputDir.get().asFile
}
File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
更新:
对于 Gradle 7.0.2和 Gradle 插件 7.0.2
而不是task.getManifestOutputDirectory()
使用task.getMultiApkManifestOutputDirectory()
希望这会帮助某人。
各种gradle版本有所不同,对我来说,我使用了gradle-5.5-rc-3
和com.android.tools.build:gradle:3.4.1
所以这将起作用:
def static setVersions(android, project, channelId) {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processorTask = output.processManifestProvider.getOrNull()
processorTask.doLast { task ->
def directory = task.getBundleManifestOutputDirectory()
def srcManifestFile = "$directory/AndroidManifest.xml"
def manifestContent = new File(srcManifestFile).getText()
def xml = new XmlParser(false, false).parseText(manifestContent)
xml.application[0].appendNode("meta-data", ['android:name': 'channelId', 'android:value': '\' + channelId])
def serializeContent = groovy.xml.XmlUtil.serialize(xml)
def buildType = getPluginBuildType(project)
new File("${project.buildDir}/intermediates/merged_manifests/$buildType/AndroidManifest.xml").write(serializeContent)
}
}
}
}
def static getPluginBuildType(project) {
def runTasks = project.getGradle().startParameter.taskNames
if (runTasks.toString().contains("Release")) {
return "release"
} else if (runTasks.toString().contains("Debug")) {
return "debug"
} else {
return ""
}
}
中间体/merged_manifests的位置是从模块的构建目录中找到的,可能是其他位置取决于 android 插件版本,只需查看构建目录并找到您的。
我需要编辑合并的清单文件,这对我有用
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newManifest = manifestOutFile.getText().replace("@string/app_name", "Broccoli")
manifestOutFile.write(newManifest, 'UTF-8')
}
}
}
在这个简单的情况下,我将应用程序名称替换为西兰花。
如果您需要更多信息,我写了一篇关于它的博客文章
https://androidexplained.github.io/android/gradle/2020/09/28/editing-manifest.html
替换清单文件中某些内容的正确方法是使用清单占位符。
该属性采用键值对的映射。
android {
defaultConfig {
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
...
}
如果使用多个风格,则可以按风格定义属性。
flavorDimensions "environment"
productFlavors {
staging {
dimension "environment"
applicationIdSuffix ".test"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
prod {
dimension "environment"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
}
然后,您可以将其中一个占位符作为属性值插入清单文件中,如下所示
<intent-filter ... >
<data android:scheme="http" android:host="${hostName}" ... />
<data android:scheme="http" android:host="${hostName2}" ... />
...
</intent-filter>