本页介绍了清单合并的运作方式以及如何应用合并偏好设置解决合并冲突。有关应用清单文件的说明,请参阅应用清单概览。
合并多个清单文件
APK 或 Android App Bundle 文件只能包含一个 AndroidManifest.xml
文件,但 Android Studio 项目可以包含多个清单文件,这些清单文件由主源代码集、build 变体和导入的库提供。在构建应用时,Gradle 构建系统会将所有清单文件合并成一个清单文件打包到应用中。
清单合并工具遵循合并启发法和您使用特殊 XML 属性定义的合并偏好设置,将各个清单文件中的所有 XML 元素组合在一起。
提示:使用下一部分中所述的合并后的清单视图可预览合并后的清单结果并找出冲突错误。
合并优先级
合并工具会根据每个清单文件的优先级按顺序合并,将所有清单文件组合到一个文件中。例如,如果您有三个清单文件,则会先将优先级最低的清单合并到优先级第二高的清单中,然后再将合并后的清单合并到优先级最高的清单中,如图 1 所示。
有三种基本的清单文件可以互相合并,它们的合并优先级如下(按优先级由高到低的顺序):
- build 变体的清单文件
如果变体有多个源代码集,则其清单优先级如下:
- Build 变体清单(如
src/demoDebug/
) - Build 类型清单(如
src/debug/
) - 产品变种清单(如
src/demo/
)如果您使用的是变种维度,则清单优先级与每个维度在
flavorDimensions
属性中的列示顺序(按优先级由高到低的顺序)对应。
- Build 变体清单(如
- 应用模块的主清单文件
- 所包含的库中的清单文件
如果您有多个库,则其清单优先级与其出现在 Gradle
dependencies
代码块中的顺序一致。
例如,先将库清单合并到主清单中,然后再将主清单合并到 build 变体清单中。注意:这些合并优先级对所有源代码集都相同,如使用源代码集构建中所述。
重要提示:build.gradle
文件中的 build 配置将替换合并后的清单文件中的所有对应属性。例如,build.gradle
或 build.gradle.kts
文件中的 minSdk
将替换 <uses-sdk>
清单元素中的匹配属性。为避免混淆,请省去 <uses-sdk>
元素并仅在 build.gradle
文件中定义这些属性。如需了解详情,请参阅配置 build。
合并冲突启发法
合并工具可以在逻辑上将一个清单中的每个 XML 元素与另一个清单中的对应元素相匹配。如需详细了解匹配功能的工作原理,请参阅上一部分中的合并优先级。
如果优先级较低的清单中的某个元素与优先级较高的清单中的所有元素都不匹配,则会将该元素添加到合并后的清单中。不过,如果有匹配的元素,则合并工具会尝试将每个元素的所有属性组合到同一元素中。如果该工具发现两个清单包含相同的属性,但值不同,就会发生合并冲突。
表 1 描述了合并工具尝试将所有属性组合到同一元素中时可能出现的结果。
高优先级属性 | 低优先级属性 | 属性的合并结果 |
---|---|---|
没有值 | 没有值 | 没有值(使用默认值) |
值 B | 值 B | |
值 A | 没有值 | 值 A |
值 A | 值 A | |
值 B | 冲突错误 - 您必须添加合并规则标记。 |
不过,在某些情况下,合并工具会采取其他行为方式以避免合并冲突:
<manifest>
元素中的属性绝不会合并在一起,只会使用优先级最高的清单中的属性。<uses-feature>
和<uses-library>
元素中的android:required
属性使用 OR 合并。如果发生冲突,系统将应用"true"
并始终包含一个清单所需的功能或库。<uses-sdk>
元素中的属性始终使用优先级较高的清单中的值,但以下情况除外:- 如果优先级较低的清单中的
minSdk
值较高,那么除非您应用overrideLibrary
合并规则,否则将会发生错误。 - 当优先级较低的清单的
targetSdkVersion
值较低时,合并工具会使用优先级较高的清单中的值,还会添加所有必要的系统权限,以确保导入的库仍可继续正常工作(适用于更高 Android 版本具有更多权限限制的情况)。如需详细了解此行为,请参阅有关隐式系统权限的部分。
- 如果优先级较低的清单中的
- 绝不会在清单之间匹配
<intent-filter>
元素。每个该元素都被视为唯一的元素,并添加到合并后的清单中的共同父元素中。
对于属性之间的其他所有冲突,您将收到一条错误,并且必须指示合并工具如何解决该错误,方法是在优先级较高的清单文件中添加一个特殊属性。请参阅下面有关合并规则标记的部分。
不依赖于默认属性值。由于所有唯一属性会组合到同一元素中,因此如果优先级较高的清单实际上依赖于某个属性的默认值而不声明该属性,则可能会导致意外结果。例如,如果优先级较高的清单不声明 android:launchMode
属性,则会使用默认值 "standard"
;但如果优先级较低的清单声明此属性具有不同的值,该值将应用于合并后的清单(替换默认值)。您应将每个属性明确定义为您希望使用的值。清单参考文档中介绍了每个属性的默认值。
合并规则标记
合并规则标记是一个 XML 属性,可用于指定您对如何解决合并冲突或移除不需要的元素和属性的偏好。您可以对整个元素应用标记,也可以只对元素中的特定属性应用标记。
合并两个清单文件时,合并工具会在优先级较高的清单文件中查找这些标记。
所有标记都属于 Android tools
命名空间,因此您必须先在 <manifest>
元素中声明此命名空间,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp" xmlns:tools="http://schemas.android.com/tools">
节点标记
如需对整个 XML 元素(给定清单元素中的所有属性及其所有子标记)应用合并规则,请使用以下属性:
tools:node="merge"
- 在没有冲突的情况下,使用合并冲突启发法合并此标记中的所有属性以及所有嵌套元素。这是元素的默认行为。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:windowSoftInputMode="stateUnchanged"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
高优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" tools:node="merge"> </activity>
合并后的清单结果:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" android:windowSoftInputMode="stateUnchanged"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
tools:node="merge-only-attributes"
- 仅合并此标记中的属性,不要合并嵌套元素。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:windowSoftInputMode="stateUnchanged"> <intent-filter> <action android:name="android.intent.action.SEND" /> <data android:type="image/*" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
高优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" tools:node="merge-only-attributes"> </activity>
合并后的清单结果:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" android:windowSoftInputMode="stateUnchanged"> </activity>
tools:node="remove"
- 从合并后的清单中移除此元素。适用于以下情形:您发现合并后的清单中有不需要的元素,而且该元素是由不受您控制的优先级较低的清单文件(比如导入的库)提供的。
低优先级清单:
<activity-alias android:name="com.example.alias"> <meta-data android:name="cow" android:value="@string/moo"/> <meta-data android:name="duck" android:value="@string/quack"/> </activity-alias>
高优先级清单:
<activity-alias android:name="com.example.alias"> <meta-data android:name="cow" tools:node="remove"/> </activity-alias>
合并后的清单结果:
<activity-alias android:name="com.example.alias"> <meta-data android:name="duck" android:value="@string/quack"/> </activity-alias>
tools:node="removeAll"
- 与
tools:node="remove"
类似,但它会移除与此元素类型匹配的所有元素(同一父元素内)。低优先级清单:
<activity-alias android:name="com.example.alias"> <meta-data android:name="cow" android:value="@string/moo"/> <meta-data android:name="duck" android:value="@string/quack"/> </activity-alias>
高优先级清单:
<activity-alias android:name="com.example.alias"> <meta-data tools:node="removeAll"/> </activity-alias>
合并后的清单结果:
<activity-alias android:name="com.example.alias"> </activity-alias>
tools:node="replace"
- 完全替换优先级较低的元素。也就是说,如果优先级较低的清单中有匹配的元素,会将其忽略并完全按照此元素在此清单中显示的样子使用它。
低优先级清单:
<activity-alias android:name="com.example.alias"> <meta-data android:name="cow" android:value="@string/moo"/> <meta-data android:name="duck" android:value="@string/quack"/> </activity-alias>
高优先级清单:
<activity-alias android:name="com.example.alias" tools:node="replace"> <meta-data android:name="fox" android:value="@string/dingeringeding"/> </activity-alias>
合并后的清单结果:
<activity-alias android:name="com.example.alias"> <meta-data android:name="fox" android:value="@string/dingeringeding"/> </activity-alias>
tools:node="strict"
- 每当优先级较低的清单中的此元素与优先级较高的清单中的相应元素不完全匹配时,都会导致构建失败(除非已通过其他合并规则标记解决)。这会替换合并冲突启发式算法。例如,如果优先级较低的清单包含一个额外属性,构建会失败(尽管默认行为是将这个额外属性添加到合并后的清单中)。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:windowSoftInputMode="stateUnchanged"> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
高优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" tools:node="strict"> </activity>
这会导致清单合并错误。在严格模式下,这两个清单元素不能有任何不同。您必须应用其他合并规则标记解决这些差异。(如果没有
tools:node="strict"
,这两个文件可以合并到一起而不会出错,如tools:node="merge"
的示例所示。)
属性标记
如需只对清单标记中的特定属性应用合并规则,请使用以下属性。每个属性可接受一个或多个属性名称(包括属性命名空间),属性名称之间用英文逗号分隔。
tools:remove="attr, ..."
- 从合并后的清单中移除指定属性。
适用于以下情形:优先级较低的清单文件包含这些属性,而且您希望确保它们不会被纳入合并后的清单中。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:windowSoftInputMode="stateUnchanged">
高优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" tools:remove="android:windowSoftInputMode">
合并后的清单结果:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait">
tools:replace="attr, ..."
- 将优先级较低的清单中的指定属性替换为此清单中的属性。换句话说,始终保留优先级较高的清单中的值。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:theme="@oldtheme" android:exported="false" android:windowSoftInputMode="stateUnchanged">
高优先级清单:
<activity android:name="com.example.ActivityOne" android:theme="@newtheme" android:exported="true" android:screenOrientation="portrait" tools:replace="android:theme,android:exported">
合并后的清单结果:
<activity android:name="com.example.ActivityOne" android:theme="@newtheme" android:exported="true" android:screenOrientation="portrait" android:windowSoftInputMode="stateUnchanged">
tools:strict="attr, ..."
- 每当优先级较低的清单中的这些属性与优先级较高的清单中的相应属性不完全匹配时,都会导致构建失败。这是所有属性的默认行为,但具有特殊行为的属性除外,如合并冲突启发法中所述。
低优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="landscape"> </activity>
高优先级清单:
<activity android:name="com.example.ActivityOne" android:screenOrientation="portrait" tools:strict="android:screenOrientation"> </activity>
这会导致清单合并错误。您必须应用其他合并规则标记解决冲突。这是默认行为,因此明确添加
tools:strict="screenOrientation"
会产生相同的结果。
您也可对 1 个元素应用多个标记,如下例所示:
低优先级清单:
<activity android:name="com.example.ActivityOne" android:theme="@oldtheme" android:exported="false" android:allowTaskReparenting="true" android:windowSoftInputMode="stateUnchanged">
高优先级清单:
<activity android:name="com.example.ActivityOne" android:theme="@newtheme" android:exported="true" android:screenOrientation="portrait" tools:replace="android:theme,android:exported" tools:remove="android:windowSoftInputMode">
合并后的清单结果:
<activity android:name="com.example.ActivityOne" android:theme="@newtheme" android:exported="true" android:allowTaskReparenting="true" android:screenOrientation="portrait">
标记选择器
如果您想只对导入的某个特定库应用合并规则标记,请添加带有库软件包名称的 tools:selector
属性。
例如,对于下面的清单,只有在优先级较低的清单文件来自 com.example.lib1
库时,才会应用 remove
合并规则:
<permission android:name="permissionOne" tools:node="remove" tools:selector="com.example.lib1">
如果优先级较低的清单来自其他任何来源,则会忽略 remove
合并规则。
注意:如果将此属性与其中一个属性标记配合使用,则它会应用到该标记中指定的所有属性。
替换导入的库的 <uses-sdk>
默认情况下,导入 minSdk
值高于主清单文件的库时会出错,而且无法导入该库。
如需使合并工具忽略此冲突并导入库,同时保留应用的较低 minSdk
值,请将 overrideLibrary
属性添加到 <uses-sdk>
标记。属性值可以是一个或多个库软件包名称(用英文逗号分隔),指明可替换主清单的 minSdk
的库。
例如,如果应用的主清单按如下方式应用 overrideLibrary
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app" xmlns:tools="http://schemas.android.com/tools"> <uses-sdk tools:overrideLibrary="com.example.lib1, com.example.lib2"/> ...
则以下清单可以合并,而不会出现与 <uses-sdk>
标记相关的错误,合并后的清单将保留应用清单中的 minSdk="2"
。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.lib1"> <uses-sdk android:minSdk="4" /> ...
隐式系统权限
一些曾经可由应用自由访问的 Android API 在最新的 Android 版本中受到了系统权限的限制。
为避免中断预期会访问这些 API 的应用,最新的 Android 版本允许应用在无权限的情况下继续访问这些 API,条件是 targetSdkVersion
设为低于添加了限制的版本的值。此行为会向应用授予隐式权限,以允许它们访问这些 API。如果合并后的清单具有不同的 targetSdkVersion
值,则可能会受到影响。
如果优先级较低的清单文件具有较低的 targetSdkVersion
值,因而为其提供了一项隐式权限,但优先级较高的清单不具备相同的隐式权限(因为它的 targetSdkVersion
等于或高于添加限制的版本),则合并工具会向合并后的清单明确添加相应的系统权限。
例如,如果应用将 targetSdkVersion
设为 4 或更高的值,但导入的库将 targetSdkVersion
设为 3 或更低的值,则合并工具会向合并后的清单添加 WRITE_EXTERNAL_STORAGE
权限。
表 2 列出了可以向合并后的清单添加的所有可能的权限:
优先级较低的清单声明 | 向合并后的清单添加的权限 |
---|---|
targetSdkVersion 为 3 或更低的值 |
WRITE_EXTERNAL_STORAGE 、READ_PHONE_STATE |
targetSdkVersion 为 15 或更低的值,并且使用 READ_CONTACTS |
READ_CALL_LOG |
targetSdkVersion 为 15 或更低的值,并且使用 WRITE_CONTACTS |
WRITE_CALL_LOG |
检查合并后的清单并查找冲突
甚至在构建应用之前,您就能预览合并后的清单会是什么样。若要查看预览,请执行以下操作:
- 在 Android Studio 中,打开
AndroidManifest.xml
文件。 - 点击编辑器底部的 Merged Manifest 标签页。
在“合并后的清单”视图中,左侧会显示合并后的清单结果,右侧会显示每个合并后的清单文件的相关信息,如图 2 所示。
从优先级较低的清单文件中合并的元素在左侧以不同的颜色突出显示。每种颜色的键在 Manifest Sources 下方指定。
属于 build 的一部分但未贡献元素或属性的清单文件列在 Other Manifest Files 下方。
如需查看有关元素来源的信息,只需在左侧窗格内点击相应元素,详细信息即会显示在 Merging Log 下方。
如果发生任何冲突,它们将显示在 Merging Errors 下方,并且包含有关如何使用合并规则标记解决冲突的建议。
错误也会显示在 Event Log 窗口中。若要查看这些错误,请依次选择 View > Tool Windows > Event Log。
若要查看合并决策树的完整日志,可以在模块的 build/outputs/logs/
目录中查找名为 manifest-merger-buildVariant-report.txt
的日志文件。
合并策略
清单合并工具可以在逻辑上将一个清单文件中的每个 XML 元素与另一个文件中的对应元素相匹配。合并工具会使用“匹配键”匹配每个元素,匹配键可以是唯一的属性值(比如 android:name
),也可以是标记本身的自然唯一性(例如,只能有 1 个 <supports-screen>
元素)。
如果两个清单具有相同的 XML 元素,则该工具会采用三种合并政策中的一种,将这两个元素合并在一起:
- 合并
- 将所有没有冲突的属性组合到同一标记中,并按各自的合并政策合并子元素。如果任何属性相互冲突,则使用合并规则标记将它们合并在一起。
- 仅合并子元素
- 不组合或合并属性(仅保留优先级最高的清单文件提供的属性),并按各自的合并政策合并子元素。
- 保留
- 将元素“按原样”保留,并将其添加到合并后的文件中的共同父元素中。只有在可以接受同一元素有多个声明时,才会采用此政策。
表 3 列出了每种元素类型、所采用的合并政策类型,以及用于确定两个清单之间元素匹配的键:
元素 | 合并政策 | 匹配键 |
---|---|---|
<action>
|
合并 | android:name 属性
|
<activity>
|
合并 | android:name 属性
|
<application>
|
合并 | 每个 <manifest> 只有 1 个。
|
<category>
|
合并 | android:name 属性
|
<data>
|
合并 | 每个 <intent-filter> 只有 1 个。
|
<grant-uri-permission>
|
合并 | 每个 <provider> 只有 1 个。
|
<instrumentation>
|
合并 | android:name 属性
|
<intent-filter>
|
保留 | 不匹配;允许父元素内的多个声明。 |
<manifest>
|
仅合并子元素 | 每个文件只有 1 个。 |
<meta-data>
|
合并 | android:name 属性
|
<path-permission>
|
合并 | 每个 <provider> 只有 1 个。
|
<permission-group>
|
合并 | android:name 属性
|
<permission>
|
合并 | android:name 属性
|
<permission-tree>
|
合并 | android:name 属性
|
<provider>
|
合并 | android:name 属性
|
<receiver>
|
合并 | android:name 属性
|
<screen>
|
合并 | android:screenSize 属性
|
<service>
|
合并 | android:name 属性
|
<supports-gl-texture>
|
合并 | android:name 属性
|
<supports-screen>
|
合并 | 每个 <manifest> 只有 1 个。
|
<uses-configuration>
|
合并 | 每个 <manifest> 只有 1 个。
|
<uses-feature>
|
合并 | android:name 属性(如果不存在,则使用 android:glEsVersion 属性)
|
<uses-library>
|
合并 | android:name 属性
|
<uses-permission>
|
合并 | android:name 属性
|
<uses-sdk>
|
合并 | 每个 <manifest> 只有 1 个。
|
自定义元素 | 合并 | 不匹配;合并工具并不知晓这些元素,因此它们始终包含在合并后的清单中。 |
将 build 变量注入清单
如果您需要将变量插入在 build.gradle
文件中定义的 AndroidManifest.xml
文件,可以使用 manifestPlaceholders
属性执行此操作。此属性采用键值对的映射,如下所示:
Groovy
android { defaultConfig { manifestPlaceholders = [hostName:"www.example.com"] } ... }
Kotlin
android { defaultConfig { manifestPlaceholders["hostName"] = "www.example.com" } ... }
然后,您可以将某个占位符作为属性值插入清单文件:
<intent-filter ... >
<data android:scheme="https" android:host="${hostName}" ... />
...
</intent-filter>
默认情况下,构建工具还会在 ${applicationId}
占位符中提供应用的应用 ID。该值始终与当前 build 的最终应用 ID(包括 build 变体的应用 ID 更改)一致。当您要对标识符(如 intent 操作)使用唯一的命名空间时,这很有用,即使要求在 build 变体之间保持唯一性,这也很有用。
例如,如果您的 build.gradle
文件如下所示:
Groovy
android { defaultConfig { applicationId "com.example.myapp" } flavorDimensions "type" productFlavors { free { applicationIdSuffix ".free" dimension "type" } pro { applicationIdSuffix ".pro" dimension "type" } } }
Kotlin
android { defaultConfig { applicationId = "com.example.myapp" } flavorDimensions += "type" productFlavors { create("free") { applicationIdSuffix = ".free" dimension = "type" } create("pro") { applicationIdSuffix = ".pro" dimension = "type" } } }
那么,您可以按如下方式在清单中插入应用 ID:
<intent-filter ... >
<action android:name="${applicationId}.TRANSMOGRIFY" />
...
</intent-filter>
当您构建“free”产品变种时,清单结果如下所示:
<intent-filter ... >
<action android:name="com.example.myapp.free.TRANSMOGRIFY" />
...
</intent-filter>
如需了解详情,请阅读设置应用 ID。