Android Bundle包全過程詳解

1.認(rèn)識(shí)Bundle

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

定義

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

作用

Google Play 會(huì)使用您的 App Bundle 針對(duì)每種設(shè)備配置生成并提供經(jīng)過優(yōu)化的 APK宵溅,因此只會(huì)下載特定設(shè)備所需的代碼和資源來運(yùn)行您的應(yīng)用凌简。即用戶只需下載最基礎(chǔ)的apk,剩余根據(jù)情景恃逻,按需下載雏搂,減少下載量,以此提高下載和迭代速度寇损,提高用戶體驗(yàn)凸郑。

Apk的分類

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

image-20210507094849989.png

1.Base APK找田,可以認(rèn)為是基礎(chǔ)版apk歌憨,即集成了基本功能,并且每個(gè)用戶都需要擁有的代碼模塊午阵。

2.Configuration APK躺孝,大致分為以下三類,對(duì)應(yīng)圖中底部三種APK包底桂,分別是像素分辨率資源植袍、cpu內(nèi)核分類、國(guó)際化語言籽懦。無論是Base APK還是Dynamic Feature APK于个,他們都擁有自身的Configuration APK,bundle包會(huì)根據(jù)設(shè)備的像素分辨率暮顺、cpu厅篓、當(dāng)前使用語言,提供適配設(shè)備的精簡(jiǎn)包捶码。

3.Dynamic Feature APK羽氮,自適應(yīng)功能APK包,簡(jiǎn)單理解為在基礎(chǔ)包功能的基礎(chǔ)上惫恼,根據(jù)不同設(shè)備型號(hào)和不同用戶手中所需的設(shè)備档押,按需下載對(duì)應(yīng)的APK包,避免全部設(shè)備類型包都下載。所以令宿,F(xiàn)eature包是滿足base所有功能基礎(chǔ)前提的一種用于細(xì)分業(yè)務(wù)的拓展叼耙。

Bundle分包配置

場(chǎng)景:我需要bundle打包,但我希望所有語言包在安裝時(shí)就安裝好粒没,以便我切換語言筛婉。

當(dāng)用戶通過Google Play Store下載應(yīng)用時(shí),如果上架的是bundle包癞松,那么就會(huì)根據(jù)當(dāng)前手機(jī)配置過的語言爽撒,動(dòng)態(tài)下載語言包,例如我本地手機(jī)配置過英語拦惋、中文匆浙。可我應(yīng)用支持德語厕妖,用戶在安裝完包以后首尼,通過系統(tǒng)切換到德語,那由于bundle在安裝的時(shí)候并沒有下載包言秸,此時(shí)也不會(huì)動(dòng)態(tài)去下載软能,因此無法切換成功德語的多語言適配。所以举畸,我們需要在bundle生成時(shí)查排,在app/build.gradle 中進(jìn)行配置,來實(shí)現(xiàn)我們所需的效果

android{
    bundle {
        language {
          //是否開啟語言分包抄沮,當(dāng)為true在這里可以添加inclue ‘ch-ZH’,配置預(yù)設(shè)語言
          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包率挣,最終通過壓縮工具校驗(yàn)包內(nèi)的apk是否和期望一致刻伊。

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

mac電腦安裝步驟

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

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

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

4.通過Finder查看器动漾,到個(gè)人用戶/usr文件下丁屎,找到“.bash.profile”,用文本打開旱眯,并寫入export PATH=PATH:$ANDROID_HOME/bundle-tool/悦屏,通過source .bash_profile保存

5.通過android studio的Terminal节沦,首先先cd到你所打的bundle包目錄键思,隨后通過bundle命令測(cè)試础爬,其中app-debug.abb,即我們cd目錄下的bundle包吼鳞,最終會(huì)在該目錄生成apks包看蚜。bundle命令如果生效,則代表安裝完成赔桌,否則會(huì)提示找不到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指令標(biāo)識(shí)表

--bundle=path 包輸入地址
--output=path apks包輸出地址
--overwrite 如果已經(jīng)在目錄下有輸出文件,output會(huì)提示以存在文件疾党,需要要用此命令重寫
--aapt2=path 指定 AAPT2 的自定義路徑音诫。 默認(rèn)情況下,bundletool 包含自己的 AAPT2 版本雪位。
--ks=path 指定用于為 APK 簽名的部署密鑰庫的路徑竭钝。此標(biāo)記是可選的。如果您不添加此標(biāo)記雹洗,bundletool 會(huì)嘗試使用調(diào)試簽名密鑰為您的 APK 簽名香罐。
--ks-pass=pass:password--ks-pass=file:/path/to/file 指定密鑰庫的密碼。如果您指定純文本格式的密碼时肿,請(qǐng)使用 pass: 限定該密碼庇茫。如果您要傳遞包含該密碼的文件的路徑,請(qǐng)使用 file: 限定該路徑螃成。如果您使用 --ks 標(biāo)記指定密鑰庫旦签,而未指定 --ks-pass,那么 bundletool 會(huì)提示您從命令行輸入密碼寸宏。
--ks-key-alias=alias 指定要使用的簽名密鑰的別名宁炫。
--key-pass=pass:password--key-pass=file:/path/to/file 指定簽名密鑰的密碼。如果您指定純文本格式的密碼击吱,請(qǐng)使用 pass: 限定該密碼淋淀。如果您要傳遞包含該密碼的文件的路徑,請(qǐng)使用 file: 限定該路徑覆醇。如果此密碼與密鑰庫的密碼相同朵纷,您可以省略此標(biāo)記。
--connected-device 根據(jù)連接設(shè)備區(qū)分永脓,把bundle包安裝到不同設(shè)備
--device-id=serial-number 如果您有多個(gè)已連接的設(shè)備袍辞,請(qǐng)使用此標(biāo)記指定要部署應(yīng)用的設(shè)備的序列 ID。
--device-spec=spec_json 使用此標(biāo)記提供 .json 文件的路徑常摧,該文件指定了您要針對(duì)其生成 APK 的設(shè)備配置搅吁。
--mode=universal 如果您希望 bundletool 只構(gòu)建一個(gè)包含應(yīng)用的所有代碼和資源的 APK威创,以使該 APK 與應(yīng)用支持的所有設(shè)備配置兼容,請(qǐng)將模式設(shè)置為 universal谎懦。注意bundletool 僅包含功能模塊肚豺,這些模塊在通用 APK 中的對(duì)應(yīng)清單中指定 <dist:fusing dist:include="true"/>。如需了解詳情界拦,請(qǐng)參閱功能模塊清單吸申。請(qǐng)注意,這些 APK 要比針對(duì)特定設(shè)備配置優(yōu)化過的 APK 更大享甸。但是截碴,這些 APK 更便于與內(nèi)部測(cè)試人員共享,例如想在多種設(shè)備配置上測(cè)試您的應(yīng)用的測(cè)試人員蛉威。
--local-testing 使用此標(biāo)志啟用 app bundle 進(jìn)行本地測(cè)試日丹。 在本地測(cè)試時(shí),由于無需上傳到 Google Play 服務(wù)器蚯嫌,因此能夠?qū)崿F(xiàn)快速的迭代測(cè)試周期哲虾。 有關(guān)如何使用 --local-testing 標(biāo)記測(cè)試模塊安裝的示例,請(qǐng)參閱在本地測(cè)試模塊的安裝齐帚。

2.Bundle功能指令

1.部署apks到設(shè)備中

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

2.為當(dāng)前連接設(shè)備生成自適應(yīng)的一組apk包

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

3.獲取設(shè)備json文件以及使用json文件

//1.當(dāng)沒有json文件妒牙,想通過設(shè)備獲取其適配的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ī)范

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

4.從已有的apks包中对妄,提取一部分特定設(shè)備的apk

bundletool extract-apks
//當(dāng)前完整的apks
--apks=/MyApp/my_existing_APK_set.apks
//不希望從bundle包再去打特定設(shè)備湘今,直接從現(xiàn)有的apks抽取部分形成特定設(shè)備的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

實(shí)現(xiàn)本地測(cè)試

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

為了實(shí)現(xiàn)這種測(cè)試情況剪菱,需要有以下前提

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補(bǔ)充

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相對(duì)路徑要用file孝常,否則直接填寫絕對(duì)路徑
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 {
          //放在項(xiàng)目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標(biāo)記

//--local-testing標(biāo)記构灸,申明本地
bundletool build-apks --local-testing
  --bundle my_app.aab
  --output my_app.apks
//直接安裝的包就是支持本地測(cè)試的包了
bundletool install-apks --apks my_app.apks

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

當(dāng)通過--local-testing標(biāo)記上渴,并將其部署到測(cè)試設(shè)備后,可以在應(yīng)用中調(diào)用play core庫中的FakeSplitInstallManager類喜颁,來模擬網(wǎng)絡(luò)請(qǐng)求連接錯(cuò)誤稠氮。

示例:

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

3.Dynamic Feature APK

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

首先鬓长,在了解bundle時(shí)谒拴,提出來Dynamic Feature APK是在base APK基礎(chǔ)上實(shí)現(xiàn)的,也就是所有Dynamic Feature module都是implementation project(":app")痢士。

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

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

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

新建流程

  1. 如需打開 New Module 對(duì)話框侨歉,請(qǐng)從菜單欄中依次選擇 File > New > New Module屋摇。
  2. 在 New Module 對(duì)話框中,選擇 Dynamic Feature Module幽邓,然后點(diǎn)擊 Next炮温。
  3. 像往常一樣配置模塊,然后點(diǎn)擊 Next牵舵。

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

android{
   dynamicFeatures = [':dynamicfeature']
}

//dynamicfeature/build.gradle
//plugins 在這里等同與apply plugin: 'com.android.dynamic-feature'
plugins {
    //  申明說我時(shí)一個(gè)Dynamic Feature Module
    id 'com.android.dynamic-feature'
}
android{
   defaultConfig {
      // 這里是你的模塊應(yīng)用id柒啤,跟清單文件中的package對(duì)應(yīng),dynamicfeature為模塊名
      applicationId "com.example.dynamicfeature"
   }
}
dependencies {
    // 自動(dòng)新增此依賴畸颅,因?yàn)樗械腄ynamic 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 模塊名標(biāo)識(shí) -->
  
    <dist:module
        dist:instant="false"
        dist:title="@string/title_dynamicfeature">
        <!-- dist:delivery 層級(jí)用于包裹 -->
        <dist:delivery>
            <!-- dist:on-demand 指定模塊按需下載 -->
            <dist:on-demand />
            <dist:install-time>
              <!-- dist:conditions 用于包裹條件 -->
                  <dist:conditions>
                <!-- 指定中國(guó)和香港地區(qū)不能下載該模塊 -->
                <dist:user-countries dist:exclude="true">
                  <dist:country dist:code="CN"/>
                  <dist:country dist:code="HK"/>
                </dist:user-countries>
                <!-- 指定華為手機(jī)才支持該模塊 -->
                    <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" 指定一個(gè)新的 dist: XML 命名空間,如下所述没炒。
split="split_name" 模塊名涛癌,通常在清單文件中的package結(jié)尾處
`android:isFeatureSplit="true false">` 當(dāng) Android Studio 構(gòu)建 App Bundle 時(shí),會(huì)包含該屬性送火。因此拳话,您不應(yīng)手動(dòng)添加或修改此屬性。指定此模塊為功能模塊漾脂。 基本模塊和配置 APK 中的清單要么省略該屬性假颇,要么將其設(shè)置為 false
<dist:module 這一新的 XML 元素定義了一些屬性骨稿,這些屬性可確定如何打包模塊并作為 APK 分發(fā)笨鸡。
`dist:instant="true false"` 指定是否應(yīng)通過 Google Play 免安裝體驗(yàn)為模塊啟用免安裝體驗(yàn)姜钳。如果應(yīng)用包含一個(gè)或多個(gè)啟用免安裝體驗(yàn)的功能模塊,您也必須為基本模塊啟用免安裝體驗(yàn)形耗。如果您使用的是 Android Studio 3.5 或更高版本哥桥,當(dāng)您創(chuàng)建啟用免安裝體驗(yàn)的功能模塊時(shí),IDE 會(huì)為您完成此操作激涤。在設(shè)置 <dist:on-demand/> 時(shí)拟糕,不能將此 XML 元素設(shè)置為 true。不過倦踢,根據(jù)免安裝體驗(yàn)的運(yùn)作方式送滞,您仍可使用 Google Play Core 庫按需下載啟用免安裝體驗(yàn)的功能模塊。當(dāng)用戶下載并安裝您的應(yīng)用時(shí)辱挥,設(shè)備會(huì)默認(rèn)下載并安裝啟用免安裝體驗(yàn)的功能模塊以及基本 APK犁嗅。
dist:title="@string/feature_name" 為模塊指定一個(gè)面向用戶的名稱。例如晤碘,當(dāng)設(shè)備請(qǐng)求確認(rèn)下載時(shí)褂微,便可能會(huì)顯示該名稱。您需要將此名稱的字符串資源包含在基本模塊的 module_root/src/source_set/res/values/strings.xml 文件中园爷。
`<dist:fusing dist:include="true false" /></dist:module>` 指定是否在面向搭載 Android 4.4(API 級(jí)別 20)及更低版本的設(shè)備的 multi-APK 中包含此模塊宠蚂。此外,當(dāng)您使用 bundletool 從 App Bundle 生成 APK 時(shí)童社,只有將此屬性設(shè)置為 true 的功能模塊才會(huì)包含在通用 APK 中求厕。通用 APK 是一個(gè)單體式 APK,其中包含了應(yīng)用所支持的所有設(shè)備配置的代碼和資源叠洗。
<dist:delivery> 封裝自定義模塊分發(fā)的選項(xiàng)甘改,如下所示:請(qǐng)注意,每個(gè)功能模塊必須只配置這些自定義分發(fā)選項(xiàng)中的一種類型灭抑。
<dist:install-time> 指定模塊應(yīng)在安裝時(shí)可用十艾。對(duì)于未指定自定義分發(fā)選項(xiàng)的其他類型的功能模塊,這是默認(rèn)行為腾节。如需詳細(xì)了解安裝時(shí)下載忘嫉,請(qǐng)參閱配置安裝時(shí)分發(fā)。此節(jié)點(diǎn)還可以指定條件案腺,用于限定要下載模塊的設(shè)備所需滿足的某些要求庆冕,例如設(shè)備功能,用戶所在國(guó)家/地區(qū)或最低 API 級(jí)別劈榨。如需了解詳情访递,請(qǐng)參閱配置按條件分發(fā)
`<dist:removable value="true false" />` 當(dāng)未設(shè)置或設(shè)置為 false 時(shí)同辣,bundletool 會(huì)在根據(jù) bundle 生成拆分 APK 時(shí)將安裝時(shí)模塊整合到基本模塊中拷姿。 由于整合會(huì)使拆分 APK 的數(shù)量減少惭载,因此此設(shè)置可以提升應(yīng)用的性能。當(dāng) removable 設(shè)置為 true 時(shí):安裝時(shí)模塊將不會(huì)整合到基本模塊中响巢。如果您想要在將來卸載這些模塊描滔,請(qǐng)將其設(shè)置為 true。 不過踪古,配置過多可移除的模塊可能會(huì)導(dǎo)致應(yīng)用的安裝時(shí)間增加含长。默認(rèn)為 false。只有當(dāng)您想要針對(duì)某個(gè)功能模塊停用整合功能時(shí)伏穆,才需要在清單中設(shè)置此值拘泞。注意:只有在使用 Android Gradle 插件 4.2 或從命令行使用 bundletool v1.0 時(shí),才能使用此功能蜈出。
</dist:install-time>
<dist:on-demand/> 指定模塊應(yīng)支持按需下載田弥。也就是說,模塊在安裝時(shí)不會(huì)下載铡原,但應(yīng)用可以稍后請(qǐng)求下載。如需詳細(xì)了解按需下載商叹,請(qǐng)參閱配置按需分發(fā)燕刻。
</dist:delivery>
`<applicationandroid:hasCode="true false">...</application>` 如果功能模塊沒有生成 DEX 文件(也就是說,它不包含之后編譯成 DEX 文件格式的代碼)剖笙,您必須執(zhí)行以下操作(否則卵洗,您可能會(huì)遇到運(yùn)行時(shí)錯(cuò)誤):在功能模塊的清單中將 android:hasCode 設(shè)置為 "false"。將以下內(nèi)容添加到基本模塊的清單中:<application android:hasCode="true" tools:replace="android:hasCode"> ...</application>

2.自定義 Feature Delivery

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

分發(fā)選項(xiàng) 行為 示例用例 使用入門
安裝時(shí)分發(fā) 默認(rèn)情況下弥咪,未配置上述任何分發(fā)選項(xiàng)的功能模塊會(huì)在安裝應(yīng)用時(shí)下載过蹂。 如果應(yīng)用包含特定的指導(dǎo) Activity(比如關(guān)于如何在購物平臺(tái)上買賣商品的交互式指南),可以配置為在應(yīng)用安裝時(shí)默認(rèn)包含該功能聚至。但是酷勺,為了減小應(yīng)用的安裝大小,應(yīng)用可在用戶完成該指導(dǎo)后請(qǐng)求刪除該功能扳躬。 清單文件中加上 <dist:install-time />
按需分發(fā) 允許您的應(yīng)用按需請(qǐng)求和下載功能模塊脆诉。 如果當(dāng)前應(yīng)用支持的設(shè)備類型只有20%,那只需要先適配這20%的設(shè)備贷币,之后按需增量下載击胜。如果某些設(shè)備被淘汰,并且已無人使用役纹,可以刪除舊功能支持包偶摔,縮減安裝包大小。 自己判斷條件通過manager.startInstall(request)添加
按條件分發(fā) 允許您指定的用戶促脉,按需請(qǐng)求和下載功能模塊辰斋。 如果購物平臺(tái)應(yīng)用的用戶遍布全球策州,您可能需要支持僅在特定地區(qū)使用的支付方式。為了減小應(yīng)用的初始下載大小亡呵,您可以創(chuàng)建單獨(dú)的功能模塊處理特定類型的支付方式抽活,并將這些模塊根據(jù)用戶的注冊(cè)區(qū)域視條件安裝在用戶設(shè)備上。 創(chuàng)建功能模塊并配置按條件分發(fā)锰什。
免安裝分發(fā) Google Play 免安裝體驗(yàn)讓用戶無需在設(shè)備上安裝 APK 即可與應(yīng)用互動(dòng)下硕。用戶可以通過 Google Play 商店中的“立即體驗(yàn)”按鈕或您創(chuàng)建的網(wǎng)址體驗(yàn)?zāi)膽?yīng)用。 假設(shè)有一款游戲汁胆,游戲的前幾個(gè)關(guān)卡包含在輕量級(jí)功能模塊中梭姓。您可以啟用該模塊的免安裝體驗(yàn),這樣用戶就可以通過網(wǎng)址或“立即體驗(yàn)”按鈕體驗(yàn)游戲嫩码,而無需安裝應(yīng)用誉尖。 創(chuàng)建功能模塊并配置免安裝分發(fā)。然后铸题,應(yīng)用就可以使用 Google Play Core 庫請(qǐng)求按需下載該模塊铡恕。請(qǐng)注意,使用功能模塊以模塊化處理應(yīng)用功能只是第一步丢间。如需支持 Google Play 免安裝體驗(yàn)探熔,應(yīng)用基本模塊的下載大小和給定的啟用免安裝體驗(yàn)的功能必須滿足嚴(yán)格的大小限制。如需了解詳情烘挫,請(qǐng)閱讀通過減少應(yīng)用或游戲大小啟用免安裝體驗(yàn)诀艰。

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

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

按需模塊

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

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

// 現(xiàn)在需要將pictureMessages和promotionalFilters按需添加卤橄,要先生成一個(gè)請(qǐng)求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
        //在應(yīng)用處于前臺(tái)時(shí)開啟一個(gè)異步線程绿满,用來執(zhí)行startInstall()任務(wù)
    // 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請(qǐng)求成功或失敗的回調(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 ->  ... }

延遲安裝按需模塊

需求場(chǎng)景:某些功能,例如數(shù)據(jù)統(tǒng)計(jì)功能虽风,由于功能模塊較大棒口,為例不影響用戶初次安裝使用,首次不獲取辜膝,在首次使用過程中通過后臺(tái)去下載延遲安裝對(duì)應(yīng)模塊无牵。

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

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

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

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

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

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

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

//執(zhí)行request安裝請(qǐng)求
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.
    }

處理請(qǐng)求錯(cuò)誤

需求場(chǎng)景:由于存在可能模塊安裝失敗的問題,所以需要對(duì)這些錯(cuò)誤進(jìn)行處理

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

fun checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
            //以列表形式為每個(gè)活動(dòng)會(huì)話返回一個(gè)SplitInstallSessionState對(duì)象
        .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.
                        //如果當(dāng)前狀態(tài)是在下載中七蜘,代表其他請(qǐng)求正在下載谭溉,需要取消當(dāng)前
                        //或者延遲安裝當(dāng)前的請(qǐng)求
                    }
                }
            }
        }
}

錯(cuò)誤碼表

錯(cuò)誤代碼 說明 建議采取的措施
ACTIVE_SESSIONS<br />_LIMIT_EXCEEDED 請(qǐng)求遭到拒絕,因?yàn)楫?dāng)前至少有一個(gè)請(qǐng)求正在下載橡卤。 檢查是否有任何仍在下載的請(qǐng)求扮念,如上例所示。
MODULE_UNAVAILABLE Google Play 無法根據(jù)當(dāng)前安裝的應(yīng)用版本碧库、設(shè)備和用戶的 Google Play 帳號(hào)找到所請(qǐng)求的模塊柜与。 如果用戶無權(quán)訪問該模塊,請(qǐng)通知他們嵌灰。
INVALID_REQUEST Google Play 已收到請(qǐng)求弄匕,但該請(qǐng)求無效。 驗(yàn)證請(qǐng)求中包含的信息是否完整準(zhǔn)確沽瞭。
SESSION_NOT_FOUND 找不到指定會(huì)話 ID 對(duì)應(yīng)的會(huì)話迁匠。 如果您嘗試通過會(huì)話 ID 監(jiān)控請(qǐng)求的狀態(tài),請(qǐng)確保會(huì)話 ID 正確無誤驹溃。
API_NOT_AVAILABLE 當(dāng)前設(shè)備不支持 Play Core 庫城丧。也就是說,該設(shè)備無法按需下載和安裝功能豌鹤。 對(duì)于搭載 Android 4.4(API 級(jí)別 20)或更低版本的設(shè)備芙贫,您應(yīng)在安裝時(shí)使用 dist:fusing 清單屬性添加功能模塊。如需了解詳情傍药,請(qǐng)參閱功能模塊清單
ACCESS_DENIED 由于權(quán)限不足魂仍,應(yīng)用無法注冊(cè)該請(qǐng)求拐辽。 通常,當(dāng)應(yīng)用在后臺(tái)運(yùn)行時(shí)擦酌,會(huì)出現(xiàn)這種情況俱诸。在應(yīng)用返回到前臺(tái)時(shí)嘗試請(qǐng)求。
NETWORK_ERROR 由于出現(xiàn)網(wǎng)絡(luò)連接錯(cuò)誤赊舶,請(qǐng)求失敗睁搭。 提示用戶建立網(wǎng)絡(luò)連接或更改為其他網(wǎng)絡(luò)。
INCOMPATIBLE_WITH
_EXISTING_SESSION
該請(qǐng)求包含一個(gè)或多個(gè)已請(qǐng)求但尚未安裝的模塊笼平。 創(chuàng)建一個(gè)新請(qǐng)求园骆,該請(qǐng)求不包含應(yīng)用已請(qǐng)求的模塊,或等待所有當(dāng)前已請(qǐng)求的模塊完成安裝寓调,然后再重試請(qǐng)求锌唾。請(qǐng)注意,請(qǐng)求已安裝的模塊無法解決錯(cuò)誤。
SERVICE_DIED 負(fù)責(zé)處理請(qǐng)求的服務(wù)已終止晌涕。 請(qǐng)重試請(qǐng)求滋捶。此錯(cuò)誤代碼會(huì)作為對(duì) SplitInstallStateUpdatedListener(其狀態(tài)為 FAILED,會(huì)話 ID 為 -1)的更新提供余黎。

處理狀態(tài)更新

需求場(chǎng)景:當(dāng)更新模塊時(shí)重窟,需要對(duì)模塊進(jìn)度信息進(jìn)行反饋,那么在之前監(jiān)聽的基礎(chǔ)上惧财,根據(jù)不同的安裝狀態(tài)巡扇,來進(jìn)行信息反饋。

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.
              //下載中更新進(jìn)度條
            }
            SplitInstallSessionStatus.INSTALLED -> {
                            //在此處可缚,你可以調(diào)用你即將跳轉(zhuǎn)的Activity界面霎迫,并且可以訪問安裝后模塊
              //的所有資源,如果你設(shè)置來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)表

請(qǐng)求狀態(tài) 說明 建議采取的措施
PENDING 已接受該請(qǐng)求,即將開始下載描姚。 初始化界面組件(例如進(jìn)度欄)涩赢,向用戶提供關(guān)于下載的反饋。
REQUIRES_USER
_CONFIRMATION
下載需要用戶確認(rèn)轩勘。這很可能是由于下載內(nèi)容大小超過 10 MB筒扒。 提示用戶接受下載請(qǐng)求。如需了解詳情绊寻,請(qǐng)轉(zhuǎn)到有關(guān)如何獲取用戶確認(rèn)的部分花墩。
DOWNLOADING 下載正在進(jìn)行中。 如果您為下載提供了進(jìn)度條澄步,請(qǐng)使用 SplitInstallSessionState.bytesDownloaded()SplitInstallSessionState.totalBytesToDownload() 方法更新界面(請(qǐng)參見此表上方的代碼示例)冰蘑。
DOWNLOADED 設(shè)備已下載模塊,但尚未開始安裝村缸。 應(yīng)用應(yīng)啟用 SplitCompat祠肥,以便訪問已下載的模塊并避免出現(xiàn)此狀態(tài)。必須執(zhí)行此操作才能訪問功能模塊的代碼和資源梯皿。
INSTALLING 設(shè)備當(dāng)前正在安裝該模塊仇箱。 更新進(jìn)度條。此狀態(tài)通常較短东羹。
INSTALLED 該模塊已安裝在設(shè)備上剂桥。 訪問模塊中的代碼和資源以繼續(xù)用戶操作流程。如果該模塊針對(duì)的是在 Android 8.0(API 級(jí)別 26)或更高版本設(shè)備上運(yùn)行的 Android 免安裝應(yīng)用百姓,您需要使用 splitInstallHelper 才能利用新模塊更新應(yīng)用組件渊额。
FAILED 在模塊安裝到設(shè)備上之前,請(qǐng)求已失敗。 提示用戶重試請(qǐng)求或取消請(qǐng)求旬迹。
CANCELING 設(shè)備正在取消請(qǐng)求火惊。 如需了解詳情,請(qǐng)轉(zhuǎn)到有關(guān)如何取消安裝請(qǐng)求的部分奔垦。
CANCELED 請(qǐng)求已取消屹耐。

獲取用戶確認(rèn)

需求場(chǎng)景:用戶當(dāng)前在app上使用移動(dòng)數(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.
        // 顯示選擇對(duì)話框
        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)
  }
}

請(qǐng)求的狀態(tài)會(huì)根據(jù)用戶響應(yīng)進(jìn)行更新:

  • 如果用戶選擇“下載”,請(qǐng)求狀態(tài)會(huì)更改為 PENDING 并繼續(xù)下載犯眠。
  • 如果用戶選擇“取消”按灶,請(qǐng)求狀態(tài)會(huì)更改為 CANCELED
  • 如果用戶在對(duì)話框被銷毀之前未做出選擇筐咧,請(qǐng)求狀態(tài)會(huì)保持為 REQUIRES_USER_CONFIRMATION鸯旁。您的應(yīng)用可能會(huì)再次提示用戶完成請(qǐng)求。

訪問模塊

需求場(chǎng)景:如需在下載后從已下載的模塊訪問代碼和資源量蕊,您的應(yīng)用需要為應(yīng)用和應(yīng)用下載的功能模塊中的每個(gè) Activity 啟用 SplitCompat 庫铺罢。例如,下載的模塊b中存在我要啟動(dòng)的ActivityB残炮,為了訪問到ActivityB韭赘,我需要啟動(dòng)SplitCompat庫。

1.啟動(dòng)SplitCompat庫:

方式1.如需啟用 SplitCompat势就,最簡(jiǎn)單的方法是在您的應(yīng)用清單中將 SplitCompatApplication 聲明

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

方式2.在運(yùn)行時(shí)調(diào)用SplitCompat

class MyApplication : SplitCompatApplication() {
    override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // 判斷模塊是否安裝泉瞻,如果您的按需模塊可同時(shí)與免安裝應(yīng)用和安裝式應(yīng)用兼容
    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)
}

訪問模塊代碼和資源

需求場(chǎng)景1:對(duì)應(yīng)的模塊已經(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
            }
        }
    }
}

需求場(chǎng)景2 :Android 8.0 及更高版本上的 Android 免安裝應(yīng)用

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                                //版本大于8.0
                if (BuildCompat.isAtLeastO()) {
                                        //由于時(shí)面安裝應(yīng)用抱完,所以不能使用createPackageContext
                    //所以需要使用updateAppInfo來實(shí)現(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”)
                }
            }
        }
    }
}

管理已安裝模塊

需求場(chǎng)景1:當(dāng)前想知道設(shè)備已安裝的功能模塊

val installedModules: Set<String> = splitInstallManager.installedModules

需求場(chǎng)景2: 想要卸載某些模塊

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

管理語言安裝包

需求場(chǎng)景1: 下載某些語言資源

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

// 創(chuàng)建請(qǐ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í)行語言請(qǐng)求安裝包
splitInstallManager.startInstall(request)

需求場(chǎng)景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()
  }
  ...
}

需有場(chǎng)景3: 卸載語言資源

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

3. APK包校驗(yàn)

全量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

解壓以后會(huì)有倆個(gè)文件夾instant和splits禁添,如果sdk版本支持低于21,即還會(huì)有另外一個(gè)standlones文件桨踪,該文件不支持按需加載老翘。instant目錄代表支持免安裝的apk資源,而splits,則是按需加載的apk資源铺峭,其中我們的自適應(yīng)功能包的apk會(huì)在其中墓怀。

所以,我們需要檢查是否對(duì)應(yīng)的Dynamic Feature module中是否有與之對(duì)應(yīng)的apk包卫键。

生成設(shè)備所需apks

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

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

//5.通過命令查看當(dāng)前目錄下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的關(guān)聯(lián)

image-20210508161627475.png

以官方提供的demo為例,instant目錄下絮宁,由于根據(jù)設(shè)備生成梆暮,所以生成了master、xxhdpi绍昂、zh對(duì)應(yīng)base module啦粹。另外,底下的split和url module治专,代表著這倆個(gè)模塊允許下載免安裝使用卖陵,接下里我們看下它們清單文件中的配置信息。

<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张峰,即免安裝使用開啟時(shí)會(huì)擁有該模塊的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代表著該目錄底下都是對(duì)應(yīng)的dynamic-feature類型的module,但有一點(diǎn)特殊的地方喘批,紅框內(nèi)生成的base模塊下的master撩荣、xxhdpi、zh饶深,跟instant中大小都是一致的餐曹,除此之外的其余包,都可以認(rèn)為是dynamic-feature類型的module生成的apk包敌厘。

<!-- dist:title 決定了我們的module名字 -->
<!-- dist:fusing dist:include="true" 支持sdk19-21的apk版本台猴,由于我們根據(jù)當(dāng)前設(shè)備生成的,當(dāng)前設(shè)備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的理解,每個(gè)module都有自己的master包宪彩,xxhdpi時(shí)根據(jù)設(shè)備分辨率生成的包休讳,這里有一個(gè)需要注意的,當(dāng)涉及到ndk時(shí)尿孔,還有一個(gè)arm64_v8a包俊柔,現(xiàn)今手機(jī)基本都是arm系列64位的筹麸,所以這個(gè)包也是最常見的包。

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

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

分析官方Demo

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

1.生成bundle包物赶,并打本地測(cè)試包

在打測(cè)試包之前,需要通過頂部菜單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,查詢當(dāng)前已安裝模塊
private lateinit var manager: SplitInstallManager
//還是通過工廠類獲取manager
manager = SplitInstallManagerFactory.create(this)

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

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

在了解apks和module關(guān)聯(lián)時(shí)需五,有提到instant目錄擁有split鹉动、url,而initialInstall是在splits目錄下宏邮,仔細(xì)查詢了下泽示,在主入口時(shí)并沒有進(jìn)行安裝處理,但在initialInstall的清單文件中發(fā)現(xiàn)了些端倪蜜氨。

<dist:module
    dist:title="@string/title_module_initial">
    <dist:fusing dist:include="true" />
    <!-- dist:install-time 就是決定安裝時(shí)械筛,便進(jìn)行下載安裝initialInstall的關(guān)鍵 -->
    <dist:delivery>
        <dist:install-time />
    </dist:delivery>
</dist:module>
3.訪問未安裝模塊kotlin
// 判斷是否已安裝,安裝執(zhí)行跳轉(zhuǎn)
if (manager.installedModules.contains(name)) {
  jumpKotlinAcitivity()
  return
}
// 未安裝新建請(qǐng)求
val request =
    SplitInstallRequest
        .newBuilder()
        .addModule("kotlin")
        .build()
//執(zhí)行安裝請(qǐng)求
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í)行完上述代碼后埋哟,我們能知道默認(rèn)已安裝的module為,[initialInstall, split, url, kotlin]

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

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

//langsInstall為true赤赊,走之前的SplitInstallStateUpdatedListener

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

5.訪問其他Module的資源
// 1.與訪問kotlin模塊一樣的安裝方式抛计,訪問assets模塊
// 2.成功時(shí)回調(diào)displayAssets
private fun displayAssets() {
        // 通過重新獲取context,并且啟動(dòng)SplitCompat庫
    val assetManager = createPackageContext(packageName, 0).also {
        SplitCompat.install(it)
    }.assets
    
    // 當(dāng)前assetManager此時(shí)時(shí)通過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文件吹截,可加上此標(biāo)識(shí)
<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>

與安裝其他模塊一致,只有當(dāng)手機(jī)版本大于等于23凝危,即6.0時(shí)才會(huì)安裝此module的apk包饭弓,可用于版本特殊處理。

4.組件化應(yīng)用

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

模塊劃分

1.App module

該模塊用于最基礎(chǔ)的apk打包苍匆,即集成了應(yīng)用的基本功能刘急。由于組件化+arouter(阿里路由框架),需要一個(gè)空殼模塊來符合組件化的設(shè)計(jì)理念浸踩,并且大量的基本功能業(yè)務(wù)邏輯叔汁,也不允許我們將app作為一個(gè)單獨(dú)的module開發(fā)。所以app模塊在我們?cè)O(shè)計(jì)中检碗,應(yīng)該是一個(gè)空殼据块,該空殼會(huì)去持有倆類基本module,第一個(gè)是定制的UI module折剃,第二個(gè)是擁有的Basic Feature module另假。

2.UI module

指的是涉及到我們公共UI的模塊,例如主入口界面怕犁、通用的Dialog边篮、Fragment界面等。由于涉及界面交互時(shí)奏甫,通暢有網(wǎng)絡(luò)請(qǐng)求戈轿、數(shù)據(jù)處理、工具類或自定義View的使用阵子、拓展方法等思杯,所以UI module需要持有一層common module,即公用的模塊款筑,這些模塊大體可以分為Network(網(wǎng)絡(luò)請(qǐng)求和網(wǎng)絡(luò)請(qǐng)求涉及的bean類)智蝠、Utils(工具類、kotlin拓展方法)奈梳、Common(base Activity等UI基礎(chǔ)類杈湾、動(dòng)態(tài)通用彈窗、自定義View)攘须、Resource(顏色表漆撞、公用資源、風(fēng)格資源)

3.Basic Feature module

指的是基礎(chǔ)功能模塊于宙,例如Pay module浮驳、Bluetooth module,都可以作為摸個(gè)單純的功能模塊捞魁。在設(shè)計(jì)這些模塊的時(shí)候至会,需要注意的是外部調(diào)用,我們需要把支付流程或藍(lán)牙的一切行為邏輯谱俭,抽取對(duì)應(yīng)的接口或抽象類出來奉件,通過傳入其實(shí)現(xiàn)類宵蛀,實(shí)現(xiàn)某個(gè)功能的黑盒操作,例如藍(lán)牙自動(dòng)打開县貌、掃描术陶、配對(duì)、回調(diào)可以通信煤痕,這一切流程都在bluetooth中完成梧宫。

4.Network module

網(wǎng)絡(luò)模塊,主要涉及接口的定義修改摆碉,該模塊只含UI module中基礎(chǔ)所需的網(wǎng)絡(luò)請(qǐng)求塘匣,如果涉及到Feature module的網(wǎng)絡(luò)請(qǐng)求,則可以在該module中單獨(dú)開network包實(shí)現(xiàn)兆解,避免隨著功能module的網(wǎng)絡(luò)請(qǐng)求需求馆铁,而頻繁修改Network module。

5.Utils module

該模塊涉及所有可以公用的工具類锅睛、kotlin可訪問到的bean類的拓展類埠巨,特殊bean類的拓展,在其功能模塊中單獨(dú)持有utils包现拒。

6.Common module

該module中持有BaseActivity辣垒、BaseFragment等一系列基礎(chǔ)類、以及它們的部分子類印蔬,同時(shí)持有各種自定義View勋桶、通用彈窗等。該模塊可以默認(rèn)持有Resource module侥猬,由于Common module大概率需要被其他module所持有例驹,所以可以讓其與Resource module綁定一起。

7.Dynamic-Feature module

自適應(yīng)功能模塊默認(rèn)是要持有app的退唠,本意是在app 的基礎(chǔ)上做一定的功能定制化狼荞,每個(gè)功能效果不一缩赛,并且支持bundle打包之后能做到增量下載。所以,當(dāng)存在某個(gè)需求肛度,例如藍(lán)牙設(shè)備的支持观蜗,由于每個(gè)藍(lán)牙型號(hào)其交互協(xié)議和交互邏輯可能存在不同壕探,針對(duì)公用的邏輯我們可以在app module中的bluetooth module定義琅催,而需要定制邏輯處理時(shí),那就需要利用拓展類實(shí)現(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)
}

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

2.Dynamic-Feature module使用須知

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

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

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

d.當(dāng)加載的Dynamic-Feature module apk大于10MB時(shí),需要使用用戶確認(rèn)功能才能進(jìn)行加載棕诵。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市妹孙,隨后出現(xiàn)的幾起案子秋柄,更是在濱河造成了極大的恐慌,老刑警劉巖蠢正,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骇笔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡嚣崭,警方通過查閱死者的電腦和手機(jī)笨触,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來雹舀,“玉大人芦劣,你說我怎么就攤上這事〈邪希” “怎么了持寄?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)娱俺。 經(jīng)常有香客問我稍味,道長(zhǎng),這世上最難降的妖魔是什么荠卷? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任模庐,我火速辦了婚禮,結(jié)果婚禮上油宜,老公的妹妹穿的比我還像新娘掂碱。我一直安慰自己怜姿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布疼燥。 她就那樣靜靜地躺著沧卢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪醉者。 梳的紋絲不亂的頭發(fā)上但狭,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音撬即,去河邊找鬼立磁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛剥槐,可吹牛的內(nèi)容都是我干的唱歧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼粒竖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼颅崩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起温圆,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤挨摸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后岁歉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體得运,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年锅移,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了熔掺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡非剃,死狀恐怖置逻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情备绽,我是刑警寧澤券坞,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站肺素,受9級(jí)特大地震影響恨锚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜倍靡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一猴伶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦他挎、人聲如沸筝尾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筹淫。三九已至,卻和暖如春呢撞,著一層夾襖步出監(jiān)牢的瞬間贸街,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工狸相, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捐川。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓脓鹃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親古沥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瘸右,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容