Android 微技巧之清单文件合并那些事儿

Android 微技巧之清单文件合并那些事儿_第1张图片
微技巧

在编译 Android 项目过程中,相信大家肯定会遇到过这样一个问题:

Manifest merger failed ...

AndroidManifest.xml 清单文件合并失败,这个时候可以用下面的命令查看到底是什么东西冲突了?

gradlew processDebugManifest --stacktrace

示例如下:

> Task :app:processDebugManifest FAILED
D:\aspjs\kinland\app\src\main\AndroidManifest.xml:14:5-67 Warning:
Element uses-permission#android.permission.INTERNET at    AndroidManifest.xml:14:5-67 duplicated with element declared at AndroidManifest.xml:6:5-67
D:\aspjs\kinland\app\src\main\AndroidManifest.xml:25:9-47 Error:
    Attribute application@icon value=(@mipmap/ic_launcher_new) from AndroidManifest.xml:25:9-47
    is also present at [com.github.**:v1.0.1] AndroidManifest.xml:13:9-43 value=(@mipmap/ic_launcher).
    Suggestion: add 'tools:replace="android:icon"' to  element at AndroidManifest.xml:22:5-125:19 to override.
D:\aspjs\kinland\app\src\main\AndroidManifest.xml:27:9-58 Error:
    Attribute application@roundIcon value=(@mipmap/ic_launcher_round_new) from AndroidManifest.xml:27:9-58
    is also present at [com.github.**] AndroidManifest.xml:15:9-54 value=(@mipmap/ic_launcher_round).
    Suggestion: add 'tools:replace="android:roundIcon"' to  element at AndroidManifest.xml:22:5-125:19 to override.

See http://g.co/androidstudio/manifest-merger for more information about the manifest merger.

日志上可以看出来有两个字段冲突了,并且给出了提示,这里直接加上就好了,两个字段的话中间用逗号分隔

tools:replace="android:icon,android:roundIcon"

导致合并失败的原因是,每个 APK 文件只能包含一个 AndroidManifest.xml 文件,但 Android Studio 项目可以包含多个清单文件,这里指的是模块化开发、导入其他库等,此时在项目工程就会包含多个 AndroidManifest.xml 文件。因此在编译您的应用时,Gradle 编译系统会将所有清单文件合并打包在 APK 中的清单文件中。

清单合并工具通过遵守某些合并启发式算法,并遵守您使用特殊 XML 属性定义的合并编号设置,来合并各个文件中的所有 XML 元素。该篇文章主要介绍清单合并的工作方式以及如何应用合并每个偏好设置来解决合并冲突

合并优先级

合并工具会根据每个清单文件的优先级按照顺序合并,将所有清单文件组合到一个文件中。例如,如果您有三个清单文件,则会先将优先级最低的清单合并到优先级第二高的清单中,然后再将合并后的清单合并到优先级最高的清单中,然后再将合并后的清单合并到优先级最高的清单中。

Android 微技巧之清单文件合并那些事儿_第2张图片
图中,合并三个清单文件的流程,从优先级最低的清单文件(左)合并到优先级最好的清单文件(右)中

有三种基本的清单文件可以互相合并,它们的合并优先级如下(按优先级由高到低的顺序):

  1. 编译变体的清单文件

如果您的变体有多个源集,则其清单优先级如下:

  • a. 编译变体清单(如 src/demoDebug/)
  • b. 版本类型清单(如 src/debug/)
  • c. 版本类型清单(如 src/demo/)

如果您使用的是类型维度,则清单优先级与每个维度在 flavorDimensions 属性中的列示顺序(按优先级由高到低的顺序)对应。

  1. 应用模块的主清单文件
  2. 所包含的库中的清单文件

如果您有多个库,则其清单优先级与依赖顺序(库出现在 Gradle dependencies 块中的顺序)匹配。例如,先将库清单合并到主清单中,然后再将主清单合并到编译变体清单中。

⭐️ 注意:这些是对所有源集都相同的合并优先级,如使用源集编译中所述。
  • 重要提示:build.gradle 文件中的编译配置将替换合并后的清单文件中的所有对应属性。例如,build.gradle 文件中的 minSdkVersion 将替换 清单元素中的匹配属性。为了避免混淆,您只需要省去 元素在 build.gradle 文件中定义这些属性。如需要了解详情,请参阅配置编译系统。
合并冲突启发式算法

合并工具可以在逻辑上将一个清单中的每个 XML 元素与清单中的对应元素相匹配。

如果优先级较低的清单中的某个元素与优先级较高的清单中的任何元素都不匹配,则会该将元素添加到合并后的清单。不过,如果有匹配的元素,则合并工具会尝试将每个元素的所有属性组合到同一元素中。如果该工具发现两个清单包含相同的属性,但值不同,则会发生合并冲突

看下合并过程中,尝试将所有属性组合到同一元素时可能出现的结果。

高优先级属性 低优先级属性 属性的合并结果
没有值 没有值 没有值(使用默认值)
没有值 值 B 值 B
值 A 没有值 值 A
值 A 值A 值 A
值 A 值 B 冲突错误-必须添加合并规则标记

不过,在某些情况下,合并工具会采取其他行为方式以避免合并冲突:

  • 元素中的属性绝不会合并在一起 - 仅使用优先级最高的清单中的属性。

  • 元素中 android required 属性使用 OR 合并,这样依赖,如果发生冲突,系统将应用 "true" 并始终包含某个清单所需的功能或库。

  • 元素中的属性始终使用优先级较高的清单中的值,但以下情况除外:

    • 如果优先级较低清单的 minSdkVersion 值较高,除非您用 overrideLibrary 合并规则,否则会发生错误。

    • 如果优先级较低的清单的 targetSdkVersion 值较低,合并工具将使用优先级较高的清单中的值,但也会添加所有必要的系统权限,以确保所导入的库继续正常工作(适用于较高的 Android 版本具有更多权限限制的情况)。如需详细了解此行为,请参阅有关隐私系统权限的部分。

  • 绝不会在清单之间匹配 元素。每个该元素都被视为唯一的元素,并添加到合并后的清单中共同的父元素。

对于属性之间的其他所有冲突,您将收到一条错误,并且必须通过在优先级较高的清单文件中添加一个特殊属性来指示合并工具如何解决此错误

  • 不依赖于默认属性值。由于所有唯一属性都组合到同一元素中,因此如果优先级较高的清单实际上依赖于某个属性的默认值而不声明该属性,则可能会导致意外结果。例如,如果优先级较高的清单不声明 android:launchMode 属性,则会使用默认值 "standard";但如果优先级较低的清单声明此属性具有其他值,则该值将应用于合并后的清单(替换默认值)。因此,您应该安期望明确定义每个属性。
合并规则标记

合并规则标记是一个 XML 属性,可用于表达您对如何解决合并冲突或移除不需要的元素和属性偏好。您可以对整个元素应用标记,也可以只对元素中的特定属性应用标记。

合并两个清单文件时,合并工具会在优先级较高的清单中查找这些标记。

所有标记都属于 Android tools 命名空间,因此您必须现在 元素中声明此命名空间,如下所示:


节点标记

要向整个 XML 元素(给定清单元素中的所有属性及其所有子标记)应用合并规则,请使用以下下属性:

  • tools:node="merge"

    如果使用合并冲突启发式算法时没有冲突,则合并此标记中的所有属性以及所有嵌套元素。这是元素的默认行为

低优先级清单


    
        
        
    

高优先级清单



合并后的清单结果


    
        
        
    

  • tools:node="merge-only-attributes"

仅合并此标记中的属性,不合并嵌套元素。

低优先级清单:


    
        
        
        
    

高优先级清单:



合并后的清单结果:



  • tools:node="remove"

从合并后的清单中移除此元素。虽然您似乎应该只删除此元素,但如果您发现合并后的清单中有不需要的元素,而且该元素是由不受您控制的优先级较低的清单文件(如导入的库)提供的,则必须使用此属性。

低优先级清单


  
  

高优先级清单


  

合并后的清单结果


  

  • tools:node="removeAll"

与 tools:node="remove" 类似,但它会移除与此元素类型匹配的所有元素(同一父元素内)。

低优先级清单


  
  

高优先级清单


  

合并后的清单结果



  • tools:node="replace"

完全替换优先级较低的元素。也就是说,如果优先级较低的清单中有匹配的元素,会将其忽略并完全按照此元素在此清单中显示的样子来使用它。

低优先级清单


  
  

高优先级清单


  

合并后的清单结果


  

  • tools:node="strict"

每当此元素在优先级较低的清单中与在优先级较高的清单中不完全匹配时,都会导致编译失败(除非已通过其他合并规则标记解决)。这将替换合并冲突启发式算法。例如,如果优先级较低的清单只是包含一个额外的属性,则编译将会失败(而默认行为会将该额外属性添加到合并后的清单)。

低优先级清单


    
        
        
    

高优先级清单



这回生成清单合并错误。这两个清单元素在严格模式下完全无法区分。因此,您必须应用其他合并规则标记来解决这些差异。通常,这两个元素会很好地合并在一起,如上面的 tools:node="merge"示例中所示。

属性标记

要仅对清单标记中的特定属性应用合并规则,请使用以下属性。每个属性接受一个或多个属性名称(包括属性命名空间),并且属性名称用英文逗号分隔。

  • tools:remove="attr, ..."

从合并后的清单中移除指定属性。虽然您似乎可以只删除这些属性,但如果优先级较低的清单文件不包含这些属性,而且您希望确保不将它们纳入合并后的清单,则必须使用此属性。

低优先级清单


高优先级清单

    

合并后的清单结果


  • tools:replace="attr, ..."

将优先级较低的清单中的指定属性替换为此清单中的属性。换句话说,始终保留优先级较高的清单的值。

低优先级清单


高优先级清单


合并后的清单结果


  • tools:strict="attr, ..."

每当这些属性在优先级较低的清单中与在优先级较高的清单中不完全匹配时,都会导致编译失败。这是所有属性的默认行为,但具有特殊行为的属性除外,如合并冲突启发式算法中所述。

低优先级清单



高优先级清单



这会生成清单合并错误。您必须应用其他合并规则标记来解决冲突。(切记:这是默认行为,因此如果您移除 tools:strict="screenOrientation" 上面的示例将具有相同的结果)

您也可以对一个元素应用多个标记,如下所示。

低优先级清单


高优先级清单


合并后的清单结果


标记选择器

如果要仅对导入的特定库应用合并规则标记,请添加带有库软件包名称的 tools:selector 属性。

例如,对于下面的清单,只有在优先级较低的清单文件来自 com.example.lib1 库是,才会应用 remove 合并规则。


如果优先级较低的清单来自其他任何来源,系统将会忽律 remove 合并规则。

☆ 注意:如果将此属性与某个属性标记一起使用,则它会应用于该标记中指定的所有属性。
替换导入的库的

默认情况下,导入 minSdkVersion 值高于主清单文件的库时会出错,而且无法导入该库。要使合并工具忽略此冲突并导入库,同时保留应用的较低 minSdkVersion 值,请将 overrideLibrary 属性添加到 标记。属性值可以是一个或多个库软件包名称(用英文逗号分隔),指明可以替换主清单的 minSdkVersion 的库。

例如,如果应用的主清单按如下方式应用 overrideLibrary:


  
...

则以下清单可以合并,而不会出现与 标记相关的错误,合并后的清单将保留应用清单中的 minSdkVersion="2"。


   
...
隐士系统权限

一些曾经可由应用自身访问的 Android API 在最新的 Android 版本中受到了系统权限的限制。为了避免中断预期会访问这些 API 的应用,最新的 Android 版本允许应用在无权限的情况下继续访问这些 API,前提是它们已经 targetSdkVersion 设为低于添加限制的版本值。此行为会有效的向应用授予隐式权限,以允许访问这些 API。因此这可能会对具有不同 targetSdkVersion 值的合并后的清单产生以下影响。

如果优先级较低的清单文件具有较低的 targetSdkVersion 值,因而为其提供了一项隐士权限,但有限级较高的清单不具备相同的隐士权限(因为它的 targetSdkVersion 等于或高于添加限制的版本),则合并工具互相合并后的清单明确添加相应的系统权限。

例如,如果您的应用将 targetSdkVersion 设为 4 或更高的值,但导入的某个库将 targetSdkVersion 设为 3 或更低的值,则合并工具会向合并后的清单添加 WRITE_EXTERNAL_STORAGE 权限。表 2 列出了可以向合并后的清单添加的所有可能的权限。

☆ 注意:如果您已将应用的 targetSdkVersion 设为 23 或更高的值,那么当应用视图访问受任何危险权限保护的 API 时,您必须对这些权限执行运行时权限请求。

合并工具可以向合并后的清单添加的权限的列表

优先级较低的清单声明 向合并后的清单添加的权限
targetSdkVersion 为 3 或更低的值 WRITE_EXTERNAL_STORAGE、READ_PHONE_STATE
targetSdkVersion 为 15 或更低的值,并且使用 READ_CONTACTS READ_CALL_LOG
targetSdkVersion 为 15 或更低的值,并且使用 WRITE_CONTACTS WRITE_CALL_LOG
检查合并后的清单并查找冲突

您可以预览合并后的清单是什么样子,甚至是编译 APK 之前就能预览,方法是在 Android Studio 中打开您的 AndroidManifest.xml 文件,然后点击编辑器底部的 Merged Manifest 标签。

在 "Merged Manifest" 视图中,左侧显示合并后的清单结果,右侧显示每个合并后的清单文件的相关信息,从优先级较低的清单文件中合并的元素在左侧以不同的颜色突出显示。每种颜色的键在右侧的 Manifest Sources 下方指定。

Android 微技巧之清单文件合并那些事儿_第3张图片
Merged Manifest 视图

属于编译的一部分但不构成元素或属性的清单文件列在右侧的 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),也可以是标记本身的自然唯一性(例如,只能有一个 元素)。如果两个清单具有相同的 XML 元素,则该工具会采用三种合并策略中的一种,将这两个元素合并在一起。

合并

将所有非冲突属性组合到同一标记中,并按各自的合并策略合并子元素。如果任何属性相互冲突,使用上面介绍的合并规则标记将它们合并在一起。

仅合并子元素

不组合或合并属性(仅保留优先级最高的清单文件提供的属性),并按各自的合并策略合并子元素。

保留

将元素“按原样”保留,并将其添加到合并后的文件中的共同父元素。只有在可接受同一元素的多个声明时,才会采用此策略。

下表列出了每种元素类型、使用的合并策略类型,以及用于确定两个清单之间的元素匹配的键。

元素 合并策略 匹配键
合并 android:nmae 属性
合并 android:name 属性
合并 每个 只有一个
合并 android:name 属性
合并 每个 只有一个
合并 每个 只有一个
合并 android:name 属性
保留 不匹配;允许父元素内的多个声明
仅合并子元素 每个文件只有一个
合并 android:name 属性
合并 每个 只有一个
合并 android:name 属性
合并 android:name 属性
合并 android:name 属性
合并 android:name 属性
合并 android:name 属性
合并 android:screenSize 属性
合并 android:name 属性
合并 android:name 属性
合并 每个 只有一个
合并 每个 只有一个
合并 android:name 属性(如果不存在,则使用 android:glEsVersion 属性)
合并 android:name 属性
合并 android:name 属性
合并 每个 只有一个
自定义元素 合并 不匹配;合并工具并不知晓这些元素,因此它们始终包含在合并后的清单中

你可能感兴趣的:(Android 微技巧之清单文件合并那些事儿)