文章目录
创建module
依赖
主模块App需要进行一下依赖:
implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.android.play:core:1.10.0'
Application
主模块App的Application需要继承SplitCompatApplication
class App : SplitCompatApplication()
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="cn.xxstudy.demo">
<!--表示应用程序模块即时启用,-->
<dist:module dist:instant="true" />
<application
android:name=".DemoApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PlayCoreDemo">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Dynamic Feature Module
该模块功能代码层面我们就当一个普通module去处理就行,只是有几点不同:
- build.gradle
plugins {
id 'com.android.dynamic-feature'
...
}
dependencies {
//它是可以依赖app模块的,因此如果app模块的依赖方式是api,那么改模块也是可以共用其依赖的
implementation project(":app")
...
}
- AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="cn.xxstudy.login_feature">
<dist:module
//指定是否应通过 Google Play 免安装体验为模块启用免安装体验。
//在设置 <dist:on-demand/> 时,不能将此 XML 元素设置为 true
dist:instant="false"
dist:title="@string/title_login_feature">
//封装自定义模块分发的选项,如下所示。
//请注意,每个功能模块必须只配置这些自定义分发选项的一种类型。
<dist:delivery>
<dist:on-demand />
</dist:delivery>
//熔断性,如果是5.0一下的版本则打包为全量包
<dist:fusing dist:include="true" />
</dist:module>
<application>
//这是我们测试的登录Activity 注意Activity不能设置为exported:true 因为别的app不知道该模块是否已经下载
<activity
android:name=".LoginActivity"
android:launchMode="singleTask" />
</application>
</manifest>
功能模块清单
属性 | 说明 |
---|---|
<manifest ... |
这是您的典型 <manifest> 块。 |
xmlns:dist="http://schemas.android.com/apk/distribution" |
指定一个新的 dist: XML 命名空间,如下所述。 |
split="split_name" |
当 Android Studio 构建 app bundle 时,会包含该属性。因此,您不应自行添加或修改此属性。 定义模块的名称,当应用使用 Google Play 核心库发出按需模块请求时会指定该名称。 Gradle 如何确定该属性的值: 默认情况下,当您使用 Android Studio 创建功能模块时,IDE 会使用您指定的模块名称,在 Gradle 设置文件中将该模块标识为 Gradle 子项目。 当您构建 app bundle 时,Gradle 会使用子项目路径的最后一个元素将此清单属性注入模块的清单。例如,如果您在 MyAppProject/features/ 目录中创建了一个新功能模块,并指定了“dynamic_feature1”作为其模块名称,IDE 会在 settings.gradle 文件中添加 ':features:dynamic_feature1' 作为子项目。构建 app bundle 时,Gradle 会将 <manifest split="dynamic_feature1"> 注入模块的清单。 |
android:isFeatureSplit="true | false"> |
当 Android Studio 构建 app bundle 时,会包含该属性。因此,您不应手动添加或修改此属性。 指定此模块为功能模块。基本模块和配置 APK 中的清单要么省略此属性,要么将其设置为 false 。 |
<dist:module |
这一新的 XML 元素定义了一些属性,这些属性可确定如何打包模块并作为 APK 分发。 |
dist:instant="true | false" |
指定是否应通过 Google Play 免安装体验为模块启用免安装体验。 如果应用包含一个或多个启用免安装体验的功能模块,您也必须为基本模块启用免安装体验。如果您使用的是 Android Studio 3.5 或更高版本,当您创建支持免安装体验的功能模块时,IDE 会为您完成此操作。 在设置 <dist:on-demand/> 时,不能将此 XML 元素设置为 true 。不过,您仍可使用 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> |
封装自定义模块分发的选项,如下所示。请注意,每个功能模块必须只配置这些自定义分发选项的一种类型。 |
<dist:install-time> |
指定模块应在安装时可用。对于未指定自定义分发选项的其他类型的功能模块,这是默认行为。 如需详细了解安装时下载,请参阅配置安装时分发。 此节点还可以指定条件,用于限定要下载模块的设备所需满足的某些要求,例如设备功能,用户所在国家/地区或最低 API 级别。如需了解详情,请参阅配置按条件分发。 |
<dist:removable dist:value="true | false" /> |
当未设置或设置为 false 时,bundletool 会在根据 bundle 生成拆分 APK 时将安装时模块整合到基本模块中。 由于整合会使拆分 APK 的数量减少,因此此设置可以提升应用的性能。当 removable 设置为 true 时:安装时模块将不会整合到基本模块中。如果您想要在将来卸载这些模块,请将其设置为 true 。 不过,配置过多可移除的模块可能会导致应用的安装时间增加。默认为 false 。只有当您想要针对某个功能模块停用融合功能时,才需要在清单中设置此值。注意:只有在使用 Android Gradle 插件 4.2 或从命令行使用 bundletool v1.0 时,才能使用此功能。 |
</dist:install-time> |
|
<dist:on-demand/> |
指定应以按需下载的形式分发模块。也就是说,模块在安装时不会下载,但应用可以稍后请求下载。 如需详细了解按需下载,请参阅配置按需分发。 |
</dist:delivery> |
|
<application [android:hasCode](https://developer.android.google.cn/guide/topics/manifest/application-element#code)="true | false"> ... </application> |
如果功能模块没有生成 DEX 文件(也就是说,它不包含之后编译成 DEX 文件格式的代码),您必须执行以下操作(否则,您可能会遇到运行时错误): 1. 在功能模块的清单中将 android:hasCode 设置为 "false" 。2. 将以下内容添加到基本模块的清单中: <application<br> android:hasCode="true"<br> tools:replace="android:hasCode"><br> ...<br></application>
|
App主模块
- 检查需要调用的模块是否安装
- 如果没有安装则下载,这个过程是googlePlayCore库处理的
- 如果已安装通过全类名进行调用访问
class MainActivity : AppCompatActivity() {
val muduleName = "login_feature"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val manager = SplitInstallManagerFactory.create(this)
//安装模块监听
manager.registerListener {
val status=it.status()
Log.d("TAG", it.toString())
}
loginModule.setOnClickListener {
//模块已安装
if (manager.installedModules.contains(muduleName)) {
val intent = Intent()
val componentName =
ComponentName(packageName, "cn.xxstudy.login_feature.LoginActivity")
intent.component = componentName
startActivity(intent)
} else {
//安装模块:
GlobalScope.launch {
try {
manager.requestInstall(listOf(muduleName))
} catch (e: Exception) {
MainScope().launch {
Toast.makeText(this@MainActivity, "该模块还未安装成功", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(newBase)
//必须添加
SplitCompat.install(this)
}
}
manager.registerListener
安装监听status说明:
SplitInstallSessionStatus.CANCELED -> 模块下载已被取消
SplitInstallSessionStatus.CANCELING -> 正在取消下载
SplitInstallSessionStatus.DOWNLOADING -> Installing(
state.bytesDownloaded.toDouble() / state.totalBytesToDownload
)下载进度
SplitInstallSessionStatus.DOWNLOADED -> 下载完成但未安装
SplitInstallSessionStatus.FAILED ->下载或安装失败
SplitInstallSessionStatus.INSTALLED -> 安装完成
SplitInstallSessionStatus.INSTALLING -> 安装中
SplitInstallSessionStatus.PENDING -> 将要进行下载
SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> 需要用户确认,大于10M
SplitInstallSessionStatus.UNKNOWN -> 未知错误
本地构建调试
- 调试
接下来运行后login_fuature模块就不会被安装了,当然也是无法使用该模块的
- 生成.aab
- Build→Build Bundle(s)/ APK→Build Bundle(s)
- Build→Generate Sigend Bundle Apk →Android App Bundle
- 通过BundleTool生成.apks
java -jar F:\Temp\bundletool.jar build-apks --local-testing --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks
- 安装
java -jar F:\Temp\bundletool.jar install-apks --apks=app.apks
编译过程:
E:\Android\Simple\Temp\PlayCoreDemo>java -jar F:\Temp\bundletool.jar build-apks --local-testing --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks
INFO: The APKs will be signed with the debug keystore found at 'C:\Users\DELL\.android\debug.keystore'.
E:\Android\Simple\Temp\PlayCoreDemo>java -jar F:\Temp\bundletool.jar install-apks --apks=app.apks
The APKs have been extracted in the directory: C:\Users\DELL\AppData\Local\Temp\1369014306226025145
The APKs have been extracted in the directory: C:\Users\DELL\AppData\Local\Temp\1369014306226025145
ADB << rm -rf '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing'
ADB >> OK
ADB << mkdir -p '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing' && rmdir '/sdcard/Android/data/cn.xxstudy.demo/files/local_testing' && mkdir -p '/sdcard/Android/data/cn.xxst
udy.demo/files/local_testing'
ADB >> OK
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-xxhdpi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-master.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ca.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-da.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fa.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ja.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ka.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pa.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ta.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-nb.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-be.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-de.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ne.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-te.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-af.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bg.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-th.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-si.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-vi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-kk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-uk.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-el.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-gl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ml.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-nl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-tl.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-am.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-km.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-in.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-kn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mn.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ko.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lo.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ro.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sq.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ar.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-fr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-mr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-or.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-tr.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ur.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-as.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-bs.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-cs.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-es.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-is.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ms.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-et.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-it.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lt.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-pt.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-eu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-gu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ru.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-zu.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-lv.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sv.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-iw.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-sw.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-hy.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-ky.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-my.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-az.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-uz.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-en.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/base-zh.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/login_feature-xxhdpi.apk"
Pushed "/sdcard/Android/data/cn.xxstudy.demo/files/local_testing/login_feature-master.apk"
这个时候就将login_feature模块导入到设备中去了,执行manager.requestInstall(listOf(muduleName))
后就会进行安装了(一般是通过googlePlay下载,我们这里是使用BundleTool本地测试)
拆分注意事项
-
dynamic-feature- moudle
引用base moudle
资源时,不能直接使用R.drawdble
需要使用[base moudle packagename].R.drawdble
的方式 -
dynamic- feature-module
项目名称不能以数字开头 -
java.lo.lOException: Cannot find PROCESSED_ RES output for Main{type=MAIN, fullName=flavor1Debug, filters=I, versionCode=-1, versionName=null}
异常需要注释掉build.gradle
的splite {abi{}}
-
base moudle
不可以访问dynamic-feature-module
中的id
dynamic-feature-module
中arssc
文件中资源索引id
的值为0x7e
base moudle
中arssc
文 件中资源索引id
的值为0x7f
因为feature
与baseMoude
都有各自的arsc
文件,虽然属性名称一直 但是id
值是不-致的,所以basemoude
中涉及访问feature moudle
的id
值都需要修改 -
动态模块配置模块名title必须通过如下方式
dist:title="@string/title_ dynamic. feature"
不能直接编写字符串,并且该字符串必须写在base moudle
中 -
dynamic-feature moudle
与base moudle
的manifest
文件最终会合并成一个manifest
文件,所以要保障manifest
的资源引用均在base moudle
中。 -
当打开新建
dynamic-feature moudle
并启用了on-demand
(按需加载) 能力时,必须开启Fusing
(熔断操作)才能正常的让Api21
以下的设备正常使用module
-
一般情况下,动态模块下发之后需要重启
App
才能加载成功,但是如果你使用SplitCompat
加载唤起动态模块,就可以立即生效 -
如果下载的模块太大,需要用户确认,googlePlay要求大于10MB需要用户确认
-
dynamic- feature moudle
中的AndroidManifest
中定义的Activity
不能有exported:true
因为别的app
不知道你何时安装好模块从而会引发问题
问题
首次安装后不重启直接打开crash No package ID 7e found for ID 0x7e020000.
由于App 模块中arssc文件中资源索引id是0x7f 而feature module是0x7e 暂时还不知道具体原因
重启可解决!
2021-07-06 18:37:45.972 6029-6029/cn.xxstudy.demo E/cn.xxstudy.dem: No package ID 7e found for ID 0x7e020000.
2021-07-06 18:37:45.973 6029-6029/cn.xxstudy.demo D/AndroidRuntime: Shutting down VM
2021-07-06 18:37:45.974 6029-6029/cn.xxstudy.demo E/AndroidRuntime: FATAL EXCEPTION: main
Process: cn.xxstudy.demo, PID: 6029
java.lang.RuntimeException: Unable to start activity ComponentInfo{cn.xxstudy.demo/cn.xxstudy.login_feature.LoginActivity}: android.content.res.Resources$NotFoundException: Resource ID #0x7e020000
...
Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7e020000
at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:237)
at android.content.res.Resources.loadXmlResourceParser(Resources.java:2281)
at android.content.res.Resources.getLayout(Resources.java:1175)
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:555)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
at cn.xxstudy.login_feature.LoginActivity.onCreate(LoginActivity.kt:19)
...
结束语
至此App Bundle构建可动态化下载模块的App就结束了,其中可能会有很多坑,可以去官网看看
Google也已经发布说明在2021年8.1以后新应用上传到Google Play必须是.aab格式了,虽然国内无法使用但是还是要学习一下的,这里也放一个视频供大家参考(需要*)
AppBundle1
AppBundle2