Skip to content

Manifest 合并机制

源:Android 官方文档 - 管理清单文件

在 Android 项目中,通常存在多个 AndroidManifest.xml 文件:主模块的清单、构建变体的清单、库模块的清单。Gradle 构建系统会将这些清单文件合并成一个最终的清单文件打包到 APK 或 AAB 中。理解清单合并机制对于管理复杂项目至关重要。

清单合并流程

为什么需要合并

  • 模块化开发:主模块、库模块各有自己的清单
  • 构建变体:不同变体需要不同的配置(如权限、组件)
  • 灵活配置:根据 Build Type 或 Product Flavor 动态调整清单内容

合并时机

清单合并发生在编译时,Gradle 按以下流程处理:

  1. 收集所有相关的清单文件
  2. 根据优先级顺序合并
  3. 应用合并规则标记
  4. 生成最终的合并清单
  5. 打包到 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>

行为

  • 合并所有不冲突的属性到同一标签
  • 根据各自策略合并子元素
  • 属性冲突时使用合并规则标记处理

示例

低优先级清单:

xml
<activity
    android:name=".MainActivity"
    android:windowSoftInputMode="stateUnchanged">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
    </intent-filter>
</activity>

高优先级清单:

xml
<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait" />

合并结果:

xml
<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>

行为

  • 元素原样保留
  • 添加到合并文件的共同父元素中

合并冲突处理

默认冲突处理

当两个清单定义相同元素的不同属性值时:

  • 默认策略:严格模式 - 构建失败并报错
  • 推荐做法:使用合并规则标记明确指定如何处理

常见冲突场景

场景一:权限冲突

库声明了更高的权限级别:

xml
<!-- 库清单 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

<!-- 主清单 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

解决方法:使用 tools:removetools:replace

场景二:Activity 配置冲突

xml
<!-- 库清单 -->
<activity
    android:name=".SomeActivity"
    android:screenOrientation="landscape" />

<!-- 主清单 -->
<activity
    android:name=".SomeActivity"
    android:screenOrientation="portrait"
    tools:replace="android:screenOrientation" />

合并规则标记

合并规则标记是 tools 命名空间下的 XML 属性,用于明确指定合并行为。

声明 Tools 命名空间

<manifest> 根元素中声明:

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapp">

节点标记

tools:node="merge"

默认行为,合并所有属性和子元素。

xml
<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    tools:node="merge">
</activity>

tools:node="mergeOnlyAttributes"

仅合并属性,不合并子元素(使用高优先级清单的子元素)。

xml
<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    tools:node="mergeOnlyAttributes">
</activity>

tools:node="remove"

从最终清单中移除低优先级清单的该元素。

示例:移除库中不需要的权限

xml
<uses-permission
    android:name="android.permission.READ_PHONE_STATE"
    tools:node="remove" />

tools:node="removeAll"

移除所有低优先级清单中该类型的元素。

xml
<!-- 移除所有库声明的 meta-data -->
<activity android:name=".MainActivity">
    <meta-data tools:node="removeAll" />
</activity>

tools:node="replace"

完全替换低优先级清单的元素(包括所有属性和子元素)。

xml
<activity
    android:name=".MainActivity"
    tools:node="replace">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
    </intent-filter>
</activity>

tools:node="strict"

严格模式,如果低优先级清单有冲突的值则构建失败。

xml
<activity
    android:name=".MainActivity"
    tools:node="strict" />

###属性标记

tools:replace

替换指定属性的值。

xml
<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    tools:replace="android:screenOrientation" />

多个属性用逗号分隔:

xml
<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme"
    tools:replace="android:screenOrientation,android:theme" />

tools:remove

从合并结果中移除指定属性。

xml
<uses-permission
    android:name="android.permission.CAMERA"
    tools:remove="android:maxSdkVersion" />

选择器标记

tools:selector

指定合并规则仅应用于特定变体。

可用的选择器:

  • product:仅应用于特定 Product Flavor
  • buildType:仅应用于特定 Build Type
  • variant:仅应用于特定 Build Variant
xml
<permission
    android:name="com.example.CUSTOM_PERMISSION"
    android:protectionLevel="signature"
    tools:selector="debug" />

构建变量注入

可以使用 manifestPlaceholders 将构建配置中的值注入到清单文件。

配置占位符

kotlin
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"
        }
    }
}
groovy
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"]
        }
    }
}

在清单中使用

xml
<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:

xml
<provider
    android:name=".MyContentProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false" />

查看合并结果

在 Android Studio 中查看

  1. 打开 Android Studio
  2. 在项目导航中切换到 Project 视图
  3. 打开 app/build/intermediates/merged_manifests/[buildVariant]/AndroidManifest.xml

或者:

  1. 打开任意 AndroidManifest.xml 文件
  2. 点击编辑器底部的 Merged Manifest 标签页
  3. 查看合并后的完整清单
  4. 左侧面板显示贡献了哪些清单文件
  5. 右侧可查看具体的合并日志

构建后查看

合并后的清单位于:

app/build/intermediates/merged_manifests/
    ├── debug/
    │   └── AndroidManifest.xml
    ├── release/
    │   └── AndroidManifest.xml
    ├── demoDebug/
    │   └── AndroidManifest.xml
    └── ...

实践案例

案例一:移除库权限

第三方库声明了不需要的权限:

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 配置

xml
<application>
    <!-- 覆盖库中 Activity 的orientation -->
    <activity
        android:name="com.library.SomeActivity"
        android:screenOrientation="portrait"
        tools:replace="android:screenOrientation" />
</application>

案例三:不同环境的配置

kotlin
android {
    productFlavors {
        create("dev") {
            manifestPlaceholders["usesCleartextTraffic"] = "true"
            manifestPlaceholders["appLabel"] = "MyApp Dev"
        }
        
        create("prod") {
            manifestPlaceholders["usesCleartextTraffic"] = "false"
            manifestPlaceholders["appLabel"] = "MyApp"
        }
    }
}
groovy
android {
    productFlavors {
        dev {
            manifestPlaceholders = [
                usesCleartextTraffic: "true",
                appLabel: "MyApp Dev"
            ]
        }
        
        prod {
            manifestPlaceholders = [
                usesCleartextTraffic: "false",
                appLabel: "MyApp"
            ]
        }
    }
}
xml
<application
    android:label="${appLabel}"
    android:usesCleartextTraffic="${usesCleartextTraffic}">
</application>

案例四:Debug 专用组件

xml
<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) 中的配置会覆盖合并后清单中的对应属性。

示例

清单中定义:

xml
<uses-sdk
    android:minSdkVersion="21"
    android:targetSdkVersion="33" />

Build 配置:

kotlin
android {
    defaultConfig {
        minSdk = 24  // 会覆盖清单中的 21
        targetSdk = 36  // 会覆盖清单中的 33
    }
}

最佳实践:省略清单中的 <uses-sdk>,仅在 build.gradle(.kts) 中定义。

调试合并问题

启用详细日志

bash
./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" 移除重复项。

最佳实践

  1. 最小化清单:仅在必要时才在变体清单中覆盖值
  2. 使用占位符:动态配置使用 manifestPlaceholders 而非硬编码
  3. 明确标记:有冲突时显式使用 tools:replace 等标记
  4. 版本集中管理:SDK 版本在 build.gradle(.kts) 中配置,不在清单中
  5. 定期检查:使用 Merged Manifest 视图检查合并结果
  6. 文档化:为复杂的合并规则添加注释说明意图