Android Bundle包全過程詳解

1.認識Bundle

官方文檔:https://developer.android.com/guide/app-bundle

定義

Android App Bundle 是一種發(fā)布格式,其中包含您應用的所有經(jīng)過編譯的代碼和資源,它會將 APK 生成及簽名交由 Google Play 來完成。通俗理解就是,bundle是多個apk集合特殊格式,該集合內(nèi)的apk根據(jù)用戶需要安裝對應apk,每個apk代表特殊的功能模塊。

作用

Google Play 會使用您的 App Bundle 針對每種設備配置生成并提供經(jīng)過優(yōu)化的 APK,因此只會下載特定設備所需的代碼和資源來運行您的應用。即用戶只需下載最基礎的apk,剩余根據(jù)情景,按需下載,減少下載量,以此提高下載和迭代速度,提高用戶體驗。

Apk的分類

bundle是一種文件形式,通常后綴為".aab",通過bundle工具就能解壓成不同模塊的apk包集合,apk包主要分為資源包和Dynamic Feature(自適應功能包)。由于bundle本質(zhì)是讓用戶根據(jù)需要,下最少的資源包,因此衍生出對bundle資源分包和自適應包的具體實現(xiàn)。

image-20210507094849989.png

1.Base APK,可以認為是基礎版apk,即集成了基本功能,并且每個用戶都需要擁有的代碼模塊。

2.Configuration APK,大致分為以下三類,對應圖中底部三種APK包,分別是像素分辨率資源、cpu內(nèi)核分類、國際化語言。無論是Base APK還是Dynamic Feature APK,他們都擁有自身的Configuration APK,bundle包會根據(jù)設備的像素分辨率、cpu、當前使用語言,提供適配設備的精簡包。

3.Dynamic Feature APK,自適應功能APK包,簡單理解為在基礎包功能的基礎上,根據(jù)不同設備型號和不同用戶手中所需的設備,按需下載對應的APK包,避免全部設備類型包都下載。所以,F(xiàn)eature包是滿足base所有功能基礎前提的一種用于細分業(yè)務的拓展。

Bundle分包配置

場景:我需要bundle打包,但我希望所有語言包在安裝時就安裝好,以便我切換語言。

當用戶通過Google Play Store下載應用時,如果上架的是bundle包,那么就會根據(jù)當前手機配置過的語言,動態(tài)下載語言包,例如我本地手機配置過英語、中文??晌覒弥С值抡Z,用戶在安裝完包以后,通過系統(tǒng)切換到德語,那由于bundle在安裝的時候并沒有下載包,此時也不會動態(tài)去下載,因此無法切換成功德語的多語言適配。所以,我們需要在bundle生成時,在app/build.gradle 中進行配置,來實現(xiàn)我們所需的效果

android{
    bundle {
        language {
          //是否開啟語言分包,當為true在這里可以添加inclue ‘ch-ZH’,配置預設語言
          enableSplit = false
        }
        //分辨率分包
        density {
          enableSplit = true
        }
        //cpu內(nèi)核分包
        abi {
          enableSplit = true
          // include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
        }
}

2.Bundle工具的配置和使用

提問:如何通過bundle工具打出,包含我想要的資源包或Dynamic Feature包的aab呢?

新建所需的Module并配置其清單文件,利用play core核心庫輔助配置,然后使用bundle工具執(zhí)行bundle命令,生成aab包,最終通過壓縮工具校驗包內(nèi)的apk是否和期望一致。

官網(wǎng)下載地址:https://github.com/google/bundletool/releases

mac電腦安裝步驟

1.通過官網(wǎng)下載最新的對應的jar包,并把jar包修改為bundletool.jar

2.通過command+shift+.查看隱藏文件,找到Android Studio中的sdk目錄,并且在目錄下創(chuàng)建bundle-tool文件夾,將jar放入其中

3.在命令行中執(zhí)行chmod +x “你的jar包絕對路徑”獲取權限

4.通過Finder查看器,到個人用戶/usr文件下,找到“.bash.profile”,用文本打開,并寫入export PATH=PATH:$ANDROID_HOME/bundle-tool/,通過source .bash_profile保存

5.通過android studio的Terminal,首先先cd到你所打的bundle包目錄,隨后通過bundle命令測試,其中app-debug.abb,即我們cd目錄下的bundle包,最終會在該目錄生成apks包。bundle命令如果生效,則代表安裝完成,否則會提示找不到bundle工具包。

bundletool build-apks --bundle=app-debug.aab --output=app-debug.apks

bundle指令

官方官網(wǎng)指令地址:https://developer.android.google.cn/studio/command-line/bundletool?hl=zh-cn

舉例:

//bundletool build-apks 指令名稱
//--bundle=path bundle包輸入地址
//--output=path apks包輸出地址
bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

//--ks=path 密鑰庫地址
--ks=/MyApp/keystore.jks
//密鑰庫密碼
--ks-pass=pass:password
//--ks-key-alia= 簽名密鑰別名
--ks-key-alias=MyKeyAlias
//密鑰別名密碼
--key-pass=pass:password

1.Bundle指令標識表

--bundle=path 包輸入地址
--output=path apks包輸出地址
--overwrite 如果已經(jīng)在目錄下有輸出文件,output會提示以存在文件,需要要用此命令重寫
--aapt2=path 指定 AAPT2 的自定義路徑。 默認情況下,bundletool 包含自己的 AAPT2 版本。
--ks=path 指定用于為 APK 簽名的部署密鑰庫的路徑。此標記是可選的。如果您不添加此標記,bundletool 會嘗試使用調(diào)試簽名密鑰為您的 APK 簽名。
--ks-pass=pass:password--ks-pass=file:/path/to/file 指定密鑰庫的密碼。如果您指定純文本格式的密碼,請使用 pass: 限定該密碼。如果您要傳遞包含該密碼的文件的路徑,請使用 file: 限定該路徑。如果您使用 --ks 標記指定密鑰庫,而未指定 --ks-pass,那么 bundletool 會提示您從命令行輸入密碼。
--ks-key-alias=alias 指定要使用的簽名密鑰的別名。
--key-pass=pass:password--key-pass=file:/path/to/file 指定簽名密鑰的密碼。如果您指定純文本格式的密碼,請使用 pass: 限定該密碼。如果您要傳遞包含該密碼的文件的路徑,請使用 file: 限定該路徑。如果此密碼與密鑰庫的密碼相同,您可以省略此標記。
--connected-device 根據(jù)連接設備區(qū)分,把bundle包安裝到不同設備
--device-id=serial-number 如果您有多個已連接的設備,請使用此標記指定要部署應用的設備的序列 ID。
--device-spec=spec_json 使用此標記提供 .json 文件的路徑,該文件指定了您要針對其生成 APK 的設備配置。
--mode=universal 如果您希望 bundletool 只構建一個包含應用的所有代碼和資源的 APK,以使該 APK 與應用支持的所有設備配置兼容,請將模式設置為 universal。注意bundletool 僅包含功能模塊,這些模塊在通用 APK 中的對應清單中指定 <dist:fusing dist:include="true"/>。如需了解詳情,請參閱功能模塊清單。請注意,這些 APK 要比針對特定設備配置優(yōu)化過的 APK 更大。但是,這些 APK 更便于與內(nèi)部測試人員共享,例如想在多種設備配置上測試您的應用的測試人員。
--local-testing 使用此標志啟用 app bundle 進行本地測試。 在本地測試時,由于無需上傳到 Google Play 服務器,因此能夠?qū)崿F(xiàn)快速的迭代測試周期。 有關如何使用 --local-testing 標記測試模塊安裝的示例,請參閱在本地測試模塊的安裝。

2.Bundle功能指令

1.部署apks到設備中

bundletool install-apks --apks=/MyApp/my_app.apks

2.為當前連接設備生成自適應的一組apk包

//--connected-device標記功能
bundletool build-apks --connected-device 
//多設備連接需要指定設備id
--device-id=serial-id
--bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

3.獲取設備json文件以及使用json文件

//1.當沒有json文件,想通過設備獲取其適配的json
bundletool get-device-spec --output=/tmp/device-spec.json

//2.已有json文件,想讓該apks遵循json文件規(guī)則
bundletool build-apks --device-spec=/MyApp/pixel2.json
--bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

json文件規(guī)范

{
  //設備支持的cpu類型
  "supportedAbis": ["arm64-v8a", "armeabi-v7a"],
  //設備支持的語言類型
  "supportedLocales": ["en", "fr"],
  //設備的像素分辨率
  "screenDensity": 640,
  //設備的sdk版本
  "sdkVersion": 27
}

4.從已有的apks包中,提取一部分特定設備的apk

bundletool extract-apks
//當前完整的apks
--apks=/MyApp/my_existing_APK_set.apks
//不希望從bundle包再去打特定設備,直接從現(xiàn)有的apks抽取部分形成特定設備的apks包
--output-dir=/MyApp/my_pixel2_APK_set.apks
--device-spec=/MyApp/bundletool/pixel2.json

5.估算apks的大小

bundletool get-size total --apks=/MyApp/my_app.apks

實現(xiàn)本地測試

情景:希望本地就能測試apks包功能,不希望上架google play測試

為了實現(xiàn)這種測試情況,需要有以下前提

1.集成Google Play Core庫

官方地址:https://developer.android.google.cn/guide/playcore?hl=zh-cn#include_playcore

1.app/build.gradle配置

dependencies {
    // This dependency is downloaded from the Google’s Maven repository.
    // So, make sure you also include that repository in your project's build.gradle file.
    implementation 'com.google.android.play:core:1.10.0'

    // For Kotlin users also add the Kotlin extensions library for Play Core:
    implementation 'com.google.android.play:core-ktx:1.8.1'
    ...
}

2.開發(fā)環(huán)境配置要求

a.Android Studio4.0或更高版本

b.sdk playform版本29或更高

c.sdk管理器中的CMake和NDK版本下載

d.play-core-native-sdk-1.10.0.zip下載,https://dl.google.com/games/play/core/play-core-native-sdk-1.10.0.zip?hl=zh-cn,若果maven過了可忽略

e.app/build.gradle補充

apply plugin: 'com.android.application'

// Define a path to the extracted Play Core SDK files.
// If using a relative path, wrap it with file() since CMake requires absolute paths.
//如果使用sdk相對路徑要用file,否則直接填寫絕對路徑
def playcoreDir = file('../path/to/playcore-native-sdk')

android {
    defaultConfig {
        ...
        externalNativeBuild {
          //cmake使用
            cmake {
                // Define the PLAYCORE_LOCATION directive.
                arguments "-DANDROID_STL=c++_static",
                          "-DPLAYCORE_LOCATION=$playcoreDir"
            }
        }
      指定ndk支持的cpu類型
        ndk {
            // Skip deprecated ABIs. Only required when using NDK 16 or earlier.
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
    buildTypes {
        release {
            // Include Play Core Library proguard config files to strip unused code while retaining the Java symbols needed for JNI.
            //加上混淆規(guī)則,防止playcore核心庫代碼被混淆找不到指定文件
            proguardFile "$playcoreDir/proguard/common.pgcfg"
            proguardFile "$playcoreDir/proguard/per-feature-proguard-files"
            ...
        }
        debug {
            ...
        }
    }
    externalNativeBuild {
        cmake {
          //放在項目main目錄的CMakeLists.txt文件
            path 'src/main/CMakeLists.txt'
        }
    }
}

dependencies {
    // Use the Play Core AAR included with the SDK.
    //下載的aar包,可替換成maven庫
    implementation files("$playcoreDir/playcore.aar")
    ...
}

CMakeLists.txt文件內(nèi)容

cmake_minimum_required(VERSION 3.6)

...

# Add a static library called “playcore” built with the c++_static STL.
include(${PLAYCORE_LOCATION}/playcore.cmake)
add_playcore_static_library()

// In this example “main” is your native code library, i.e. libmain.so.
add_library(main SHARED
        ...)

target_include_directories(main PRIVATE
        ${PLAYCORE_LOCATION}/include
        ...)

target_link_libraries(main
        android
        playcore
        ...)

2.使用--local-testing標記

//--local-testing標記,申明本地
bundletool build-apks --local-testing
  --bundle my_app.aab
  --output my_app.apks
//直接安裝的包就是支持本地測試的包了
bundletool install-apks --apks my_app.apks

3.模擬play store的網(wǎng)絡錯誤情況

當通過--local-testing標記,并將其部署到測試設備后,可以在應用中調(diào)用play core庫中的FakeSplitInstallManager類,來模擬網(wǎng)絡請求連接錯誤。

示例:

// 通過FakeSplitInstallManagerFactory工廠類,傳入上下文,獲取到fakeSplitInstallManager
val fakeSplitInstallManager = FakeSplitInstallManagerFactory.create(context)
//告訴核心庫,我要模擬網(wǎng)絡請求連接錯誤的情況
fakeSplitInstallManager.setShouldNetworkError(true)

3.Dynamic Feature APK

提問:上述描述的只是將bundle包,根據(jù)不同設備資源所需,生成的apks包,即開始描述的資源包,那Dynamic Feature APK(動態(tài)分發(fā)包)如何跟實際業(yè)務邏輯結(jié)合實現(xiàn)呢?

首先,在了解bundle時,提出來Dynamic Feature APK是在base APK基礎上實現(xiàn)的,也就是所有Dynamic Feature module都是implementation project(":app")。

其次,bundel通過在<manifest> 清單文件中使用dist: XML這種命名空間形式,來定義不同屬性,這些行為稱之有對應的功能清單屬性,依據(jù)屬性說明配置。

最后,自定義 Feature Delivery,是用于處理不同需求場景下的分發(fā),例如安裝時下載、使用時下載等情景。

1.Dynamic Feature module創(chuàng)建

新建流程

  1. 如需打開 New Module 對話框,請從菜單欄中依次選擇 File > New > New Module。
  2. 在 New Module 對話框中,選擇 Dynamic Feature Module,然后點擊 Next。
  3. 像往常一樣配置模塊,然后點擊 Next。

現(xiàn)在來查看module中已創(chuàng)建的文件

android{
   dynamicFeatures = [':dynamicfeature']
}

//dynamicfeature/build.gradle
//plugins 在這里等同與apply plugin: 'com.android.dynamic-feature'
plugins {
    //  申明說我時一個Dynamic Feature Module
    id 'com.android.dynamic-feature'
}
android{
   defaultConfig {
      // 這里是你的模塊應用id,跟清單文件中的package對應,dynamicfeature為模塊名
      applicationId "com.example.dynamicfeature"
   }
}
dependencies {
    // 自動新增此依賴,因為所有的Dynamic Feature Module都是基于base module的
    implementation project(":app")
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.example.dynamicfeature">  <!-- 我們的applicationId -->
        
    <!-- dist:instant 是否免安裝 -->
    <!-- dist:title 模塊名標識 -->
  
    <dist:module
        dist:instant="false"
        dist:title="@string/title_dynamicfeature">
        <!-- dist:delivery 層級用于包裹 -->
        <dist:delivery>
            <!-- dist:on-demand 指定模塊按需下載 -->
            <dist:on-demand />
            <dist:install-time>
              <!-- dist:conditions 用于包裹條件 -->
                  <dist:conditions>
                <!-- 指定中國和香港地區(qū)不能下載該模塊 -->
                <dist:user-countries dist:exclude="true">
                  <dist:country dist:code="CN"/>
                  <dist:country dist:code="HK"/>
                </dist:user-countries>
                <!-- 指定華為手機才支持該模塊 -->
                    <dist:device-feature dist:name="android.hardware.camera.ar"/>
                <!-- 指定最小sdk21,最大sdk30 -->
                <dist:min-sdk dist:value="21"/>
                            <dist:max-sdk dist:value="30"/>
                    </dist:conditions>
            </dist:install-time>
        </dist:delivery>
        <dist:fusing dist:include="true" />
    </dist:module>
</manifest>

dynamicFeatures清單文件表

屬性 說明
xmlns:dist="http://schemas.<br />android.com/apk/distribution" 指定一個新的 dist: XML 命名空間,如下所述。
split="split_name" 模塊名,通常在清單文件中的package結(jié)尾處
`android:isFeatureSplit="true false">` 當 Android Studio 構建 App Bundle 時,會包含該屬性。因此,您不應手動添加或修改此屬性。指定此模塊為功能模塊。 基本模塊和配置 APK 中的清單要么省略該屬性,要么將其設置為 false。
<dist:module 這一新的 XML 元素定義了一些屬性,這些屬性可確定如何打包模塊并作為 APK 分發(fā)。
`dist:instant="true false"` 指定是否應通過 Google Play 免安裝體驗為模塊啟用免安裝體驗。如果應用包含一個或多個啟用免安裝體驗的功能模塊,您也必須為基本模塊啟用免安裝體驗。如果您使用的是 Android Studio 3.5 或更高版本,當您創(chuàng)建啟用免安裝體驗的功能模塊時,IDE 會為您完成此操作。在設置 <dist:on-demand/> 時,不能將此 XML 元素設置為 true。不過,根據(jù)免安裝體驗的運作方式,您仍可使用 Google Play Core 庫按需下載啟用免安裝體驗的功能模塊。當用戶下載并安裝您的應用時,設備會默認下載并安裝啟用免安裝體驗的功能模塊以及基本 APK。
dist:title="@string/feature_name" 為模塊指定一個面向用戶的名稱。例如,當設備請求確認下載時,便可能會顯示該名稱。您需要將此名稱的字符串資源包含在基本模塊的 module_root/src/source_set/res/values/strings.xml 文件中。
`<dist:fusing dist:include="true false" /></dist:module>` 指定是否在面向搭載 Android 4.4(API 級別 20)及更低版本的設備的 multi-APK 中包含此模塊。此外,當您使用 bundletool 從 App Bundle 生成 APK 時,只有將此屬性設置為 true 的功能模塊才會包含在通用 APK 中。通用 APK 是一個單體式 APK,其中包含了應用所支持的所有設備配置的代碼和資源。
<dist:delivery> 封裝自定義模塊分發(fā)的選項,如下所示:請注意,每個功能模塊必須只配置這些自定義分發(fā)選項中的一種類型。
<dist:install-time> 指定模塊應在安裝時可用。對于未指定自定義分發(fā)選項的其他類型的功能模塊,這是默認行為。如需詳細了解安裝時下載,請參閱配置安裝時分發(fā)。此節(jié)點還可以指定條件,用于限定要下載模塊的設備所需滿足的某些要求,例如設備功能,用戶所在國家/地區(qū)或最低 API 級別。如需了解詳情,請參閱配置按條件分發(fā)。
`<dist:removable value="true false" />` 當未設置或設置為 false 時,bundletool 會在根據(jù) bundle 生成拆分 APK 時將安裝時模塊整合到基本模塊中。 由于整合會使拆分 APK 的數(shù)量減少,因此此設置可以提升應用的性能。當 removable 設置為 true 時:安裝時模塊將不會整合到基本模塊中。如果您想要在將來卸載這些模塊,請將其設置為 true。 不過,配置過多可移除的模塊可能會導致應用的安裝時間增加。默認為 false。只有當您想要針對某個功能模塊停用整合功能時,才需要在清單中設置此值。注意:只有在使用 Android Gradle 插件 4.2 或從命令行使用 bundletool v1.0 時,才能使用此功能。
</dist:install-time>
<dist:on-demand/> 指定模塊應支持按需下載。也就是說,模塊在安裝時不會下載,但應用可以稍后請求下載。如需詳細了解按需下載,請參閱配置按需分發(fā)
</dist:delivery>
`<applicationandroid:hasCode="true false">...</application>` 如果功能模塊沒有生成 DEX 文件(也就是說,它不包含之后編譯成 DEX 文件格式的代碼),您必須執(zhí)行以下操作(否則,您可能會遇到運行時錯誤):在功能模塊的清單中將 android:hasCode 設置為 "false"。將以下內(nèi)容添加到基本模塊的清單中:<application android:hasCode="true" tools:replace="android:hasCode"> ...</application>

2.自定義 Feature Delivery

官方技術網(wǎng)址:https://developer.android.google.cn/guide/playcore/dynamic-delivery?hl=zh-cn#kotlin

分發(fā)選項 行為 示例用例 使用入門
安裝時分發(fā) 默認情況下,未配置上述任何分發(fā)選項的功能模塊會在安裝應用時下載。 如果應用包含特定的指導 Activity(比如關于如何在購物平臺上買賣商品的交互式指南),可以配置為在應用安裝時默認包含該功能。但是,為了減小應用的安裝大小,應用可在用戶完成該指導后請求刪除該功能。 清單文件中加上 <dist:install-time />
按需分發(fā) 允許您的應用按需請求和下載功能模塊。 如果當前應用支持的設備類型只有20%,那只需要先適配這20%的設備,之后按需增量下載。如果某些設備被淘汰,并且已無人使用,可以刪除舊功能支持包,縮減安裝包大小。 自己判斷條件通過manager.startInstall(request)添加
按條件分發(fā) 允許您指定的用戶,按需請求和下載功能模塊。 如果購物平臺應用的用戶遍布全球,您可能需要支持僅在特定地區(qū)使用的支付方式。為了減小應用的初始下載大小,您可以創(chuàng)建單獨的功能模塊處理特定類型的支付方式,并將這些模塊根據(jù)用戶的注冊區(qū)域視條件安裝在用戶設備上。 創(chuàng)建功能模塊并配置按條件分發(fā)
免安裝分發(fā) Google Play 免安裝體驗讓用戶無需在設備上安裝 APK 即可與應用互動。用戶可以通過 Google Play 商店中的“立即體驗”按鈕或您創(chuàng)建的網(wǎng)址體驗您的應用。 假設有一款游戲,游戲的前幾個關卡包含在輕量級功能模塊中。您可以啟用該模塊的免安裝體驗,這樣用戶就可以通過網(wǎng)址或“立即體驗”按鈕體驗游戲,而無需安裝應用。 創(chuàng)建功能模塊并配置免安裝分發(fā)。然后,應用就可以使用 Google Play Core 庫請求按需下載該模塊。請注意,使用功能模塊以模塊化處理應用功能只是第一步。如需支持 Google Play 免安裝體驗,應用基本模塊的下載大小和給定的啟用免安裝體驗的功能必須滿足嚴格的大小限制。如需了解詳情,請閱讀通過減少應用或游戲大小啟用免安裝體驗

前提:模塊配置都需要用到play core(核心庫),所以需要下載核心庫arr包或maven

建議:在理解之前可以先結(jié)合下個內(nèi)容 [3.APK包校驗] 中的例子,來輔助理解。

按需模塊

需求場景:假設某個具有按需模塊的應用可使用設備的相機拍攝和發(fā)送圖片消息,并且此按需模塊在其清單中指定了 split="pictureMessages"

// Creates an instance of SplitInstallManager.
val splitInstallManager = SplitInstallManagerFactory.create(context)

// 現(xiàn)在需要將pictureMessages和promotionalFilters按需添加,要先生成一個請求request
val request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build()

splitInstallManager
        //在應用處于前臺時開啟一個異步線程,用來執(zhí)行startInstall()任務
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
        //request請求成功或失敗的回調(diào)監(jiān)聽
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener { sessionId -> ... }
    .addOnFailureListener { exception ->  ... }

延遲安裝按需模塊

需求場景:某些功能,例如數(shù)據(jù)統(tǒng)計功能,由于功能模塊較大,為例不影響用戶初次安裝使用,首次不獲取,在首次使用過程中通過后臺去下載延遲安裝對應模塊。

//promotionalFilters,代表要被延遲加載的模塊名稱
splitInstallManager.deferredInstall(listOf("promotionalFilters"))

監(jiān)聽異步安裝模塊

需求場景:我希望在安裝成功某些模塊時,觸發(fā)回調(diào)處理一些邏輯業(yè)務

// Initializes a variable to later track the session ID for a given request.
//某個安裝請求的id編號,用于回調(diào)校驗
var mySessionId = 0

// Creates a listener for request status updates.
// 創(chuàng)建我們的更新回調(diào)監(jiān)聽對象
val listener = SplitInstallStateUpdatedListener { state ->
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
}

// Registers the listener.
//注冊
splitInstallManager.registerListener(listener)

// When your app no longer requires further updates, unregister the listener.
//解注冊
splitInstallManager.unregisterListener(listener)
...

//執(zhí)行request安裝請求
splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener { sessionId -> mySessionId = sessionId }
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener { exception ->
        // Handle request errors.
    }

處理請求錯誤

需求場景:由于存在可能模塊安裝失敗的問題,所以需要對這些錯誤進行處理

splitInstallManager
    .startInstall(request)
    .addOnFailureListener { exception ->
        when ((exception as SplitInstallException).errorCode) {
            // 沒有網(wǎng)絡連接
            SplitInstallErrorCode.NETWORK_ERROR -> {
                // Display a message that requests the user to establish a
                // network connection.
            }
            //請求被拒絕,當前有其他請求正在下載中
            SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads()
            ...
        }
    }

fun checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
            //以列表形式為每個活動會話返回一個SplitInstallSessionState對象
        .sessionStates
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                // Check for active sessions.
                for (state in task.result) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                        //如果當前狀態(tài)是在下載中,代表其他請求正在下載,需要取消當前
                        //或者延遲安裝當前的請求
                    }
                }
            }
        }
}

錯誤碼表

錯誤代碼 說明 建議采取的措施
ACTIVE_SESSIONS<br />_LIMIT_EXCEEDED 請求遭到拒絕,因為當前至少有一個請求正在下載。 檢查是否有任何仍在下載的請求,如上例所示。
MODULE_UNAVAILABLE Google Play 無法根據(jù)當前安裝的應用版本、設備和用戶的 Google Play 帳號找到所請求的模塊。 如果用戶無權訪問該模塊,請通知他們。
INVALID_REQUEST Google Play 已收到請求,但該請求無效。 驗證請求中包含的信息是否完整準確。
SESSION_NOT_FOUND 找不到指定會話 ID 對應的會話。 如果您嘗試通過會話 ID 監(jiān)控請求的狀態(tài),請確保會話 ID 正確無誤。
API_NOT_AVAILABLE 當前設備不支持 Play Core 庫。也就是說,該設備無法按需下載和安裝功能。 對于搭載 Android 4.4(API 級別 20)或更低版本的設備,您應在安裝時使用 dist:fusing 清單屬性添加功能模塊。如需了解詳情,請參閱功能模塊清單。
ACCESS_DENIED 由于權限不足,應用無法注冊該請求。 通常,當應用在后臺運行時,會出現(xiàn)這種情況。在應用返回到前臺時嘗試請求。
NETWORK_ERROR 由于出現(xiàn)網(wǎng)絡連接錯誤,請求失敗。 提示用戶建立網(wǎng)絡連接或更改為其他網(wǎng)絡。
INCOMPATIBLE_WITH
_EXISTING_SESSION
該請求包含一個或多個已請求但尚未安裝的模塊。 創(chuàng)建一個新請求,該請求不包含應用已請求的模塊,或等待所有當前已請求的模塊完成安裝,然后再重試請求。請注意,請求已安裝的模塊無法解決錯誤。
SERVICE_DIED 負責處理請求的服務已終止。 請重試請求。此錯誤代碼會作為對 SplitInstallStateUpdatedListener(其狀態(tài)為 FAILED,會話 ID 為 -1)的更新提供。

處理狀態(tài)更新

需求場景:當更新模塊時,需要對模塊進度信息進行反饋,那么在之前監(jiān)聽的基礎上,根據(jù)不同的安裝狀態(tài),來進行信息反饋。

SplitInstallStateUpdatedListener中的onStateUpdate

override fun onStateUpdate(state : SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIES) {
       // Retry the request.
       // 安裝失敗重試
       return
    }
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.DOWNLOADING -> {
              val totalBytes = state.totalBytesToDownload()
              val progress = state.bytesDownloaded()
              // Update progress bar.
              //下載中更新進度條
            }
            SplitInstallSessionStatus.INSTALLED -> {
                            //在此處,你可以調(diào)用你即將跳轉(zhuǎn)的Activity界面,并且可以訪問安裝后模塊
              //的所有資源,如果你設置來demand模式按需安裝,在8.0或之上的系統(tǒng),需要
              //使用SplitInstallHelper的api更新上下文context
              
              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
            }
        }
    }
}

模塊安裝狀態(tài)表

請求狀態(tài) 說明 建議采取的措施
PENDING 已接受該請求,即將開始下載。 初始化界面組件(例如進度欄),向用戶提供關于下載的反饋。
REQUIRES_USER
_CONFIRMATION
下載需要用戶確認。這很可能是由于下載內(nèi)容大小超過 10 MB。 提示用戶接受下載請求。如需了解詳情,請轉(zhuǎn)到有關如何獲取用戶確認的部分。
DOWNLOADING 下載正在進行中。 如果您為下載提供了進度條,請使用 SplitInstallSessionState.bytesDownloaded()SplitInstallSessionState.totalBytesToDownload() 方法更新界面(請參見此表上方的代碼示例)。
DOWNLOADED 設備已下載模塊,但尚未開始安裝。 應用應啟用 SplitCompat,以便訪問已下載的模塊并避免出現(xiàn)此狀態(tài)。必須執(zhí)行此操作才能訪問功能模塊的代碼和資源。
INSTALLING 設備當前正在安裝該模塊。 更新進度條。此狀態(tài)通常較短。
INSTALLED 該模塊已安裝在設備上。 訪問模塊中的代碼和資源以繼續(xù)用戶操作流程。如果該模塊針對的是在 Android 8.0(API 級別 26)或更高版本設備上運行的 Android 免安裝應用,您需要使用 splitInstallHelper 才能利用新模塊更新應用組件。
FAILED 在模塊安裝到設備上之前,請求已失敗。 提示用戶重試請求或取消請求。
CANCELING 設備正在取消請求。 如需了解詳情,請轉(zhuǎn)到有關如何取消安裝請求的部分。
CANCELED 請求已取消。

獲取用戶確認

需求場景:用戶當前在app上使用移動數(shù)據(jù)流量,由于新功能模塊包需要流量數(shù)據(jù),要經(jīng)用戶同意后才允許下載。

override fun onSessionStateUpdate(state: SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a dialog for the user to either “Download”
        // or “Cancel” the request.
        // 顯示選擇對話框
        splitInstallManager.startConfirmationDialogForResult(
          state,
          /* activity = */ this,
          // You use this request code to later retrieve the user's decision.
          /* requestCode = */ MY_REQUEST_CODE)
    }
    ...
 }

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  if (requestCode == MY_REQUEST_CODE) {
    // Handle the user's decision. For example, if the user selects "Cancel",
    // you may want to disable certain functionality that depends on the module.
    //在此處處理回調(diào)
  }
}

請求的狀態(tài)會根據(jù)用戶響應進行更新:

  • 如果用戶選擇“下載”,請求狀態(tài)會更改為 PENDING 并繼續(xù)下載。
  • 如果用戶選擇“取消”,請求狀態(tài)會更改為 CANCELED。
  • 如果用戶在對話框被銷毀之前未做出選擇,請求狀態(tài)會保持為 REQUIRES_USER_CONFIRMATION。您的應用可能會再次提示用戶完成請求。

訪問模塊

需求場景:如需在下載后從已下載的模塊訪問代碼和資源,您的應用需要為應用和應用下載的功能模塊中的每個 Activity 啟用 SplitCompat 庫。例如,下載的模塊b中存在我要啟動的ActivityB,為了訪問到ActivityB,我需要啟動SplitCompat庫。

1.啟動SplitCompat庫:

方式1.如需啟用 SplitCompat,最簡單的方法是在您的應用清單中將 SplitCompatApplication 聲明

<application         android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
</application>

方式2.在運行時調(diào)用SplitCompat

class MyApplication : SplitCompatApplication() {
    override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // 判斷模塊是否安裝,如果您的按需模塊可同時與免安裝應用和安裝式應用兼容
    if (!InstantApps.isInstantApp(this)) {
        // Emulates installation of future on demand modules using SplitCompat.
            // 在此處調(diào)用,以便獲取正確的context
        SplitCompat.install(this)
    }
    }
}

2.為Activity啟用SplitCompact

//ActivityB中
override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this)
}

訪問模塊代碼和資源

需求場景1:對應的模塊已經(jīng)安裝完成,我需要訪問模塊內(nèi)的資源,例如ActivityB,只需要獲取到最新的上下文就可以跳轉(zhuǎn)訪問

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                // 使用createPackageContext獲取新的上下文
                val newContext = context.createPackageContext(context.packageName, 0)
                //newContext :Context就是最新的可用上下文
                val am = newContext.assets
            }
        }
    }
}

需求場景2 :Android 8.0 及更高版本上的 Android 免安裝應用

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                                //版本大于8.0
                if (BuildCompat.isAtLeastO()) {
                                        //由于時面安裝應用,所以不能使用createPackageContext
                    //所以需要使用updateAppInfo來實現(xiàn)上下文的更新
                    SplitInstallHelper.updateAppInfo(context)
                    Handler().post {
                        // Loads contents from the module using AssetManager
                        val am = context.assets
                        ...
                    }
                    //使用免安裝c庫
                    SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”)
                }
            }
        }
    }
}

管理已安裝模塊

需求場景1:當前想知道設備已安裝的功能模塊

val installedModules: Set<String> = splitInstallManager.installedModules

需求場景2: 想要卸載某些模塊

//pictureMessages即模塊名,申明在清單文件的 package 中的最后一個單詞
splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))

管理語言安裝包

需求場景1: 下載某些語言資源

sharedPrefs.edit().putString(LANGUAGE_SELECTION, "zh").apply()

// 創(chuàng)建請求,添加語言,包含“zh-CN、zh-TW”的所有“zh”資源
val request = SplitInstallRequest.newBuilder()
 .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build()

// Submits the request to install the additional language resources.
// 執(zhí)行語言請求安裝包
splitInstallManager.startInstall(request)

需求場景2: 訪問已下載的語言資源

//1.Activity中
override fun attachBaseContext(base: Context) {
  super.attachBaseContext(base)
  SplitCompat.installActivity(this)
}

//2.application中
override fun attachBaseContext(base: Context) {
  val configuration = Configuration()
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
  val context = base.createConfigurationContext(configuration)
  super.attachBaseContext(context)
  SplitCompat.install(this)
}

//3.使語言生效
when (state.status()) {
  SplitInstallSessionStatus.INSTALLED -> {
      // Recreates the activity to load resources for the new language
      // preference.
        // 需要重新加載Activity
      activity.recreate()
  }
  ...
}

需有場景3: 卸載語言資源

//1.查看已安裝語言
val installedLanguages: Set<String> = splitInstallManager.installedLanguages
//2.卸載指定語言
splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

3. APK包校驗

全量apks

1.生成全量apks集合包

//生成bundle包
bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

2.解壓apks包,查看apk的完整性

image-20210507184501204.png
image-20210507184903071.png

解壓以后會有倆個文件夾instant和splits,如果sdk版本支持低于21,即還會有另外一個standlones文件,該文件不支持按需加載。instant目錄代表支持免安裝的apk資源,而splits,則是按需加載的apk資源,其中我們的自適應功能包的apk會在其中。

所以,我們需要檢查是否對應的Dynamic Feature module中是否有與之對應的apk包。

生成設備所需apks

//1.cd到指定目錄
cd /Users/qiushujie/AndroidStudioProjects/app-bundle-samples-master/DynamicFeatures/app/build/outputs/bundle/debug
//2.利用bundle指令--connected-device 模擬手機通過play store安裝的apks包
bundletool build-apks --connected-device --bundle=app-debug.aab --output=app-debug.apks 

//3.自動會提示你當前用的是debug.keystore,正式key參照其余指令,此時已生成apks
INFO: The APKs will be signed with the debug keystore found at '/Users/qiushujie/.android/debug.keystore'.
  
//4.將apks安裝到手機中,前提:adb命令通暢,通過adb version校驗
bundletool install-apks --apks=app-debug.apks
The APKs have been extracted in the directory: /var/folders/53/x84c5smn67v0_gbpvmy1dv3w0000gn/T/2817755201777769660

//5.通過命令查看當前目錄下apks的大小,可以明顯看到少了將近16MB
ls -l
total 25864
-rw-------  1 qiushujie  staff  7401480 May  8 12:17 app-debug.aab
-rw-------  1 qiushujie  staff  5835855 May  8 15:25 app-debug.apks

了解apks和module的關聯(lián)

image-20210508161627475.png

以官方提供的demo為例,instant目錄下,由于根據(jù)設備生成,所以生成了master、xxhdpi、zh對應base module。另外,底下的split和url module,代表著這倆個模塊允許下載免安裝使用,接下里我們看下它們清單文件中的配置信息。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.google.android.samples.instantdynamicfeatures">
        
    <!-- dist:instant="true" 才讓instant目錄擁有該模塊apk,即免安裝使用開啟時會擁有該模塊的apk在bundle生成的apks中 -->
    <dist:module
        dist:instant="true"
        dist:title="@string/module_instant_feature_split_install">
        <dist:fusing dist:include="true" />
        <dist:delivery>
            <dist:install-time />
        </dist:delivery>
    </dist:module>
</manifest>
image-20210508162712842.png

接下來看下splits目錄,splits代表著該目錄底下都是對應的dynamic-feature類型的module,但有一點特殊的地方,紅框內(nèi)生成的base模塊下的master、xxhdpi、zh,跟instant中大小都是一致的,除此之外的其余包,都可以認為是dynamic-feature類型的module生成的apk包。

<!-- dist:title 決定了我們的module名字 -->
<!-- dist:fusing dist:include="true" 支持sdk19-21的apk版本,由于我們根據(jù)當前設備生成的,當前設備sdk版本大于21,因而沒有standlones目錄-->
<dist:module
    <dist:title="@string/module_feature_kotlin">
    <dist:fusing dist:include="true" />
    <dist:delivery>
        <dist:on-demand />
    </dist:delivery>
</dist:module

清楚了包的生成和命名,接下來就是master和xxhdpi的理解,每個module都有自己的master包,xxhdpi時根據(jù)設備分辨率生成的包,這里有一個需要注意的,當涉及到ndk時,還有一個arm64_v8a包,現(xiàn)今手機基本都是arm系列64位的,所以這個包也是最常見的包。

場景:當我們需要調(diào)用native module中的c庫方法,那就需要額外添加代碼

SplitInstallHelper.loadLibrary(this, "hello-jni")

分析官方Demo

官方Demo地址:https://github.com/android/app-bundle-samples/tree/master/DynamicFeatures

1.生成bundle包,并打本地測試包

在打測試包之前,需要通過頂部菜單Build>>Build Bundle/APK(s)>>Build Bundle(s)生成bundle包。

//與之前打apks的命令一致,新增了--local-testing
bundletool build-apks --connected-device --local-testing --bundle=app-debug.aab --output=app-debug.apks 
//安裝
bundletool install-apks --apks=app-debug.apks
2.打開app,查詢當前已安裝模塊
private lateinit var manager: SplitInstallManager
//還是通過工廠類獲取manager
manager = SplitInstallManagerFactory.create(this)

private fun getCurInstallModule(){
    //manager.installedModules是獲取當前安裝module的方法,返回set<String>
    Log.e(TAG, "getCurInstallModule: ${manager.installedModules}")
}

執(zhí)行完上述代碼后,我們能知道默認已安裝的module為,[initialInstall, split, url]

在了解apks和module關聯(lián)時,有提到instant目錄擁有split、url,而initialInstall是在splits目錄下,仔細查詢了下,在主入口時并沒有進行安裝處理,但在initialInstall的清單文件中發(fā)現(xiàn)了些端倪。

<dist:module
    dist:title="@string/title_module_initial">
    <dist:fusing dist:include="true" />
    <!-- dist:install-time 就是決定安裝時,便進行下載安裝initialInstall的關鍵 -->
    <dist:delivery>
        <dist:install-time />
    </dist:delivery>
</dist:module>
3.訪問未安裝模塊kotlin
// 判斷是否已安裝,安裝執(zhí)行跳轉(zhuǎn)
if (manager.installedModules.contains(name)) {
  jumpKotlinAcitivity()
  return
}
// 未安裝新建請求
val request =
    SplitInstallRequest
        .newBuilder()
        .addModule("kotlin")
        .build()
//執(zhí)行安裝請求
private lateinit var manager: SplitInstallManager
manager.startInstall(request)

//參照Feature Delivery 監(jiān)聽異步安裝模塊
private val listener = SplitInstallStateUpdatedListener { state ->
   //判斷state.status(),通常為1 pengding > 2 INSTALLING > 5 INSTALLED
    when (state.status()) { 
        SplitInstallSessionStatus.INSTALLED -> {
                    //判斷是否為語言安裝
          if (langsInstall) {
            onSuccessfulLanguageLoad(names)
          } else {
            //回調(diào)跳轉(zhuǎn), jumpKotlinAcitivity()
            onSuccessfulLoad(names, launch = !multiInstall)
          }
        }
    }                                                      
}
manager.registerListener(listener)

執(zhí)行完上述代碼后,我們能知道默認已安裝的module為,[initialInstall, split, url, kotlin]

4.切換新語言
// 是否存在對應語言包
if (manager.installedLanguages.contains(lang)) {
    //安裝語言包成,執(zhí)行recreate()重初始化界面
    onSuccessfulLanguageLoad(lang)
    return
}

//執(zhí)行下載語言包請求
val request = SplitInstallRequest.newBuilder()
        .addLanguage(Locale.forLanguageTag(lang))
        .build()
manager.startInstall(request)

//langsInstall為true,走之前的SplitInstallStateUpdatedListener

執(zhí)行完之后,在installedLanguages中,就能打印出對應“l(fā)ang”的module了

5.訪問其他Module的資源
// 1.與訪問kotlin模塊一樣的安裝方式,訪問assets模塊
// 2.成功時回調(diào)displayAssets
private fun displayAssets() {
        // 通過重新獲取context,并且啟動SplitCompat庫
    val assetManager = createPackageContext(packageName, 0).also {
        SplitCompat.install(it)
    }.assets
    
    // 當前assetManager此時時通過context.getAssets()獲取的
    // 如果獲取資源,可以用context.getResources()獲取res文件資源
  
    //讀取assets/assets.text文本
    val assetsStream = assetManager.open("assets.txt")
    val assetContent = assetsStream.bufferedReader()
            .use {
                it.readText()
            }
    
    //將assets.text的文本以彈窗顯示
    AlertDialog.Builder(this)
            .setTitle(getString(R.string.asset_content))
            .setMessage(assetContent)
            .show()
}
// 純資源無java生成的dex文件,可加上此標識
<application android:hasCode="false" />
6.為特定的sdk版本新增模塊
<dist:module dist:title="@string/module_feature_maxsdk">
    <dist:fusing dist:include="true" />
    <dist:delivery>
        <dist:install-time>
            <dist:conditions>
                <dist:max-sdk dist:value="23" />
            </dist:conditions>
        </dist:install-time>
    </dist:delivery>
</dist:module>

與安裝其他模塊一致,只有當手機版本大于等于23,即6.0時才會安裝此module的apk包,可用于版本特殊處理。

4.組件化應用

Bundle是一種很好的打包方式,為了利用好該方式,對于模塊的組件化,有更高要求,那么我們就需要思考,如何更好的將組件化和bundle的Dynamic-Feature模塊結(jié)合好。

模塊劃分

1.App module

該模塊用于最基礎的apk打包,即集成了應用的基本功能。由于組件化+arouter(阿里路由框架),需要一個空殼模塊來符合組件化的設計理念,并且大量的基本功能業(yè)務邏輯,也不允許我們將app作為一個單獨的module開發(fā)。所以app模塊在我們設計中,應該是一個空殼,該空殼會去持有倆類基本module,第一個是定制的UI module,第二個是擁有的Basic Feature module。

2.UI module

指的是涉及到我們公共UI的模塊,例如主入口界面、通用的Dialog、Fragment界面等。由于涉及界面交互時,通暢有網(wǎng)絡請求、數(shù)據(jù)處理、工具類或自定義View的使用、拓展方法等,所以UI module需要持有一層common module,即公用的模塊,這些模塊大體可以分為Network(網(wǎng)絡請求和網(wǎng)絡請求涉及的bean類)、Utils(工具類、kotlin拓展方法)、Common(base Activity等UI基礎類、動態(tài)通用彈窗、自定義View)、Resource(顏色表、公用資源、風格資源)

3.Basic Feature module

指的是基礎功能模塊,例如Pay module、Bluetooth module,都可以作為摸個單純的功能模塊。在設計這些模塊的時候,需要注意的是外部調(diào)用,我們需要把支付流程或藍牙的一切行為邏輯,抽取對應的接口或抽象類出來,通過傳入其實現(xiàn)類,實現(xiàn)某個功能的黑盒操作,例如藍牙自動打開、掃描、配對、回調(diào)可以通信,這一切流程都在bluetooth中完成。

4.Network module

網(wǎng)絡模塊,主要涉及接口的定義修改,該模塊只含UI module中基礎所需的網(wǎng)絡請求,如果涉及到Feature module的網(wǎng)絡請求,則可以在該module中單獨開network包實現(xiàn),避免隨著功能module的網(wǎng)絡請求需求,而頻繁修改Network module。

5.Utils module

該模塊涉及所有可以公用的工具類、kotlin可訪問到的bean類的拓展類,特殊bean類的拓展,在其功能模塊中單獨持有utils包。

6.Common module

該module中持有BaseActivity、BaseFragment等一系列基礎類、以及它們的部分子類,同時持有各種自定義View、通用彈窗等。該模塊可以默認持有Resource module,由于Common module大概率需要被其他module所持有,所以可以讓其與Resource module綁定一起。

7.Dynamic-Feature module

自適應功能模塊默認是要持有app的,本意是在app 的基礎上做一定的功能定制化,每個功能效果不一,并且支持bundle打包之后能做到增量下載。所以,當存在某個需求,例如藍牙設備的支持,由于每個藍牙型號其交互協(xié)議和交互邏輯可能存在不同,針對公用的邏輯我們可以在app module中的bluetooth module定義,而需要定制邏輯處理時,那就需要利用拓展類實現(xiàn),這些拓展類就是Dynamic-Feature module中涵蓋的內(nèi)容。

模塊通信

1.路由通信

interface RouterPath {
    companion object {
        //mobile
        const val MOBILE_HOME = "/mobile/home_activity"
        //tablet
        const val TABLET_HOME = "/tablet/home_activity"
                //pay
        const val PAY_ACTIVITY = "/pay/activity"
    }
}

fun open(path: String, requestCode: Int = 0, action: Postcard.() -> Unit = {}) {
        val postcard = ARouter.getInstance().build(path)
        postcard.action()
        postcard.navigation(this, requestCode)
}

定義路徑名,然后在對應的類中新增注解@Route(path = RouterPath.MOBILE_HOME),之后通過Arouter的navigation()方法跳轉(zhuǎn)到不同模塊的界面,從而實現(xiàn)跨模塊的跳轉(zhuǎn)

2.Dynamic-Feature module使用須知

a.引用app module資源時,不能直接使用R.drawdble 需要使用 [base moudle packagename].R.drawdble的方式

b.app module無法訪問Dynamic中的資源id,原因倆個模塊相同id,會在arssc中生成不一樣的值。

c.當加載完畢Dynamic-Feature module,需要啟動SplitCompat庫之后,才能訪問跳轉(zhuǎn)module中的頁面或資源

d.當加載的Dynamic-Feature module apk大于10MB時,需要使用用戶確認功能才能進行加載。

e.如果Dynamic-Feature module持有module A,moduleA中擁有ActivityA,那app module中不能訪問到ActivityA。

f.要清楚的知道幾種安裝方式,免安裝instant = true,安裝包時安裝dist:install-time,按需安裝dist:on-demand,dist:fusing dist:include="true"支持19-21sdk版本

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容