在 Gradle 任务 processManifest.doLast 中编辑 AndroidManifest.xml 在 Android Studio 中的 运行 应用时无效

Editing AndroidManifest.xml in Gradle task processManifest.doLast has no effect when running app from Android Studio

我使用下面的 Gradle 脚本在编译时对 AndroidManifest.xml 进行了一些修改。在这个例子中,我想注入一个 <meta-data> 元素。代码基于this answer.

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 Studio 2.0 和 Android Gradle 插件 2.0.0.

我发现它与 Android Studio 2.0 中引入的 Instant 运行 功能有关。如果我将其关闭,一切都会按预期进行。但是因为我想使用 Instant 运行,所以我进一步挖掘了一点。

事实是,启用 Instant 运行 后,中间 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 的文档。我得到的唯一 Google 搜索结果是 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-3com.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 ""
    }
}

intermediates/merged_manifests 的位置是从模块的构建目录中找到的,它可能是其他的取决于 android-plugin 版本,只需查看构建目录并找到你的。

我需要编辑合并的清单文件,这对我有用

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')
   }
}
}

在这个简单的例子中,我将应用程序名称替换为 Broccoli。

如果您需要更多信息,我写了一篇关于它的博客post

https://androidexplained.github.io/android/gradle/2020/09/28/editing-manifest.html

替换清单文件中内容的正确方法是使用 manifest placeholders.

属性 获取 key-value 对的映射。

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>