Android 使用Google Core动态功能交付(Feature Delivery)
介绍
动态功能交付(Feature Delivery),是谷歌Google Play Core 库为谷歌商店发布的应用所提供的功能之一。
使用Play Core 库还可以提供以下这些功能:
- 下载其他语言资源
- 管理功能模块分发
- 管理资源包分发
- 触发应用内更新
- 请求应用内评价
本文的重点是说明功能模块分发与资源包分发的使用。
Android App Bundle
Android App Bundle 是一种发布格式(.aab),这种格式和传统的apk类似,都会包含应用的所有经过编译的代码和资源。
不同之处在于将这种格式不能直接安装,而开发者需要将包上传至Google Play之后,Google Play会使用这个 App Bundle 包针对每种设备配置生成并提供经过优化的 APK,即 APK 的生成及签名交由了 Google Play 来完成。
如果要使用动态功能交付或资产交付,则必须要使用App Bundle方式打包。
如上图所示,传统的apk打包是将这些组成部分打包成一个压缩包,即apk包。而app bundle打包的aab文件也是包含以上所有内容,但是在将这个aab格式的包上传至Google Play后,用户进入商店选择下载应用时,下载的包将不是包含所有内容的包,而是只包含该用户手机所对应的相应类型资源的包。
如该用户的手机是使用英语、xhdpi、arm-v7架构cpu的设备,那么该用户下载到的包将只包含(如果你启用了这些类型的拆分)这些类型对应的语言、资源图、lib库文件。显而易见,这种方式将有助于缩小用户下载的包大小与减小应用所占用的空间。
而将现有的项目修改为app bundle打包的方法也很简单:
-
在项目gradle中引入google core库
// In your app’s build.gradle file: ... 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.9.1' // For Kotlin users also import the Kotlin extensions library for Play Core: implementation 'com.google.android.play:core-ktx:1.8.1' ... }
-
在项目的app模块的build.gradle文件中添加以下内容:
android { // 在gradle的android结点下
bundle {
density {
// Different APKs are generated for devices with different screen densities; true by default.
enableSplit true
}
abi {
// Different APKs are generated for devices with different CPU architectures; true by default.
enableSplit true
}
language {
// This is disabled so that the App Bundle does NOT split the APK for each language.
// We're gonna use the same APK for all languages.
enableSplit true
}
}
}
- 在打包时选择app bundle打包即可
这样打出来的包就是aab格式的包,发版时将这个aab文件传到谷歌后台即可。如果需要测试这个aab包的功能,则需要使用谷歌的bundletool工具来安装aab包到测试机上。
资产交付(Asset Delivery)
通常,对于用户不会立刻使用到的一些比较占用空间的assets资源,如大量图片资源、机器学习模型参数包、较大的视频资源等,可以使用动态资产交付。
具体的使用方式为,将这些资源单独拆分在一个Module的assets文件夹下,并将该Module设置为动态分发的模块。
动态资产交付主要有三种交付模式,分别为:
- install-time:安装时分发,应用安装时就会下载,安装后可立即使用,资产包的大小会计入应用详情页的下载大小
- fast-follow:快速跟进式分发,应用安装后会立即开始下载,安装后不一定保证可以使用,资产包大小暂时还不会计入详情页大小
- on-demand:按需分发,资产包不会主动下载,需要代码在特定时机向用户征求开始下载,确保下载后才可使用,资产包大小不会计入详情页大小
这三种交付模式可以在资源Module的AndroidManifest.xml中设置。
代码中,对于动态分发的资源的访问,install-time与另外两种模式不同,对于install-time的模块,与常规资源一样使用AssertManager访问,官方代码示例为:
import android.content.res.AssetManager;
...
Context context = createPackageContext("com.example.app", 0);
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("asset-name");
而对于另外两种模式,要访问动态分发模块的资源,则要复杂许多。
首先需要AssetPackManager#getPackLocation()方法获取 Asset Pack 的根文件夹。如果返回值存在,则说明动态模块可用,如果为null,则需要代码申请下载并安装该模块才可用。流程如下图,由于不是本文重点,故详细代码不列出来了。具体可以查看官方文档。
动态功能交付(Feature Delivery)
介绍
动态功能交付与资产交付类似,同样是需要单独拆分出一个模块,而且这个模块与常规的功能模块存在很大的不同。
我们通常添加一个模块,是希望这个模块中的代码能被多个项目复用,所以app模块与这个feature模块的引用关系为:app引用feature模块。
这种引用关系决定了app模块中可以随意使用feature模块中的代码和资源,而feature模块无法访问app中的资源和代码。
在动态功能交付中,这种引用关系需要反过来,feature模块引用app模块。所以feature中可以随意访问app中的代码和资源,而反过来则不行。
接下来,我们演示一下将一部分代码和资源从原有的项目中拆分出来。
在我负责的项目中,由于opencv的so文件占用的空间太大,产品不满意,所以决定将openCV对应的so文件和相应的activity页面与功能代码拆分出来配置成动态功能交付。
使用方式
首先,先新建一个Module,可以直接在AS中选择创建动态功能交付的Module,然后根据提示填好Module的Name和Title。
创建完成之后,确认以下配置:
app的gradle文件中,以动态模块的形式引入了刚刚创建的模块
android { // android结点下
dynamicFeatures = [':opencvfeature']
}
创建模块的gradle文件中,引入了app模块
dependencies {
implementation project(":app")
}
新创建的模块AndroidManifest.xml文件中,已经添加了以下内容:
<dist:module dist:title="@string/module_name">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
确认以上内容后,一个动态功能交付的模块就创建完成了。接下来要做的工作就是将原app模块中需要拆分的代码迁移到新创建的这个模块中,并消除app模块对这部分代码的引用(即能成功编译)。这部分工作和每个人的项目相关,有些甚至需要重构功能代码,所以就不展开讲了。
在这个过程中可能会发现一个问题,那就是app模块无法引用动态模块中的代码和资源后,那动态模块我要如何使用呢?无法引用其中的类,也就无法调用其中的功能方法,那动态分发的意义在哪里?
这个问题的答案也很简答:使用反射。
对于调用动态模块中的代码,使用反射的方式去调用对应类中的方法。对于跳转动态模块中的activity,使用Intent指定类名和包名即可,示例如下。
Intent intent = new Intent();
intent.setClassName(getPackageName(), "com.xxx.xxx.XXXActivity");
startActivityForResult(intent, requestCode);
修改代码完成后,工程目录大致如下,其中opencvfeature就是我拆分出的动态功能模块。
完成了以上工作,还有一个重要的工作没有完成,即使用该模块的功能前,判断模块是否安装以及申请下载安装该模块,毕竟我们的功能模块设置的是on-demand(按需交付)模式。
private SplitInstallManager splitInstallManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
splitInstallManager = SplitInstallManagerFactory.create(this); // 创建manager
}
@Override
public void onResume() {
// ...
splitInstallManager.registerListener(splitInstallStateUpdatedListener);
}
@Override
public void onPause() {
// ...
splitInstallManager.unregisterListener(splitInstallStateUpdatedListener);
}
// 使用动态模块功能
public void tryUseDynamicFeature() {
String moduleName = FeatureManager.getInstance().getModuleName();
if (splitInstallManager.getInstalledModules().contains(moduleName)) {
// 已下载对应的模块
// TODO 使用反射代码
} else { // 没有下载模块,需要申请下载
SplitInstallRequest request = SplitInstallRequest.newBuilder().addModule(moduleName).build();
splitInstallManager.startInstall(request);
boolean isGoogleCanUse = PhoneUtil.isGooglePlayServiceAvailable(this);
if (isGoogleCanUse) {
T.show("Starting install for " + moduleName);
} else {
T.show("The Google service appears to be unavailable and the installation cannot begin");
}
}
}
// 动态模块的状态监听
private final SplitInstallStateUpdatedListener splitInstallStateUpdatedListener = state -> {
switch (state.status()) {
case SplitInstallSessionStatus.DOWNLOADING: {
// 正在下载
}
break;
case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: {
// 需要用户确认
}
break;
case SplitInstallSessionStatus.INSTALLED: {
// 安装模块成功
}
break;
case SplitInstallSessionStatus.INSTALLING: {
// 正在安装
}
break;
case SplitInstallSessionStatus.FAILED: {
// 下载模块失败
}
break;
default:
break;
}
};
动态交付的优势
- 减少必要apk的体积,使得在流量分发中更具优势
- 使用谷歌官方服务器分发,稳定性更有保障,不需要消耗宝贵的服务器资源
- 下载过程由google自身处理,支持断点续传等
注意事项
- 动态功能交付和资产交付不同,只有两种模式,install-time和on-demand,没有提供fast-follow
- 测试动态功能交付需要使用Google Play提供的测试服功能,将包当作测试包传上商店,将测试的账号加入测试账号列表,即可测试