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)。
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)建
新建流程
- 如需打開 New Module 對(duì)話框侨歉,請(qǐng)從菜單欄中依次選擇 File > New > New Module屋摇。
- 在 New Module 對(duì)話框中,選擇 Dynamic Feature Module幽邓,然后點(diǎn)擊 Next炮温。
- 像往常一樣配置模塊,然后點(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的完整性
解壓以后會(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)
以官方提供的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>
接下來看下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版本