Manifest 合并机制
在 Android 项目中,通常存在多个 AndroidManifest.xml 文件:主模块的清单、构建变体的清单、库模块的清单。Gradle 构建系统会将这些清单文件合并成一个最终的清单文件打包到 APK 或 AAB 中。理解清单合并机制对于管理复杂项目至关重要。
清单合并流程
为什么需要合并
- 模块化开发:主模块、库模块各有自己的清单
- 构建变体:不同变体需要不同的配置(如权限、组件)
- 灵活配置:根据 Build Type 或 Product Flavor 动态调整清单内容
合并时机
清单合并发生在编译时,Gradle 按以下流程处理:
- 收集所有相关的清单文件
- 根据优先级顺序合并
- 应用合并规则标记
- 生成最终的合并清单
- 打包到 APK/AAB
合并优先级
合并优先级从高到低:
1. Build Variant 清单
对于包含多个源代码集的变体,优先级为:
- Build Variant 清单(如
src/demoDebug/) - 最高优先级 - Build Type 清单(如
src/debug/) - Product Flavor 清单(如
src/demo/)
如果使用多个 Flavor Dimensions,清单优先级与 flavorDimensions 中的列示顺序对应(排在前面的维度优先级更高)。
2. 主模块清单
src/main/AndroidManifest.xml
3. 库模块清单
- 库的清单优先级与它们在
dependencies块中的顺序一致 - 先声明的库优先级更高
示例优先级链:
src/demoDebug/AndroidManifest.xml (最高)
↓
src/debug/AndroidManifest.xml
↓
src/demo/AndroidManifest.xml
↓
src/main/AndroidManifest.xml
↓
library1/AndroidManifest.xml
↓
library2/AndroidManifest.xml (最低)合并策略
当两个清单包含相同的 XML 元素时,合并工具使用以下策略之一:
Merge(合并)
适用于:大多数元素(如 <application>、<activity>)
行为:
- 合并所有不冲突的属性到同一标签
- 根据各自策略合并子元素
- 属性冲突时使用合并规则标记处理
示例:
低优先级清单:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="stateUnchanged">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>高优先级清单:
<activity
android:name=".MainActivity"
android:screenOrientation="portrait" />合并结果:
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateUnchanged">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>Merge Children Only(仅合并子元素)
适用于:某些特殊元素
行为:
- 不合并属性(仅保留高优先级清单的属性)
- 根据策略合并子元素
Keep(保留)
适用于:允许声明多个的元素(如 <uses-feature>、<uses-permission>)
行为:
- 元素原样保留
- 添加到合并文件的共同父元素中
合并冲突处理
默认冲突处理
当两个清单定义相同元素的不同属性值时:
- 默认策略:严格模式 - 构建失败并报错
- 推荐做法:使用合并规则标记明确指定如何处理
常见冲突场景
场景一:权限冲突
库声明了更高的权限级别:
<!-- 库清单 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- 主清单 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />解决方法:使用 tools:remove 或 tools:replace
场景二:Activity 配置冲突
<!-- 库清单 -->
<activity
android:name=".SomeActivity"
android:screenOrientation="landscape" />
<!-- 主清单 -->
<activity
android:name=".SomeActivity"
android:screenOrientation="portrait"
tools:replace="android:screenOrientation" />合并规则标记
合并规则标记是 tools 命名空间下的 XML 属性,用于明确指定合并行为。
声明 Tools 命名空间
在 <manifest> 根元素中声明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.myapp">节点标记
tools:node="merge"
默认行为,合并所有属性和子元素。
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
tools:node="merge">
</activity>tools:node="mergeOnlyAttributes"
仅合并属性,不合并子元素(使用高优先级清单的子元素)。
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
tools:node="mergeOnlyAttributes">
</activity>tools:node="remove"
从最终清单中移除低优先级清单的该元素。
示例:移除库中不需要的权限
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />tools:node="removeAll"
移除所有低优先级清单中该类型的元素。
<!-- 移除所有库声明的 meta-data -->
<activity android:name=".MainActivity">
<meta-data tools:node="removeAll" />
</activity>tools:node="replace"
完全替换低优先级清单的元素(包括所有属性和子元素)。
<activity
android:name=".MainActivity"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>tools:node="strict"
严格模式,如果低优先级清单有冲突的值则构建失败。
<activity
android:name=".MainActivity"
tools:node="strict" />###属性标记
tools:replace
替换指定属性的值。
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
tools:replace="android:screenOrientation" />多个属性用逗号分隔:
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"
tools:replace="android:screenOrientation,android:theme" />tools:remove
从合并结果中移除指定属性。
<uses-permission
android:name="android.permission.CAMERA"
tools:remove="android:maxSdkVersion" />选择器标记
tools:selector
指定合并规则仅应用于特定变体。
可用的选择器:
product:仅应用于特定 Product FlavorbuildType:仅应用于特定 Build Typevariant:仅应用于特定 Build Variant
<permission
android:name="com.example.CUSTOM_PERMISSION"
android:protectionLevel="signature"
tools:selector="debug" />构建变量注入
可以使用 manifestPlaceholders 将构建配置中的值注入到清单文件。
配置占位符
android {
defaultConfig {
manifestPlaceholders["hostName"] = "www.example.com"
manifestPlaceholders["apiKey"] = "default-api-key"
}
buildTypes {
getByName("debug") {
manifestPlaceholders["hostName"] = "debug.example.com"
manifestPlaceholders["apiKey"] = "debug-api-key"
}
getByName("release") {
manifestPlaceholders["hostName"] = "api.example.com"
manifestPlaceholders["apiKey"] = System.getenv("API_KEY") ?: "release-api-key"
}
}
productFlavors {
create("free") {
manifestPlaceholders["adProvider"] = "admob"
}
create("paid") {
manifestPlaceholders["adProvider"] = "none"
}
}
}android {
defaultConfig {
manifestPlaceholders = [
hostName: "www.example.com",
apiKey: "default-api-key"
]
}
buildTypes {
debug {
manifestPlaceholders = [
hostName: "debug.example.com",
apiKey: "debug-api-key"
]
}
release {
manifestPlaceholders = [
hostName: "api.example.com",
apiKey: System.getenv("API_KEY") ?: "release-api-key"
]
}
}
productFlavors {
free {
manifestPlaceholders = [adProvider: "admob"]
}
paid {
manifestPlaceholders = [adProvider: "none"]
}
}
}在清单中使用
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- 使用占位符 -->
<meta-data
android:name="com.example.API_KEY"
android:value="${apiKey}" />
<meta-data
android:name="com.example.HOST_NAME"
android:value="${hostName}" />
<!-- 条件性包含组件 -->
<service
android:name=".AdService"
android:enabled="${adProvider == 'admob'}" />
</application>
</manifest>使用 Application ID
${applicationId} 占位符会被自动替换为实际的 Application ID:
<provider
android:name=".MyContentProvider"
android:authorities="${applicationId}.provider"
android:exported="false" />查看合并结果
在 Android Studio 中查看
- 打开 Android Studio
- 在项目导航中切换到 Project 视图
- 打开
app/build/intermediates/merged_manifests/[buildVariant]/AndroidManifest.xml
或者:
- 打开任意
AndroidManifest.xml文件 - 点击编辑器底部的 Merged Manifest 标签页
- 查看合并后的完整清单
- 左侧面板显示贡献了哪些清单文件
- 右侧可查看具体的合并日志
构建后查看
合并后的清单位于:
app/build/intermediates/merged_manifests/
├── debug/
│ └── AndroidManifest.xml
├── release/
│ └── AndroidManifest.xml
├── demoDebug/
│ └── AndroidManifest.xml
└── ...实践案例
案例一:移除库权限
第三方库声明了不需要的权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 移除库声明的权限 -->
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />
</manifest>案例二:覆盖库的 Activity 配置
<application>
<!-- 覆盖库中 Activity 的orientation -->
<activity
android:name="com.library.SomeActivity"
android:screenOrientation="portrait"
tools:replace="android:screenOrientation" />
</application>案例三:不同环境的配置
android {
productFlavors {
create("dev") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
manifestPlaceholders["appLabel"] = "MyApp Dev"
}
create("prod") {
manifestPlaceholders["usesCleartextTraffic"] = "false"
manifestPlaceholders["appLabel"] = "MyApp"
}
}
}android {
productFlavors {
dev {
manifestPlaceholders = [
usesCleartextTraffic: "true",
appLabel: "MyApp Dev"
]
}
prod {
manifestPlaceholders = [
usesCleartextTraffic: "false",
appLabel: "MyApp"
]
}
}
}<application
android:label="${appLabel}"
android:usesCleartextTraffic="${usesCleartextTraffic}">
</application>案例四:Debug 专用组件
<application>
<!-- 仅在 debug 构建中启用 -->
<activity
android:name=".DebugActivity"
tools:selector="debug"
tools:node="merge" />
<!-- 在 release 中移除 -->
<activity
android:name=".DebugActivity"
tools:selector="release"
tools:node="remove" />
</application>Build.gradle 配置优先
⚠️ 重要:build.gradle(.kts) 中的配置会覆盖合并后清单中的对应属性。
示例:
清单中定义:
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="33" />Build 配置:
android {
defaultConfig {
minSdk = 24 // 会覆盖清单中的 21
targetSdk = 36 // 会覆盖清单中的 33
}
}最佳实践:省略清单中的 <uses-sdk>,仅在 build.gradle(.kts) 中定义。
调试合并问题
启用详细日志
./gradlew assembleDebug --info在日志中搜索 "manifest merger" 查看详细的合并信息。
常见错误
错误一:属性冲突
Attribute application@theme value=(@style/AppTheme) from AndroidManifest.xml:10
is also present at [com.library:1.0] AndroidManifest.xml:12 value=(@style/LibTheme).
Suggestion: add 'tools:replace="android:theme"' to <application> element.解决:按提示添加 tools:replace 标记。
错误二:元素冲突
Multiple entries with same key: @android:name=PERMISSION_NAME解决:使用 tools:node="remove" 移除重复项。
最佳实践
- 最小化清单:仅在必要时才在变体清单中覆盖值
- 使用占位符:动态配置使用
manifestPlaceholders而非硬编码 - 明确标记:有冲突时显式使用
tools:replace等标记 - 版本集中管理:SDK 版本在
build.gradle(.kts)中配置,不在清单中 - 定期检查:使用 Merged Manifest 视图检查合并结果
- 文档化:为复杂的合并规则添加注释说明意图