App Bundle构建可动态化下载模块的App

文章目录

创建module

App Bundle构建可动态化下载模块的App
App Bundle构建可动态化下载模块的App
App Bundle构建可动态化下载模块的App

依赖

主模块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 -> 未知错误

本地构建调试

  1. 调试
    App Bundle构建可动态化下载模块的App
    接下来运行后login_fuature模块就不会被安装了,当然也是无法使用该模块的
    App Bundle构建可动态化下载模块的App
  2. 生成.aab
  • Build→Build Bundle(s)/ APK→Build Bundle(s)
  • Build→Generate Sigend Bundle Apk →Android App Bundle
  1. 通过BundleTool生成.apks
java -jar F:\Temp\bundletool.jar  build-apks --local-testing --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks
  1. 安装
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本地测试)
App Bundle构建可动态化下载模块的App

拆分注意事项

  1. dynamic-feature- moudle 引用base moudle 资源时,不能直接使用R.drawdble需要使用[base moudle packagename].R.drawdble的方式

  2. dynamic- feature-module项目名称不能以数字开头

  3. java.lo.lOException: Cannot find PROCESSED_ RES output for Main{type=MAIN, fullName=flavor1Debug, filters=I, versionCode=-1, versionName=null}异常需要注释掉build.gradlesplite {abi{}}

  4. base moudle不可以访问dynamic-feature-module中的id dynamic-feature-modulearssc文件中资源索引id的值为0x7e base moudlearssc文 件中资源索引id的值为0x7f因为featurebaseMoude都有各自的arsc文件,虽然属性名称一直 但是id值是不-致的,所以basemoude中涉及访问feature moudleid值都需要修改

  5. 动态模块配置模块名title必须通过如下方式dist:title="@string/title_ dynamic. feature"不能直接编写字符串,并且该字符串必须写在base moudle

  6. dynamic-feature moudlebase moudlemanifest文件最终会合并成一个manifest文件,所以要保障manifest的资源引用均在base moudle中。

  7. 当打开新建dynamic-feature moudle 并启用了on-demand(按需加载) 能力时,必须开启Fusing(熔断操作)才能正常的让Api21以下的设备正常使用module

  8. 一般情况下,动态模块下发之后需要重启App才能加载成功,但是如果你使用SplitCompat加载唤起动态模块,就可以立即生效

  9. 如果下载的模块太大,需要用户确认,googlePlay要求大于10MB需要用户确认

  10. 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

上一篇:使用 webpack-dev-server 自动刷新不成功


下一篇:Android Fragment(一):基本使用