功能模块允许您将某些功能和资源与应用的基本模块分开,并将它们包含在应用包中。通过 Play 功能交付,用户可以在安装应用的基本 APK 后,按需下载和安装这些组件。
例如,考虑一个包含捕获和发送图片消息功能的短信应用,但只有一小部分用户会发送图片消息。将图片消息作为可下载的功能模块可能更有意义。这样,所有用户的初始应用下载都会更小,只有发送图片消息的用户才需要下载该附加组件。
请记住,这种模块化需要更多工作,可能需要重构应用的现有代码,因此请仔细考虑哪些应用功能最适合按需提供给用户。为了更好地理解按需功能的最佳用例和指南,请阅读 按需交付的 UX 最佳实践。
如果您想逐步模块化应用功能,而无需启用高级交付选项(例如按需交付),请改为 配置安装时交付。
此页面帮助您将功能模块添加到您的应用项目并配置其按需交付。在开始之前,请确保您使用的是Android Studio 3.5或更高版本以及 Android Gradle Plugin 3.5.0 或更高版本。
配置用于按需交付的新模块
创建新功能模块最简单的方法是使用Android Studio 3.5或更高版本。由于功能模块固有地依赖于基础应用模块,因此您只能将它们添加到现有的应用项目中。
要使用 Android Studio 将功能模块添加到您的应用项目,请按以下步骤操作
- 如果您尚未这样做,请在 IDE 中打开您的应用项目。
- 从菜单栏中选择文件 > 新建 > 新建模块。
- 在创建新的模块对话框中,选择动态功能模块,然后点击下一步。
- 在配置您的新模块部分中,完成以下步骤
- 从下拉菜单中选择您的应用项目的基础应用模块。
- 指定模块名称。IDE 使用此名称将模块识别为 Gradle 子项目,位于您的Gradle 设置文件中。构建应用包时,Gradle 使用子项目名称的最后一个元素在功能模块的清单中注入
<manifest split>
属性。 - 指定模块的包名。默认情况下,Android Studio 会建议一个包名,该包名结合了基础模块的根包名和您在上一步中指定的模块名称。
- 选择模块要支持的最低 API 级别。此值应与基础模块的值匹配。
- 点击下一步。
在模块下载选项部分中,完成以下步骤
使用最多 50 个字符来指定模块标题。例如,在确认用户是否要下载模块时,平台使用此标题来向用户标识模块。因此,您的应用的基础模块必须包含模块标题作为字符串资源,您可以对其进行翻译。使用 Android Studio 创建模块时,IDE 会为您将字符串资源添加到基础模块,并在功能模块的清单中注入以下条目
<dist:module ... dist:title="@string/feature_title"> </dist:module>
在安装时包含下的下拉菜单中,选择安装时不包含模块。Android Studio 会在模块的清单中注入以下内容以反映您的选择
<dist:module ... > <dist:delivery> <dist:on-demand/> </dist:delivery> </dist:module>
如果您希望此模块可用于运行 Android 4.4(API 级别 20)及更低版本的设备并包含在多 APK 中,请选中融合旁边的复选框。这意味着您可以为此模块启用按需行为并禁用融合,以将其从不支持下载和安装拆分 APK 的设备中省略。Android Studio 会在模块的清单中注入以下内容以反映您的选择
<dist:module ...> <dist:fusing dist:include="true | false" /> </dist:module>
点击完成。
Android Studio 完成创建模块后,请从项目窗格中自行检查其内容(从菜单栏中选择查看 > 工具窗口 > 项目)。默认代码、资源和组织结构应与标准应用模块类似。
接下来,您需要使用 Play 功能交付库来实现按需安装功能。
在您的项目中包含 Play 功能交付库
在开始之前,您首先需要将 Play 功能交付库添加到您的项目中。
请求按需模块
当您的应用需要使用功能模块时,它可以通过SplitInstallManager
类在前台请求一个模块。发出请求时,您的应用需要指定模块的名称,该名称由目标模块清单中的split
元素定义。使用 Android Studio创建功能模块时,构建系统使用您提供的模块名称在编译时将此属性注入到模块的清单中。有关更多信息,请阅读有关功能模块清单的内容。
例如,考虑一个应用,它有一个按需模块用于使用设备的摄像头捕获和发送图片消息,并且此按需模块在其清单中指定了split="pictureMessages"
。以下示例使用SplitInstallManager
来请求pictureMessages
模块(以及用于某些促销过滤器的附加模块)
Kotlin
// 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 -> ... }
Java
// 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 功能交付库采用“即发即忘”策略。也就是说,它会将下载模块的请求发送到平台,但不会监控安装是否成功。要在安装后推进用户旅程或优雅地处理错误,请确保您监控请求状态。
注意:请求已安装在设备上的功能模块是可以的。如果检测到模块已安装,则 API 会立即将请求视为已完成。此外,模块安装后,Google Play 会自动对其进行更新。也就是说,当您上传新版本的应用包时,平台会更新属于您的应用的所有已安装 APK。有关更多信息,请阅读管理应用更新。
要访问模块的代码和资源,您的应用需要启用 SplitCompat。请注意,Android 即时应用不需要 SplitCompat。
延迟安装按需模块
如果您不需要应用立即下载和安装按需模块,您可以将其安装延迟到应用处于后台时。例如,如果您想预加载一些促销材料,以便稍后启动您的应用。
您可以使用deferredInstall()
方法指定稍后下载的模块,如下所示。并且,与SplitInstallManager.startInstall()
不同,您的应用不需要在前台才能启动延迟安装请求。
Kotlin
// 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"))
Java
// 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()
来请求它,如上一节所示。
监控请求状态
为了能够更新进度条、在安装后触发意图或优雅地处理请求错误,您需要侦听异步SplitInstallManager.startInstall()
任务的状态更新。在您可以开始接收安装请求的更新之前,请注册侦听器并获取请求的会话 ID,如下所示。
Kotlin
// 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)
Java
// 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()
来处理下载或安装模块失败的情况,如下所示
Kotlin
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. } } } } }
Java
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 功能交付库。也就是说,该设备无法按需下载和安装功能。 | 对于运行 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, SPLITCOMPAT_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()
处理状态更改,如下所示。
Kotlin
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. } } } }
Java
@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 | 请求已接受,下载即将开始。 | 初始化 UI 组件(例如进度条),以便向用户提供下载反馈。 |
REQUIRES_USER_CONFIRMATION | 下载需要用户确认。此状态最常见于应用并非通过 Google Play 安装的情况。 | 提示用户通过 Google Play 确认功能下载。要了解更多信息,请转到关于如何 获取用户确认 的部分。 |
DOWNLOADING | 下载正在进行中。 | 如果您为下载提供了进度条,请使用 SplitInstallSessionState.bytesDownloaded() 和 SplitInstallSessionState.totalBytesToDownload() 方法更新 UI(请参见上表中的代码示例)。 |
DOWNLOADED | 设备已下载模块,但尚未开始安装。 | 应用应 启用 SplitCompat 以访问已下载的模块并避免出现此状态。这是访问功能模块的代码和资源所必需的。 |
INSTALLING | 设备当前正在安装模块。 | 更新进度条。此状态通常很短。 |
INSTALLED | 模块已安装在设备上。 | 访问模块中的代码和资源 以继续用户体验。 如果模块用于在 Android 8.0(API 级别 26)或更高版本上运行的 Android 即时应用,则需要使用 |
FAILED | 模块在安装到设备之前请求失败。 | 提示用户重试请求或取消请求。 |
CANCELING | 设备正在取消请求。 | 要了解更多信息,请转到关于如何 取消安装请求 的部分。 |
CANCELED | 请求已被取消。 |
获取用户确认
在某些情况下,Google Play 可能会在满足下载请求之前需要用户确认。例如,如果您的应用并非通过 Google Play 安装,或者您正在尝试通过移动数据进行大规模下载。在这种情况下,请求的状态会报告 REQUIRES_USER_CONFIRMATION
,您的应用需要在设备能够下载和安装请求中的模块之前获得用户确认。要获得确认,您的应用应如下提示用户
Kotlin
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) } ... }
Java
@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
合约注册活动结果启动器。请参阅 活动结果 API。
请求的状态会根据用户的回应而更新
- 如果用户接受确认,请求状态将更改为
PENDING
,并继续下载。 - 如果用户拒绝确认,请求状态将更改为
CANCELED
。 - 如果用户在对话框销毁之前没有做出选择,请求状态将保持为
REQUIRES_USER_CONFIRMATION
。您的应用可以再次提示用户完成请求。
要接收包含用户回应的回调,您可以覆盖 ActivityResultCallback,如下所示。
Kotlin
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. } }
Java
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()
方法,如下所示。
Kotlin
splitInstallManager // Cancels the request for the given session ID. .cancelInstall(mySessionId)
Java
splitInstallManager // Cancels the request for the given session ID. .cancelInstall(mySessionId);
访问模块
要在下载模块后访问已下载模块中的代码和资源,您的应用需要为您的应用和应用下载的功能模块中的每个活动启用 SplitCompat 库。
但是,您应该注意,平台在下载模块后的一段时间内(在某些情况下为几天)访问模块内容时,会遇到以下限制:
- 平台无法应用模块引入的任何新的清单条目。
- 平台无法访问模块的系统 UI 组件资源(例如通知)。如果您需要立即使用此类资源,请考虑将这些资源包含在应用的基本模块中。
启用 SplitCompat
为了使您的应用能够访问已下载模块中的代码和资源,您需要仅使用以下部分中描述的方法之一启用 SplitCompat。
启用应用的 SplitCompat 后,还需要 为应用要访问的功能模块中的每个活动启用 SplitCompat。
在清单中声明 SplitCompatApplication
启用 SplitCompat 的最简单方法是在应用的清单中将 SplitCompatApplication
声明为 Application
子类,如下所示
<application
...
android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
</application>
应用安装在设备上后,您可以自动访问已下载功能模块中的代码和资源。
在运行时调用 SplitCompat
您也可以在运行时为特定的活动或服务启用 SplitCompat。要启动功能模块中包含的活动,需要通过这种方式启用 SplitCompat。为此,请覆盖 attachBaseContext
,如下所示。
如果您有自定义的 Application 类,请改为使其扩展 SplitCompatApplication
以启用应用的 SplitCompat,如下所示
Kotlin
class MyApplication : SplitCompatApplication() { ... }
Java
public class MyApplication extends SplitCompatApplication { ... }
SplitCompatApplication
只是覆盖 ContextWrapper.attachBaseContext()
以包含 SplitCompat.install(Context applicationContext)
。如果您不希望您的 Application
类扩展 SplitCompatApplication
,您可以手动覆盖 attachBaseContext()
方法,如下所示
Kotlin
override fun attachBaseContext(base: Context) { super.attachBaseContext(base) // Emulates installation of future on demand modules using SplitCompat. SplitCompat.install(this) }
Java
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Emulates installation of future on demand modules using SplitCompat. SplitCompat.install(this); }
如果您的按需模块与即时应用和已安装应用都兼容,您可以有条件地调用 SplitCompat,如下所示
Kotlin
override fun attachBaseContext(base: Context) { super.attachBaseContext(base) if (!InstantApps.isInstantApp(this)) { SplitCompat.install(this) } }
Java
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); if (!InstantApps.isInstantApp(this)) { SplitCompat.install(this); } }
为模块活动启用 SplitCompat
启用基本应用的 SplitCompat 后,需要为应用在功能模块中下载的每个活动启用 SplitCompat。为此,请使用 SplitCompat.installActivity()
方法,如下所示
Kotlin
override fun attachBaseContext(base: Context) { super.attachBaseContext(base) // Emulates installation of on demand modules using SplitCompat. SplitCompat.installActivity(this) }
Java
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Emulates installation of on demand modules using SplitCompat. SplitCompat.installActivity(this); }
访问功能模块中定义的组件
启动功能模块中定义的活动
启用 SplitCompat 后,可以使用 startActivity()
启动功能模块中定义的活动。
Kotlin
startActivity(Intent() .setClassName("com.package", "com.package.module.MyActivity") .setFlags(...))
Java
startActivity(new Intent() .setClassName("com.package", "com.package.module.MyActivity") .setFlags(...));
setClassName
的第一个参数是应用的包名,第二个参数是活动的完整类名。
如果您在按需下载的功能模块中有一个活动,则必须 在活动中启用 SplitCompat。
启动功能模块中定义的服务
启用 SplitCompat 后,可以使用 startService()
启动功能模块中定义的服务。
Kotlin
startService(Intent() .setClassName("com.package", "com.package.module.MyService") .setFlags(...))
Java
startService(new Intent() .setClassName("com.package", "com.package.module.MyService") .setFlags(...));
导出功能模块中定义的组件
不应在可选模块中包含导出的 Android 组件。
构建系统会将所有模块的清单条目合并到基本模块中;如果可选模块包含导出的组件,则即使在安装模块之前也能访问该组件,并且当从另一个应用调用时,由于缺少代码而可能导致崩溃。
对于内部组件,这不是问题;它们只能由应用访问,因此应用可以在访问组件之前 检查模块是否已安装。
如果您需要导出组件,并且希望其内容位于可选模块中,请考虑实现代理模式。您可以通过在基础模块中添加一个代理导出组件来实现此目的;访问时,代理组件可以检查包含内容的模块是否存在。如果模块存在,则代理组件可以通过Intent
从模块启动内部组件,并将意图从调用方应用中转发。如果模块不存在,则组件可以下载模块或向调用方应用返回相应的错误消息。
访问已安装模块中的代码和资源
如果您为基础应用程序上下文和功能模块中的活动启用 SplitCompat,则一旦安装了可选模块,就可以像使用基础 APK 的一部分一样使用功能模块中的代码和资源。
访问不同模块中的代码
从模块访问基础代码
基础模块中的代码可以直接被其他模块使用。您无需执行任何特殊操作;只需导入并使用所需的类即可。
从另一个模块访问模块代码
模块内的对象或类不能直接从另一个模块静态访问,但可以使用反射间接访问。
您应该注意这种情况发生的频率,因为反射会带来性能成本。对于复杂的用例,请使用依赖注入框架(如Dagger 2)来保证每个应用程序生命周期只有一个反射调用。
为了简化实例化后与对象交互的操作,建议在基础模块中定义接口,并在功能模块中定义其实现。例如
Kotlin
// 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();
Java
// 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())或对其重新安装 SplitCompat。
在使用按需交付的应用中加载原生代码
我们建议使用ReLinker在使用功能模块的按需交付时加载所有原生库。ReLinker 修复了在安装功能模块后加载原生库的问题。您可以在Android JNI 提示中了解更多关于 ReLinker 的信息。
从可选模块加载原生代码
安装拆分后,我们建议通过ReLinker加载其原生代码。对于即时应用,您应该使用此特殊方法。
如果您使用System.loadLibrary()
加载原生代码,并且您的原生库依赖于模块中的另一个库,则必须先手动加载另一个库。如果您使用 ReLinker,则等效操作是Relinker.recursively().loadLibrary()
。
如果您在原生代码中使用dlopen()
加载可选模块中定义的库,它将无法使用相对库路径。最佳解决方案是通过ClassLoader.findLibrary()
从 Java 代码中检索库的绝对路径,然后在dlopen()
调用中使用它。在进入原生代码之前执行此操作,或使用从原生代码到 Java 的 JNI 调用。
访问已安装的 Android 即时应用
Android 即时应用模块报告为INSTALLED
后,您可以使用更新的应用Context访问其代码和资源。应用在安装模块之前创建的上下文(例如,已经存储在变量中的上下文)不包含新模块的内容。但是新的上下文包含——例如,可以使用createPackageContext
获取。
Kotlin
// 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 } } } }
Java
// 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
在下一个主线程事件期间加载模块的内容,如下所示
Kotlin
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 ... } } } } } }
Java
@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)
,如下所示
Kotlin
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”) ... } } } }
Java
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”); ... } } }
已知限制
- 无法在访问可选模块中的资源或资产的活动中使用 Android WebView。这是由于 WebView 和 SplitCompat 在 Android API 级别 28 及更低版本上的不兼容性。
- 您不能缓存 Android
ApplicationInfo
对象、其内容或应用中包含它们的任何对象。您应该始终根据需要从应用上下文中获取这些对象。缓存此类对象可能会导致在安装功能模块时应用崩溃。
管理已安装模块
要检查设备上当前安装了哪些功能模块,您可以调用SplitInstallManager.getInstalledModules()
,它返回已安装模块名称的Set<String>
,如下所示。
Kotlin
val installedModules: Set<String> = splitInstallManager.installedModules
Java
Set<String> installedModules = splitInstallManager.getInstalledModules();
卸载模块
您可以通过调用SplitInstallManager.deferredUninstall(List<String> moduleNames)
请求设备卸载模块,如下所示。
Kotlin
// Specifies two feature modules for deferred uninstall. splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))
Java
// Specifies two feature modules for deferred uninstall. splitInstallManager.deferredUninstall(Arrays.asList("pictureMessages", "promotionalFilters"));
模块卸载不会立即发生。也就是说,设备会根据需要在后台卸载它们以节省存储空间。您可以通过调用SplitInstallManager.getInstalledModules()
并检查结果来确认设备已删除模块,如上一节所述。
下载其他语言资源
使用应用包时,设备仅下载运行您的应用所需的代码和资源。因此,对于语言资源,用户的设备仅下载与设备设置中当前选择的某种或多种语言匹配的应用语言资源。
如果您希望您的应用能够访问其他语言资源(例如,实现应用内语言选择器),您可以使用 Play 功能交付库按需下载它们。此过程类似于下载功能模块,如下所示。
Kotlin
// 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)
Java
// 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);
此请求的处理方式与功能模块请求相同。也就是说,您可以像平时一样监控请求状态。
如果您的应用不需要立即使用其他语言资源,您可以将其安装延迟到应用处于后台时,如下所示。
Kotlin
splitInstallManager.deferredLanguageInstall( Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
Java
splitInstallManager.deferredLanguageInstall( Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
访问已下载的语言资源
要访问已下载的语言资源,您的应用需要在需要访问这些资源的每个活动的attachBaseContext()
方法中运行SplitCompat.installActivity()
方法,如下所示。
Kotlin
override fun attachBaseContext(base: Context) { super.attachBaseContext(base) SplitCompat.installActivity(this) }
Java
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); SplitCompat.installActivity(this); }
对于要使用应用已下载的语言资源的每个活动,更新基本上下文并通过其Configuration
设置新的语言环境
Kotlin
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) }
Java
@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#recreate()
方法。
Kotlin
when (state.status()) { SplitInstallSessionStatus.INSTALLED -> { // Recreates the activity to load resources for the new language // preference. activity.recreate() } ... }
Java
switch (state.status()) { case SplitInstallSessionStatus.INSTALLED: // Recreates the activity to load resources for the new language // preference. activity.recreate(); ... }
卸载其他语言资源
与功能模块类似,您可以随时卸载其他资源。在请求卸载之前,您可能首先需要确定当前安装了哪些语言,如下所示。
Kotlin
val installedLanguages: Set<String> = splitInstallManager.installedLanguages
Java
Set<String> installedLanguages = splitInstallManager.getInstalledLanguages();
然后,您可以使用deferredLanguageUninstall()
方法决定要卸载哪些语言,如下所示。
Kotlin
splitInstallManager.deferredLanguageUninstall( Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
Java
splitInstallManager.deferredLanguageUninstall( Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
本地测试模块安装
Play 功能交付库允许您在不连接到 Play 商店的情况下本地测试您的应用执行以下操作的能力
- 请求和监控模块安装。
- 处理安装错误。
- 使用
SplitCompat
访问模块。
此页面介绍如何将应用的拆分 APK 部署到您的测试设备,以便 Play 功能交付自动使用这些 APK 来模拟从 Play 商店请求、下载和安装模块。
虽然您无需对应用的逻辑进行任何更改,但您需要满足以下要求
- 下载并安装最新版本的
bundletool
。您需要bundletool
才能从应用的包中构建一组新的可安装 APK。
构建一组 APK
如果您尚未这样做,请按照以下步骤构建应用的拆分 APK
- 使用以下方法之一为您的应用构建应用包
- 使用 Android Studio 和 Android Gradle 插件构建和签名 Android 应用包。
- 从命令行构建应用包.
使用
bundletool
生成适用于所有设备配置的一组 APK,使用以下命令bundletool build-apks --local-testing --bundle my_app.aab --output my_app.apks
--local-testing
标志在您的 APK 清单中包含元数据,使 Play 功能交付库知道使用本地拆分 APK 来测试安装功能模块,而无需连接到 Play 商店。
将您的应用部署到设备
使用--local-testing
标志构建一组 APK后,使用bundletool
安装应用的基本版本并将其他 APK 传输到设备的本地存储。您可以使用以下命令执行这两个操作
bundletool install-apks --apks my_app.apks
现在,当您启动应用并完成下载和安装功能模块的用户流程时,Play 功能交付库将使用bundletool
传输到设备本地存储的 APK。
模拟网络错误
为了模拟从 Play 商店安装模块,Play 功能交付库使用SplitInstallManager
的替代方法,称为FakeSplitInstallManager
来请求模块。当您使用--local-testing
标志使用bundletool
构建一组 APK并将其部署到您的测试设备时,它包含指示 Play 功能交付库自动切换应用的 API 调用以调用FakeSplitInstallManager
而不是SplitInstallManager
的元数据。
FakeSplitInstallManager
包含一个布尔型标志,您可以启用它来模拟您的应用下次请求安装模块时发生的网络错误。如需在测试中访问FakeSplitInstallManager
,您可以使用FakeSplitInstallManagerFactory
获取它的实例,如下所示。
Kotlin
// 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)
Java
// 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);