借助功能模块,您可以从应用的基本模块中分离某些功能和资源,并将其纳入到 app bundle 中。然后,用户在安装应用的基本 APK 后,便可以通过 Play Feature Delivery 按需下载和安装这些组件。
例如,假设某个短信应用包含拍摄和发送图片消息的功能,但只有一小部分用户发送图片消息。合理的做法是,将图片消息功能添加为可下载的功能模块。这样,所有用户最初下载的应用所占的空间会更小,而只有那些发送图片消息的用户才需要下载该附加组件。
请注意,这种类型的模块化所需的工作量更大,并且可能需要重构应用的现有代码,因此请仔细考虑按需提供给用户对应用的哪些功能益处最大。如需更好地了解有关按需功能的最佳用例和准则,请阅读有关按需分发的用户体验最佳做法。
如果您想逐步对应用功能进行模块化处理,而不启用按需分发等高级分发选项,请改为配置安装时分发。
本文旨在帮助您向应用项目中添加功能模块,并将其配置为按需分发。开始前,请确保您使用的是 Android Studio 3.5 或更高版本以及 Android Gradle 插件 3.5.0 或更高版本。
将新模块配置为按需分发
如需创建新功能模块,最简单的方法是使用 Android Studio 3.5 或更高版本。由于功能模块本身依赖于应用的基础模块,因此您只能在创建好应用项目之后再向其中添加功能模块。
如需使用 Android Studio 向应用项目中添加功能模块,请按以下步骤操作:
- 在 IDE 中打开您的应用项目(如果您尚未打开)。
- 从菜单栏中依次选择 File(文件)> New(新建)> New Module(新建模块)。
- 在Create New Module(创建新模块)对话框中,选择 Dynamic Feature Module(动态功能模块),然后点击 Next(下一步)。
- 在 Configure your new module(配置新模块)部分中,完成以下操作:
- 从下拉菜单中选择应用项目的 Base application module(应用基础模块)。
- 指定 Module name(模块名称)。IDE 会使用此名称在 Gradle 设置文件中将该模块标识为 Gradle 子项目。当您构建 app bundle 时,Gradle 会使用子项目名称的最后一个元素在功能模块的清单中注入
<manifest split>
属性。 - 指定该模块的 package name(软件包名称)。默认情况下,Android Studio 会给出一个软件包名称建议,该名称由基础模块的根软件包名称和您在上一步中指定的模块名称组合而成。
- 选择您希望该模块支持的 Minimum API level(最低 API 级别)。此值应与基础模块的值一致。
- 点击 Next(下一步)。
在 Module Download Options(模块下载选项)部分中,完成以下操作:
指定最多包含 50 个字符的 Module title。例如,平台会在确认用户是否要下载模块时,使用此标题向用户标识该模块。因此,应用的基本模块必须将模块标题作为您能读懂的字符串资源纳入其中。使用 Android Studio 创建模块时,IDE 会为您将字符串资源添加到基本模块中,并在功能模块的清单中注入以下条目:
<dist:module
...
dist:title="@string/feature_title">
</dist:module>在 Install-time inclusion 下的下拉菜单中,选择 Do not include module at install-time。Android Studio 会在该模块的清单中注入以下内容来反映您的选择:
<dist:module ... >
<dist:delivery>
<dist:on-demand/>
</dist:delivery>
</dist:module>如果您希望此模块可提供给搭载 Android 4.4(API 级别 20)及更低版本的设备并包含在多 APK 中,请选中 Fusing 旁边的复选框。这意味着,您可以为此模块启用按需行为,也可以停用融合功能,以便从不支持下载和安装拆分 APK 的设备上将其省略。Android Studio 会在该模块的清单中注入以下内容来反映您的选择:
<dist:module ...>
<dist:fusing dist:include="true | false" />
</dist:module>
点击 Finish(完成)。
在 Android Studio 完成模块创建后,从 Project(项目)窗格(从菜单栏中依次选择 View [查看] > Tool Windows [工具窗口] > Project[项目])中自行检查其内容。默认代码、资源和组织应与标准应用模块类似。
接下来,您需要使用 Play Feature Delivery 库实现按需安装功能。
将 Play Feature Delivery 库添加到您的项目中
开始之前,您需要先将 Play Feature Delivery 库添加到您的项目中。
请求按需模块
当您的应用需要使用功能模块时,它可以通过 SplitInstallManager
类在前台进行请求。在发起请求时,您的应用需要指定由目标模块清单中的 split
元素所定义的模块名称。当您使用 Android Studio 创建功能模块时,构建系统会使用您提供的模块名称,在编译时将该属性注入模块清单中。如需了解详情,请参阅功能模块清单。
例如,假设某个具有按需模块的应用可使用设备的相机拍摄和发送图片消息,并且此按需模块在其清单中指定了 split="pictureMessages"
。以下示例使用 SplitInstallManager
请求 pictureMessages
模块(以及包含一些宣传过滤器的另一个模块):
// Creates an instance of SplitInstallManager.
val splitInstallManager = SplitInstallManagerFactory.create(context)
// Creates a request to install a module.
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
// 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)
// 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 -> ... }
// Creates an instance of SplitInstallManager.
SplitInstallManager splitInstallManager =
SplitInstallManagerFactory.create(context);
// Creates a request to install a module.
SplitInstallRequest 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
// 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)
// 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 -> { ... });
当您的应用请求按需模块时,Play Feature Delivery 库会采用“即发即弃”策略。也就是说,它会发送请求以将该模块下载到平台,但不会监控安装是否成功。如需在安装后继续用户体验历程或妥善处理错误,请务必监控请求状态。
注意:您可以请求已安装在设备上的功能模块。如果检测到该模块已安装,API 会立即将该请求视为已完成。此外,安装模块后,Google Play 会自动使其保持最新状态。也就是说,当您上传新版 app bundle 时,平台会更新所有属于您应用的已安装 APK。如需了解详情,请参阅管理应用更新。
如需访问模块的代码和资源,您的应用需要启用 SplitCompat。请注意,Android 免安装应用不需要使用 SplitCompat。
延迟安装按需模块
如果您不需要应用立即下载并安装按需模块,可以延迟到应用在后台运行时再安装该模块。例如,您想要预先加载一些宣传材料并在之后应用启动时使用这些材料。
您可以使用 deferredInstall()
方法指定之后需要下载的模块,如下所示。而且,与 SplitInstallManager.startInstall()
不同,您的应用无需在前台就可发起延迟安装请求。
// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(listOf("promotionalFilters"))
// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(Arrays.asList("promotionalFilters"));
收到延迟安装请求后,系统将尽力而为,您无法跟踪其进度。因此,在尝试访问您已指定为延迟安装的模块之前,应检查该模块是否已安装。如果您需要立即使用该模块,请改为使用 SplitInstallManager.startInstall()
进行请求,如上一部分中所示。
监控请求状态
为了能够更新进度条、在安装后触发 Intent 或者妥善处理请求错误,您需要监听来自异步 SplitInstallManager.startInstall()
任务的状态更新。在开始接收安装请求更新之前,请先注册监听器并获取该请求的会话 ID,如下所示。
// Initializes a variable to later track the session ID for a given request.
var mySessionId = 0
// Creates a listener for request status updates.
val listener = SplitInstallStateUpdatedListener { state ->
if (state.sessionId() == mySessionId) {
// Read the status of the request to handle the state update.
}
}
// Registers the listener.
splitInstallManager.registerListener(listener)
...
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.
}
// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener)
// Initializes a variable to later track the session ID for a given request.
int mySessionId = 0;
// Creates a listener for request status updates.
SplitInstallStateUpdatedListener listener = state -> {
if (state.sessionId() == mySessionId) {
// Read the status of the request to handle the state update.
}
};
// Registers the listener.
splitInstallManager.registerListener(listener);
...
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.
});
// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener);
处理请求错误
请注意,按需安装功能模块有时可能会失败,就像应用安装并非总能成功一样。安装失败可能是设备存储空间不足、没有网络连接或用户未登录 Google Play 商店等问题所致。如需了解有关如何从用户的角度妥善处理这些情况的建议,请参阅按需分发的用户体验指南。
在代码方面,您应该使用 addOnFailureListener()
处理下载或安装模块时出现的失败,如下所示:
splitInstallManager
.startInstall(request)
.addOnFailureListener { exception ->
when ((exception as SplitInstallException).errorCode) {
SplitInstallErrorCode.NETWORK_ERROR -> {
// Display a message that requests the user to establish a
// network connection.
}
SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads()
...
}
}
fun checkForActiveDownloads() {
splitInstallManager
// Returns a SplitInstallSessionState object for each active session as a List.
.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.
}
}
}
}
}
splitInstallManager
.startInstall(request)
.addOnFailureListener(exception -> {
switch (((SplitInstallException) exception).getErrorCode()) {
case SplitInstallErrorCode.NETWORK_ERROR:
// Display a message that requests the user to establish a
// network connection.
break;
case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
checkForActiveDownloads();
...
});
void checkForActiveDownloads() {
splitInstallManager
// Returns a SplitInstallSessionState object for each active session as a List.
.getSessionStates()
.addOnCompleteListener( task -> {
if (task.isSuccessful()) {
// Check for active sessions.
for (SplitInstallSessionState state : task.getResult()) {
if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
// Cancel the request, or request a deferred installation.
}
}
}
});
}
下表介绍了您的应用可能需要处理的错误状态:
错误代码 | 说明 | 建议采取的措施 |
---|---|---|
ACTIVE_SESSIONS_LIMIT_EXCEEDED | 请求遭到拒绝,因为当前至少有一个请求正在下载。 | 检查是否有任何仍在下载的请求,如上例所示。 |
MODULE_UNAVAILABLE | Google Play 无法根据当前安装的应用版本、设备和用户的 Google Play 账号找到所请求的模块。 | 如果用户无权访问该模块,请通知他们。 |
INVALID_REQUEST | Google Play 已收到请求,但该请求无效。 | 验证请求中包含的信息是否完整准确。 |
SESSION_NOT_FOUND | 找不到指定会话 ID 对应的会话。 | 如果您尝试通过会话 ID 监控请求的状态,请确保会话 ID 正确无误。 |
API_NOT_AVAILABLE | 当前设备不支持 Play Feature Delivery 库。也就是说,该设备无法按需下载和安装功能。 | 对于搭载 Android 4.4(API 级别 20)或更低版本的设备,您应在安装时使用 dist:fusing 清单属性添加功能模块。如需了解详情,请参阅功能模块清单。 |
NETWORK_ERROR | 由于出现网络连接错误,请求失败。 | 提示用户建立网络连接或更改为其他网络。 |
ACCESS_DENIED | 由于权限不足,应用无法注册该请求。 | 通常,当应用在后台运行时,会出现这种情况。在应用返回到前台时尝试请求。 |
INCOMPATIBLE_WITH_EXISTING_SESSION | 该请求包含一个或多个已请求但尚未安装的模块。 | 创建一个新请求,该请求不包含应用已请求的模块,或等待所有当前已请求的模块完成安装,然后再重试请求。 请注意,请求已安装的模块无法解决错误。 |
SERVICE_DIED | 负责处理请求的服务已终止。 | 请重试请求。
您的 |
INSUFFICIENT_STORAGE | 设备没有足够的可用存储空间,无法安装功能模块。 | 通知用户他们没有足够的存储空间,无法安装此功能。 |
SPLITCOMPAT_VERIFICATION_ERROR、SPLITCOMPAT_EMULATION_ERROR、SLIITCOMPAT_COPY_ERROR | SplitCompat 无法加载功能模块。 | 这些错误应该会在下次应用重启后自动得到解决。 |
PLAY_STORE_NOT_FOUND | 设备上未安装 Play 商店应用。 | 告知用户必须安装 Play 商店应用才能下载此功能。 |
APP_NOT_OWNED | Google Play 尚未安装该应用,因此无法下载功能。只有在延迟安装时才会出现此问题。 | 如果您想让用户在 Google Play 上获取应用,请使用 startInstall() ,它可以获取必要的用户确认。 |
INTERNAL_ERROR | Play 商店内发生内部错误。 | 请重试请求。 |
如果用户请求下载按需模块并出现错误,请考虑显示一个对话框并为用户提供如下两个选项:重试(再次尝试该请求)和取消(放弃该请求)。如需其他支持,您还应该提供帮助链接,引导用户访问 Google Play 帮助中心。
处理状态更新
注册监听器并记录请求的会话 ID 后,请使用 StateUpdatedListener.onStateUpdate()
处理状态变更,如下所示。
override fun onStateUpdate(state : SplitInstallSessionState) {
if (state.status() == SplitInstallSessionStatus.FAILED
&& state.errorCode() == SplitInstallErrorCode.SERVICE_DIED) {
// Retry the request.
return
}
if (state.sessionId() == mySessionId) {
when (state.status()) {
SplitInstallSessionStatus.DOWNLOADING -> {
val totalBytes = state.totalBytesToDownload()
val progress = state.bytesDownloaded()
// Update progress bar.
}
SplitInstallSessionStatus.INSTALLED -> {
// 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.
}
}
}
}
@Override
public void onStateUpdate(SplitInstallSessionState state) {
if (state.status() == SplitInstallSessionStatus.FAILED
&& state.errorCode() == SplitInstallErrorCode.SERVICE_DIES) {
// Retry the request.
return;
}
if (state.sessionId() == mySessionId) {
switch (state.status()) {
case SplitInstallSessionStatus.DOWNLOADING:
int totalBytes = state.totalBytesToDownload();
int progress = state.bytesDownloaded();
// Update progress bar.
break;
case SplitInstallSessionStatus.INSTALLED:
// 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.
}
}
}
安装请求的可能状态如下表所述。
请求状态 | 说明 | 建议采取的措施 |
---|---|---|
PENDING | 已接受该请求,即将开始下载。 | 初始化界面组件(例如进度条),向用户提供关于下载的反馈。 |
REQUIRES_USER_CONFIRMATION | 下载需要用户确认。如果尚未通过 Google Play 安装应用,通常会出现此状态。 | 提示用户确认通过 Google Play 下载该功能。 如需了解详情,请转到有关如何获取用户确认的部分。 |
DOWNLOADING | 下载正在进行中。 | 如果您为下载提供了进度条,请使用 SplitInstallSessionState.bytesDownloaded() 和 SplitInstallSessionState.totalBytesToDownload() 方法更新界面(请参见此表上方的代码示例)。 |
DOWNLOADED | 设备已下载模块,但尚未开始安装。 | 应用应启用 SplitCompat,以便访问已下载的模块并避免出现此状态。必须执行此操作才能访问功能模块的代码和资源。 |
INSTALLING | 设备当前正在安装该模块。 | 更新进度条。此状态通常较短。 |
INSTALLED | 该模块已安装在设备上。 | 访问模块中的代码和资源以继续用户体验历程。
如果该模块针对的是在 Android 8.0(API 级别 26)或更高版本设备上运行的 Android 免安装应用,您需要使用 |
FAILED | 在模块安装到设备上之前,请求已失败。 | 提示用户重试请求或取消请求。 |
CANCELING | 设备正在取消请求。 | 如需了解详情,请转到有关如何取消安装请求的部分。 |
CANCELED | 请求已取消。 |
获取用户确认
在某些情况下,Google Play 在满足下载请求之前可能需要用户确认。例如,如果您的应用并非通过 Google Play 安装,或者您正尝试通过移动流量下载大文件。在这种情况下,请求的状态会报告 REQUIRES_USER_CONFIRMATION
,您的应用需要先获得用户确认,然后设备才能下载并安装请求的模块。如需获得确认,您的应用应按以下方式提示用户:
override fun onSessionStateUpdate(state: SplitInstallSessionState) {
if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
// Displays a confirmation for the user to confirm the request.
splitInstallManager.startConfirmationDialogForResult(
state,
// an activity result launcher registered via registerForActivityResult
activityResultLauncher)
}
...
}
@Override void onSessionStateUpdate(SplitInstallSessionState state) {
if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
// Displays a confirmation for the user to confirm the request.
splitInstallManager.startConfirmationDialogForResult(
state,
// an activity result launcher registered via registerForActivityResult
activityResultLauncher);
}
...
}
您可以使用内置的 ActivityResultContracts.StartIntentSenderForResult
协定来注册 activity 结果启动器。请参阅 Activity Result API。
请求的状态会根据用户响应进行更新:
- 如果用户接受确认,请求状态会更改为
PENDING
并继续下载。 - 如果用户拒绝确认,请求状态会更改为
CANCELED
。 - 如果用户在对话框被销毁之前未做出选择,请求状态会保持为
REQUIRES_USER_CONFIRMATION
。您的应用可能会再次提示用户完成请求。
如需通过用户的响应接收回调,您可以替换 ActivityResultCallback,如下所示。
registerForActivityResult(StartIntentSenderForResult()) { result: ActivityResult -> {
// Handle the user's decision. For example, if the user selects "Cancel",
// you may want to disable certain functionality that depends on the module.
}
}
registerForActivityResult(
new ActivityResultContracts.StartIntentSenderForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
// Handle the user's decision. For example, if the user selects "Cancel",
// you may want to disable certain functionality that depends on the module.
}
});
取消安装请求
如果您的应用需要在安装之前取消请求,它可以使用请求的会话 ID 调用 cancelInstall()
方法,如下所示。
splitInstallManager
// Cancels the request for the given session ID.
.cancelInstall(mySessionId)
splitInstallManager
// Cancels the request for the given session ID.
.cancelInstall(mySessionId);
访问模块
如需在下载后从已下载的模块访问代码和资源,您的应用需要为应用和应用下载的功能模块中的每个 activity 启用 SplitCompat 库。
不过请注意,在下载模块后的一段时间(在某些情况下是数天),该平台在访问模块内容时会受到以下限制:
- 平台无法应用模块引入的任何新的清单条目。
- 平台无法访问系统界面组件(如通知)的模块资源。如果您需要立即使用此类资源,请考虑将这些资源添加到应用的基本模块中。
启用 SplitCompat
为了让您的应用从已下载的模块访问代码和资源,您只需使用以下几部分中所述的其中一个方法启用 SplitCompat。
为您的应用启用 SplitCompat 后,您还需要为您希望应用访问的功能模块中的每个 Activity 启用 SplitCompat。
在清单中声明 SplitCompatApplication
如需启用 SplitCompat,最简单的方法是在您的应用清单中将 SplitCompatApplication
声明为 Application
子类,如下所示:
<application
...
android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
</application>
应用安装在设备上后,您可以自动从已下载的功能模块访问代码和资源。
在运行时调用 SplitCompat
您还可以在运行时在特定 Activity 或服务中启用 SplitCompat。如需以这种方式启用 SplitCompat,您需要启动功能模块中所包含的 Activity。为此,请替换 attachBaseContext
,如下所示。
如果您有自定义 Application 类,应使其改为扩展 SplitCompatApplication
,以便为您的应用启用 SplitCompat,如下所示:
class MyApplication : SplitCompatApplication() {
...
}
public class MyApplication extends SplitCompatApplication {
...
}
SplitCompatApplication
仅会替换 ContextWrapper.attachBaseContext()
以包含 SplitCompat.install(Context applicationContext)
。如果您不想 Application
类扩展 SplitCompatApplication
,您可以手动替换 attachBaseContext()
方法,如下所示:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
// Emulates installation of future on demand modules using SplitCompat.
SplitCompat.install(this)
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// Emulates installation of future on demand modules using SplitCompat.
SplitCompat.install(this);
}
如果您的按需模块可同时与免安装应用和安装式应用兼容,您可以根据具体情况调用 SplitCompat,如下所示:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (!InstantApps.isInstantApp(this)) {
SplitCompat.install(this)
}
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (!InstantApps.isInstantApp(this)) {
SplitCompat.install(this);
}
}
为模块 Activity 启用 SplitCompat
为基本应用启用 SplitCompat 后,您需要为应用在功能模块中下载的每个 Activity 启用 SplitCompat。为此,请使用 SplitCompat.installActivity()
方法,如下所示:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
// Emulates installation of on demand modules using SplitCompat.
SplitCompat.installActivity(this)
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// Emulates installation of on demand modules using SplitCompat.
SplitCompat.installActivity(this);
}
访问功能模块中定义的组件
启动功能模块中定义的 activity
启用 SplitCompat 后,您可以使用 startActivity()
启动功能模块中定义的 activity。
startActivity(Intent()
.setClassName("com.package", "com.package.module.MyActivity")
.setFlags(...))
startActivity(new Intent()
.setClassName("com.package", "com.package.module.MyActivity")
.setFlags(...));
setClassName
的第一个参数是应用的软件包名称,第二个参数是 activity 的完整类名。
如果您按需下载的功能模块中存在一个 activity,您必须在该 activity 中启用 SplitCompat。
启动在功能模块中定义的服务
启用 SplitCompat 后,您可以使用 startService()
启动在功能模块中定义的服务。
startService(Intent()
.setClassName("com.package", "com.package.module.MyService")
.setFlags(...))
startService(new Intent()
.setClassName("com.package", "com.package.module.MyService")
.setFlags(...));
导出在功能模块中定义的组件
您不应在可选模块中包含导出的 Android 组件。
构建系统会将所有模块的清单条目合并到基本模块中;如果某个可选模块中包含导出的组件,则该组件在安装相应模块之前就已经可以访问,并且从其他应用调用该组件时,可能会因缺少代码导致崩溃。
内部组件没有这种问题;它们只能由应用进行访问,因此应用在访问此类组件之前可以检查是否已安装相应模块。
如果您需要使用导出的组件,并且希望其内容存在于可选模块中,请考虑实现代理模式。为此,您可以在基本模块中通过代理模式添加导出的组件;访问此代理组件时,它可以检查是否存在包含相应内容的模块。如果存在相应模块,则代理组件可以通过 Intent
启动模块中的内部组件,以中继调用方应用中的 intent。如果模块不存在,则组件可以下载模块或向调用方应用返回相应的错误消息。
访问已安装的模块中的代码和资源
如果您为基本应用上下文和功能模块中的 activity 启用 SplitCompat,则安装该可选模块后,您便可以使用功能模块中的代码和资源,就像它们是基本 APK 的一部分。
访问其他模块中的代码
访问基本模块中的代码
基本模块中的代码可以直接由其他模块使用。 您无需执行任何特殊操作;只需导入并使用您需要的类即可。
访问其他模块中的模块代码
模块中的对象或类无法通过另一个模块直接静态访问,但可以使用反射间接访问。
您应避免经常使用这种方法,因为反射会导致性能下降。对于复杂的用例,请使用依赖项注入框架(如 Dagger 2)来确保每个应用生命周期仅使用一次反射调用。
为了简化实例化后与对象的交互,建议在基本模块中定义接口,并在功能模块中定义该接口的实现。例如:
// In the base module
interface MyInterface {
fun hello(): String
}
// In the feature module
object MyInterfaceImpl : MyInterface {
override fun hello() = "Hello"
}
// In the base module, where we want to access the feature module code
val stringFromModule = (Class.forName("com.package.module.MyInterfaceImpl")
.kotlin.objectInstance as MyInterface).hello();
// In the base module
public interface MyInterface {
String hello();
}
// In the feature module
public class MyInterfaceImpl implements MyInterface {
@Override
public String hello() {
return "Hello";
}
}
// In the base module, where we want to access the feature module code
String stringFromModule =
((MyInterface) Class.forName("com.package.module.MyInterfaceImpl").getConstructor().newInstance()).hello();
访问其他模块中的资源
安装模块后,您就可以按标准方式访问模块中的资源和资源,但要注意以下两点:
- 如果您要访问其他模块中的资源,当前模块将无法访问相应资源的标识符,不过您仍然可以通过名称访问该资源。请注意,用于引用该资源的软件包是定义该资源的模块的软件包。
- 如果您想从应用的一个已安装模块访问新安装模块中存在的资源,您必须使用应用上下文执行此操作。 尝试访问资源的组件上下文此时还不会更新。或者,您也可以重新创建该组件(例如,调用 Activity.recreate())或在安装功能模块后对创建的 activity 重新安装 SplitCompat。
使用按需分发在应用中加载原生代码
在对功能模块使用按需分发时,我们建议使用 ReLinker 加载所有原生库。ReLinker 会修复在安装功能模块后加载原生库时出现的问题。如需详细了解 ReLinker,您可参阅 Android JNI 提示。
加载可选模块中的原生代码
安装某个分块后,我们建议通过 ReLinker 加载其原生代码。对于免安装应用,您应使用这种特殊方法。
如果您使用 System.loadLibrary()
加载原生代码,并且您的原生库依赖于此模块中的另一个库,您必须先手动加载被依赖库。如果您使用 ReLinker,则等效操作为 Relinker.recursively().loadLibrary()
。
如果您在原生代码中使用 dlopen()
加载可选模块中定义的库,则无法使用相对库路径。最佳解决方案是通过 ClassLoader.findLibrary()
从 Java 代码中检索库的绝对路径,然后在 dlopen()
调用中使用该路径。此操作需要在输入原生代码之前执行,或者,请在原生代码中使用基于 Java 的 JNI 调用。
访问已安装的 Android 免安装应用
当 Android 免安装应用模块报告为 INSTALLED
后,您可以使用刷新后的应用上下文访问其代码和资源。您的应用在安装模块之前创建的上下文(例如,已存储在变量中的上下文)不包含新模块的内容。不过,新的上下文包含此类内容,您可以使用 createPackageContext
获取新的上下文。
// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
override fun onStateUpdate(state: SplitInstallSessionState ) {
if (state.sessionId() == mySessionId) {
when (state.status()) {
...
SplitInstallSessionStatus.INSTALLED -> {
val newContext = context.createPackageContext(context.packageName, 0)
// If you use AssetManager to access your app’s raw asset files, you’ll need
// to generate a new AssetManager instance from the updated context.
val am = newContext.assets
}
}
}
}
// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
@Override
public void onStateUpdate(SplitInstallSessionState state) {
if (state.sessionId() == mySessionId) {
switch (state.status()) {
...
case SplitInstallSessionStatus.INSTALLED:
Context newContext = context.createPackageContext(context.getPackageName(), 0);
// If you use AssetManager to access your app’s raw asset files, you’ll need
// to generate a new AssetManager instance from the updated context.
AssetManager am = newContext.getAssets();
}
}
}
Android 8.0 及更高版本上的 Android 免安装应用
在 Android 8.0(API 级别 26)及更高版本的设备上请求 Android 免安装应用的按需模块时,在安装请求报告为 INSTALLED
后,您需要通过调用 SplitInstallHelper.updateAppInfo(Context context)
,使用新模块的上下文更新该应用。否则,应用不会知道该模块的代码和资源。更新应用的元数据后,您应通过调用新的 Handler
,在下一个主线程事件期间加载模块内容,如下所示:
override fun onStateUpdate(state: SplitInstallSessionState ) {
if (state.sessionId() == mySessionId) {
when (state.status()) {
...
SplitInstallSessionStatus.INSTALLED -> {
// You need to perform the following only for Android Instant Apps
// running on Android 8.0 (API level 26) and higher.
if (BuildCompat.isAtLeastO()) {
// Updates the app’s context with the code and resources of the
// installed module.
SplitInstallHelper.updateAppInfo(context)
Handler().post {
// Loads contents from the module using AssetManager
val am = context.assets
...
}
}
}
}
}
}
@Override
public void onStateUpdate(SplitInstallSessionState state) {
if (state.sessionId() == mySessionId) {
switch (state.status()) {
...
case SplitInstallSessionStatus.INSTALLED:
// You need to perform the following only for Android Instant Apps
// running on Android 8.0 (API level 26) and higher.
if (BuildCompat.isAtLeastO()) {
// Updates the app’s context with the code and resources of the
// installed module.
SplitInstallHelper.updateAppInfo(context);
new Handler().post(new Runnable() {
@Override public void run() {
// Loads contents from the module using AssetManager
AssetManager am = context.getAssets();
...
}
});
}
}
}
}
加载 C/C++ 库
如果您想从设备已下载到免安装应用中的模块加载 C/C++ 库,请使用 SplitInstallHelper.loadLibrary(Context context, String libName)
,如下所示:
override fun onStateUpdate(state: SplitInstallSessionState) {
if (state.sessionId() == mySessionId) {
when (state.status()) {
SplitInstallSessionStatus.INSTALLED -> {
// Updates the app’s context as soon as a module is installed.
val newContext = context.createPackageContext(context.packageName, 0)
// To load C/C++ libraries from an installed module, use the following API
// instead of System.load().
SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”)
...
}
}
}
}
public void onStateUpdate(SplitInstallSessionState state) {
if (state.sessionId() == mySessionId) {
switch (state.status()) {
case SplitInstallSessionStatus.INSTALLED:
// Updates the app’s context as soon as a module is installed.
Context newContext = context.createPackageContext(context.getPackageName(), 0);
// To load C/C++ libraries from an installed module, use the following API
// instead of System.load().
SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”);
...
}
}
}
已知限制
- 如果某个 activity 会访问可选模块中的资源,则无法在其中使用 Android WebView。这是因为在 Android API 级别 28 及更低版本上,WebView 和 SplitCompat 不兼容。
- 您不可在应用中缓存 Android
ApplicationInfo
对象及其内容,或包含这些内容的对象。您应始终根据需要从应用上下文中提取这些对象。安装功能模块时,缓存此类对象可能会导致应用崩溃。
管理已安装的模块
如需检查设备上当前已安装的功能模块,您可以调用 SplitInstallManager.getInstalledModules()
,它会返回已安装模块名称的 Set<String>
,如下所示。
val installedModules: Set<String> = splitInstallManager.installedModules
Set<String> installedModules = splitInstallManager.getInstalledModules();
卸载模块
您可以通过调用 SplitInstallManager.deferredUninstall(List<String> moduleNames)
请求设备卸载模块,如下所示。
// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))
// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(Arrays.asList("pictureMessages", "promotionalFilters"));
模块卸载不会立即发生。也就是说,设备会根据需要在后台进行卸载,以节省存储空间。您可以通过调用 SplitInstallManager.getInstalledModules()
并检查结果以确认设备是否已删除模块,如上一部分中所述。
下载其他语言资源
通过 app bundle,设备只会下载运行应用所需的代码和资源。因此,对于语言资源,用户的设备只会下载与设备设置中当前所选的一种或多种语言相符的应用语言资源。
如果您希望应用能够访问其他语言资源(例如,为了实现应用内语言选择器),您可以使用 Play Feature Delivery 库根据需要下载这些资源。该流程与下载功能模块的流程相似,如下所示。
// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply()
...
// Creates a request to download and install additional language resources.
val request = SplitInstallRequest.newBuilder()
// Uses the addLanguage() method to include French language resources in the request.
// Note that country codes are ignored. That is, if your app
// includes resources for “fr-FR” and “fr-CA”, resources for both
// country codes are downloaded when requesting resources for "fr".
.addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
.build()
// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request)
// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply();
...
// Creates a request to download and install additional language resources.
SplitInstallRequest request =
SplitInstallRequest.newBuilder()
// Uses the addLanguage() method to include French language resources in the request.
// Note that country codes are ignored. That is, if your app
// includes resources for “fr-FR” and “fr-CA”, resources for both
// country codes are downloaded when requesting resources for "fr".
.addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
.build();
// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request);
该请求的处理方式与功能模块请求的处理方式相同。也就是说,您可以像平常一样监控请求状态。
如果您的应用不需要立即使用其他语言资源,您可以延迟到应用在后台运行时再进行安装,如下所示。
splitInstallManager.deferredLanguageInstall(
Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
splitInstallManager.deferredLanguageInstall(
Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
访问已下载的语言资源
如需访问已下载的语言资源,您的应用需要在需要访问这些资源的每个 Activity 的 attachBaseContext()
方法内运行 SplitCompat.installActivity()
方法,如下所示。
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
SplitCompat.installActivity(this)
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
SplitCompat.installActivity(this);
}
对于您想要使用应用已下载的语言资源的每个 Activity,请更新基本上下文并通过其 Configuration
设置新的语言区域:
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)
}
@Override
protected void attachBaseContext(Context base) {
Configuration configuration = new Configuration();
configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
Context context = base.createConfigurationContext(configuration);
super.attachBaseContext(context);
SplitCompat.install(this);
}
为使这些更改生效,您必须在新语言安装完毕且可供使用后重新创建 Activity。您可以使用 Activity#recreate()
方法。
when (state.status()) {
SplitInstallSessionStatus.INSTALLED -> {
// Recreates the activity to load resources for the new language
// preference.
activity.recreate()
}
...
}
switch (state.status()) {
case SplitInstallSessionStatus.INSTALLED:
// Recreates the activity to load resources for the new language
// preference.
activity.recreate();
...
}
卸载其他语言资源
与功能模块类似,您可以随时卸载其他资源。在请求卸载之前,您可能需要先确定当前安装的语言,如下所示。
val installedLanguages: Set<String> = splitInstallManager.installedLanguages
Set<String> installedLanguages = splitInstallManager.getInstalledLanguages();
然后,您可以使用 deferredLanguageUninstall()
方法确定需要卸载的语言,如下所示。
splitInstallManager.deferredLanguageUninstall(
Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
splitInstallManager.deferredLanguageUninstall(
Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
在本地测试模块的安装情况
Play Feature Delivery 库可让您在本地测试应用能否执行以下操作,而无需连接到 Play 商店:
- 请求并监控模块的安装。
- 处理安装错误。
- 使用
SplitCompat
访问模块。
本页介绍了如何将应用的拆分 APK 部署到测试设备,以便 Play Feature Delivery 自动使用这些 APK 模拟从 Play 商店请求、下载和安装模块。
虽然您不需要对应用的逻辑进行任何更改,但需要满足以下要求:
- 下载并安装最新版
bundletool
。您需要使用bundletool
从 app bundle 构建一组新的可安装 APK。
构建一组 APK
您应构建应用的拆分 APK(如果您尚未执行此操作),具体操作步骤如下:
- 使用以下某种方法为您的应用构建 app bundle:
- 使用 Android Studio 和 Android Plugin for Gradle 构建 Android App Bundle 并为其签名。
- 从命令行构建 app bundle。
使用
bundletool
针对所有设备配置生成一组 APK,具体命令如下:bundletool build-apks --local-testing --bundle
my_app.aab --outputmy_app.apks
--local-testing
标志包含 APK 清单中的元数据,会告知 Play Feature Delivery 库使用本地拆分 APK 测试功能模块的安装,而无需连接到 Play 商店。
将应用部署到设备
使用 --local-testing
标记构建一组 APK 后,应使用 bundletool
安装应用的基础版本,并将其他 APK 转移到设备的本地存储空间。您可以使用以下命令执行这两项操作:
bundletool install-apks --apksmy_app.apks
现在,当您启动应用并完成下载和安装功能模块的用户流时,Play Feature Delivery 库会使用 bundletool
转移到设备本地存储空间的 APK。
模拟网络连接错误
为了模拟从 Play 商店安装模块的过程,Play Feature Delivery 库使用 SplitInstallManager
的替代方法(名为 FakeSplitInstallManager
)请求模块。当您将 bundletool
与 --local-testing
标志一起使用以构建一组 APK 并将其部署到测试设备时,它包含元数据,指示 Play Feature Delivery 库将应用的 API 调用自动切换为调用 FakeSplitInstallManager
,而不是 SplitInstallManager
。
FakeSplitInstallManager
包含一个布尔标志,当应用下次请求安装模块时,您可以启用该标志来模拟网络连接错误。如需在测试中访问 FakeSplitInstallManager
,您可以使用 FakeSplitInstallManagerFactory
获取它的实例,如下所示:
// Creates an instance of FakeSplitInstallManager with the app's context.
val fakeSplitInstallManager = FakeSplitInstallManagerFactory.create(context)
// Tells Play Feature Delivery Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true)
// Creates an instance of FakeSplitInstallManager with the app's context.
FakeSplitInstallManager fakeSplitInstallManager =
FakeSplitInstallManagerFactory.create(context);
// Tells Play Feature Delivery Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true);