AndroidManifest 合併衝突處理

0. 前言

AndroidStuido 採用模塊化構建工程的方式,每個模塊配置一個 AndroidManifest.xml ,甚至每個構建類型、產品特性都可以配置一個 AndroidManifest.xml。最終生成 apk 的時候,按照下圖指定的優先級進行合併處理。(圖片來源:Google 官方文檔)

1. 優先級定義

上圖介紹了三種清單文件的合併流程,從左到右,優先級由低到高。

1.1 構建變體清單文件

構建變體清單,主要包含兩種:buildType 和 productFlavor 。

優先級由高到低依次是:

  1. 產品特性(productFlavors)。
  2. 構建類型 (buildType)。
  3. 編譯變體(前兩者組合)。
    buildTypes {
        // 測試版本
        debug {
        }
        // 發行版本
        release {
        }
    }

    flavorDimensions 'stage', 'api'

    productFlavors {
        // 開發階段
        dev {
            dimension 'stage'
        }
        // 生產階段
        pro {
            dimension 'stage'
        }

        minApi21 {
            dimension 'api'
        }
        minApi23 {
            dimension 'api'
        }
        minApi26 {
            dimension 'api'
        }
    }

分別建立對應的編譯變體目錄和 AndroidManifest.xml 文件。

以上面的配置爲例,優先級依次是(高到低的順序):

dev -> minApi23 -> devMinApi23 -> debug -> devMinApi23Debug

  • dev 高於 minApi23 是因爲 flavorDimensions 聲明 stage 優先於 api 。
  • 不存在 devDebug 和 minApi23Debug 這樣的組合。

1.2 應用主模塊清單文件

主模塊中的清單文件,即 main 目錄下的 AndroidManifest.xml 文件。

1.3 庫模塊及依賴庫清單文件

依賴庫指依賴本地的 aar 文件或依賴遠程 maven 倉庫的 aar 文件。它們通常都包含一個 AndroidManifest.xml 文件。

jar 類型的三方庫,只有 class 文件,不在此討論範圍。

與主模塊相對立,庫模塊也可以包含多個構建類型和產品特性。首先,它們先按照 構建變體清單文件 定義的優先級,合併出自身的一個 AndroidManifest.xml ,再作爲庫模塊的清單文件與主模塊的清單文件合併。

2. 隱式系統權限

早期 Android 版本中,應用可以自由訪問的 API ,在新版本中受到系統權限的限制。爲兼容這些應用,新版本中會允許這些應用在無權限的情況下訪問這些受限的 API 。

WRITE_EXTERNAL_STORAGE 最早出現在 Android API 4 中。那麼,當你應用的 targetSdkVersion 設置成小於 4 時,也可以在無權限的情況下訪問 外部存儲

在 AndroidManifest 合併中,如果優先級低的 Manifest 中 targetSdkVersion 小於 4,那麼在 合併後的 AndroidManifest 中,會自動添加這些隱式權限。

下面兩張圖是合併前後,app/AndrodManifest.xmllibrary/AndroidManifest.xml 的對比。

發現 合併前 app/AndrodManifest.xml 中未申明權限,但是合併後多了三條權限記錄。

優先級較低的清單聲明 向合併後的的清單添加的權限
targetSdkVersion <= 3 WRITE_EXTERNAL_STORAGE
READ_EXTERNAL_STORAGE
READ_PHONE_STATE
targetSdkVersion <= 15
且使用 READ_CONTACTS
READ_CALL_LOG
targetSdkVersion <= 15
且使用 WRITE_CONTACTS
WRITE_CALL_LOG

隱式系統權限主要爲兼容 Android 早起版本。在 9102 年及以後的開發中並不多見。因此,瞭解即可。

3. 合併規則

借用官方的一張圖,介紹了在合併過程中,默認的一些合併方式。

以 dev 是高優先級,debug 是低優先級爲例:

<!-- dev 的 Manifest-->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:windowSoftInputMode="adjustPan">
    <meta-data
        android:name="dev_index"
        android:value="dev" />
</activity>
<!-- debug 的 Manifest-->
<activity android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:screenOrientation="portrait">
    <meta-data
        android:name="debug_index"
        android:value="debug" />
</activity>   
  1. dev 沒有值,debug 值 B,合併後值 B,android:screenOrientation=“portrait”
  2. dev 值 A,debug 沒有值,合併後值 A,android:windowSoftInputMode=“adjustPan”
  3. dev 值 A,debug 值 A,合併後值 A,android:exported=“false”

因此最終合併結果如下:

<!-- 合併後devDebug 的 Manifest-->
<activity android:name="com.flueky.lib.TestActivity"
    android:screenOrientation="portrait"
    android:exported="false"
    android:windowSoftInputMode="adjustPan">
    <meta-data
        android:name="dev_index"
        android:value="dev" />
    <meta-data
        android:name="debug_index"
        android:value="debug" />
</activity>  

上述提到的都屬性合併,但是 meta-data 標籤最終卻也合併了。標籤的合併,大家都熟悉的是 Activity 的合併。每個模塊的 Activity 只需要在該模塊的 AndroidManifst.xml 註冊,不需要在主模塊中再次註冊,因爲最終會將它們合併在一起。

標籤合併使用匹配鍵作爲依據,activity 使用 android:name 作爲匹配件,meta-data 也是。匹配鍵的屬性值不同,即可無衝突合併。

下表列出,manifest 全部子標籤的匹配鍵,做解決合併衝突的參考。

標籤 合併策略 匹配鍵
action 合併 android:name 屬性
activity 合併 android:name 屬性
application 合併 每個 manifest 只有一個
category 合併 android:name 屬性
data 合併 每個intent-filter 只有一個
grant-uri-permission 合併 每個 provider 只有一個
instrumentation 合併 android:name 屬性
intent-filter 保留 不匹配;允許父元素內的多個聲明
manifest 僅合併子元素 每個文件只有一個
meta-data 合併 android:name 屬性
path-permission 合併 每個 provider 只有一個
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 只有一個
uses-configuration 合併 每個 manifest 只有一個
uses-feature 合併 android:name 屬性
(如果不存在,則使用 android:glEsVersion 屬性)
uses-library 合併 android:name 屬性
uses-permission 合併 android:name 屬性
uses-sdk 合併 每個 manifest 只有一個
自定義元素 合併 不匹配;合併工具並不知曉這些元素,
因此它們始終包含在合併後的清單中

Android 中通過標記的方式,在合併過程中解決衝突。按照前面介紹的屬性合併和標籤合併,對應採用屬性標記和節點標記(標籤作爲節點)的方式人爲解決衝突。

需要使用命名空間 Android tools

xmlns:tools="http://schemas.android.com/tools"

4. 屬性標記

屬性標記有四個。默認標記、移除、替換、選擇器。

標記 作用
tools:strict 默認標記,無實際意義,需要使用 replace、remove 解決衝突
tools:replace 指定替換的屬性名字
tools:remove 指定移除的屬性名字
tools:selector 選擇指定模塊進行合併,結合 replace、remove 使用

這些例子比較簡單 ,借用下官方的示例代碼。

  1. replace 使用示例。
<!-- 低優先級 -->
<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">            
  1. remove 使用示例。
<!-- 低優先級 -->
<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">            
  1. selector 表示,只針對指定來源的低優先級 AndroidManifest.xml 進行合併處理。
<!-- 來源於 com.example.lib1 的低優先級 -->
<activity android:name="com.example.ActivityOne"
    android:windowSoftInputMode="stateUnchanged">
<!-- 高優先級 -->
<activity android:name="com.example.ActivityOne"
    android:screenOrientation="portrait"
    tools:remove="android:windowSoftInputMode"
    tools:selector="com.example.lib1">
<!-- 合併後 -->    
<activity android:name="com.example.ActivityOne"
    android:screenOrientation="portrait">            

5. 節點標記

節點標記使用 tools:node

屬性值 作用
merge 沒有發生衝突時合併標籤中所有屬性和所有嵌套元素,即是默認方式
merge-only-attributes 未正確驗證,估計已失效。
remove 合併後移除此標籤
removeAll 同 remove 類似,但是移除全部此標籤
replace 替換低優先級中相同的標籤
strict 使用時,遇到不匹配的標籤都導致合併失敗,需要使用上述屬性值解決

1. 下面的結果,即使默認方式合併後。
<!-- 合併後devDebug 的 Manifest-->
<activity android:name="com.flueky.lib.TestActivity"
    android:screenOrientation="portrait"
    android:exported="false"
    android:windowSoftInputMode="adjustPan">
    <meta-data
        android:name="dev_index"
        android:value="dev" />
    <meta-data
        android:name="debug_index"
        android:value="debug" />
</activity>  
  1. merge-only-attributes 與資料說明的不太一樣,未發現正確用途 。

  2. 使用 remove 後,移除匹配鍵相同的標籤。

<!-- 低優先級 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:screenOrientation="portrait">
    <meta-data
        android:name="debug_index"
        android:value="debug" />
</activity>
<!-- 高優先級 -->
<activity
<activity
    android:name="com.flueky.lib.TestActivity"
    android:windowSoftInputMode="adjustPan">
    <meta-data
        android:name="debug_index"
        tools:node="remove" />
</activity>
<!-- 合併後 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:windowSoftInputMode="adjustPan"
    android:screenOrientation="portrait">
</activity>    
  1. 使用 removeAll 後:
<!-- 低優先級 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:screenOrientation="portrait">
    <meta-data
        android:name="debug_index"
        android:value="debug" />
    <meta-data
        android:name="dev_index"
        android:value="dev" />    
</activity>
<!-- 高優先級 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:windowSoftInputMode="adjustPan">
    <meta-data
        tools:node="removeAll" />
</activity>
<!-- 合併後 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:exported="false"
    android:windowSoftInputMode="adjustPan"
    android:screenOrientation="portrait">
</activity>    

removeAll 移除全部相同標籤。

  1. 使用 replace 後,結果比較直觀。
<!-- 低優先級 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
<!-- 高優先級 -->
<activity
    android:name="com.flueky.lib.TestActivity"
    android:windowSoftInputMode="adjustPan"
    tools:node="replace">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>   
<!-- 合併後 --> 
<activity
    android:name="com.flueky.lib.TestActivity"
    android:windowSoftInputMode="adjustPan">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>       

使用 replace 合併後,直接按照高優先級中的配置。

6. 常見問題

集成第三方庫時,遇到過最多的問題是:庫的最小 sdk 版本小於項目的最小 sdk 版本。此時集成第三方庫時必定出現問題。

解決方法:

<!-- com.flueky.library 即是第三方庫的 packageName -->
<uses-sdk tools:overrideLibrary="com.flueky.library" />

最後,偷偷的說下,AndroidStudio 3.5.2 版本支持直接看合併後的 AndroidManifest.xml 文件。

源碼地址

覺得有用?那打賞一個唄。去打賞

個人主頁已經更新 ,歡迎收藏https://flueky.github.io/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章