功能模块允许您将某些功能和资源与应用的基本模块分离,并将它们包含在应用包中。通过 Play 功能分发,用户例如可以在已安装应用的基本 APK 后,按需下载和安装这些组件。
例如,考虑一个包含捕捉和发送图片消息功能的短信应用,但只有一小部分用户发送图片消息。将图片消息功能作为可下载的功能模块可能是有意义的。这样,所有用户的初始应用下载都会更小,并且只有发送图片消息的用户才需要下载该附加组件。
请记住,这种模块化需要更多的努力,并且可能需要重构您应用现有的代码,因此请仔细考虑您应用的哪些功能最能从按需提供中受益。要更好地理解按需功能的最佳用例和指南,请阅读按需分发的用户体验最佳实践。
如果您想随着时间的推移逐步模块化应用功能,而无需启用高级分发选项(例如按需分发),则可以配置安装时分发。
本页面帮助您向应用项目添加功能模块并将其配置为按需分发。在开始之前,请确保您使用的是 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 部分,完成以下操作:
- 从下拉菜单中选择应用项目的基础应用模块。
- 指定模块名称。IDE 使用此名称将模块标识为 Gradle 设置文件中的 Gradle 子项目。构建应用包时,Gradle 使用子项目名称的最后一个元素将
<manifest split>
属性注入功能模块的清单。 - 指定模块的软件包名称。默认情况下,Android Studio 会建议一个将基本模块的根软件包名称与您在上一步中指定的模块名称结合起来的软件包名称。
- 选择模块要支持的最低 API 级别。此值应与基本模块的值匹配。
- 点击 Next。
在 Module Download Options 部分,完成以下操作:
指定模块标题,最多 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>
点击 Finish。
Android Studio 完成创建模块后,请从项目窗格(从菜单栏中选择 View > Tool Windows > Project)自行检查其内容。默认代码、资源和组织应与标准应用模块的相似。
接下来,您需要使用 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
契约注册一个 activity 结果启动器。请参阅 Activity Result 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()
加载可选模块中定义的库,它将无法与相对库路径一起使用。最佳解决方案是从 Java 代码通过 ClassLoader.findLibrary()
检索库的绝对路径,然后在您的 dlopen()
调用中使用它。在进入原生代码之前或从您的原生代码通过 JNI 调用 Java 进行此操作。
访问已安装的 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 API 级别 28 及更低版本上,在访问可选模块中资源或资产的活动中,无法使用 Android WebView。这是由于 WebView 和 SplitCompat 之间存在不兼容性。
- 您不能在应用中缓存 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 App Bundle。
- 从命令行构建您的应用包:.
使用
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);