配置按需交付

功能模块允许您将某些功能和资源与应用的基础模块分开,并将它们包含在应用包中。通过 Play 功能交付,用户可以例如在安装了应用的基础 APK 后,按需下载和安装这些组件。

例如,考虑一个文本消息应用,它包含用于捕获和发送图片消息的功能,但只有很小一部分用户发送图片消息。将图片消息作为可下载功能模块包含在内可能是有意义的。这样,所有用户的初始应用下载都较小,只有发送图片消息的用户才需要下载该附加组件。

请记住,这种类型的模块化需要更多工作,可能还需要重构应用的现有代码,因此请仔细考虑应用中的哪些功能最适合按需提供给用户。为了更好地理解按需功能的最佳用例和指南,请阅读 按需交付的 UX 最佳实践

如果您想逐渐模块化应用功能,但又不想启用高级交付选项(例如按需交付),请改用 配置安装时交付

此页面将帮助您将功能模块添加到应用项目中,并为其配置按需交付。在开始之前,请确保您使用的是 Android Studio 3.5 或更高版本以及 Android Gradle 插件 3.5.0 或更高版本。

配置一个用于按需交付的新模块

使用 Android Studio 3.5 或更高版本,创建新功能模块的最简单方法是使用它。由于功能模块本质上依赖于基础应用模块,因此只能将它们添加到现有的应用项目中。

要使用 Android Studio 将功能模块添加到应用项目,请按以下步骤操作

  1. 如果您尚未这样做,请在 IDE 中打开您的应用项目。
  2. 从菜单栏中选择**文件 > 新建 > 新模块**。
  3. 在**创建新模块**对话框中,选择**动态功能模块**,然后点击**下一步**。
  4. 配置新模块部分,完成以下操作
    1. 从下拉菜单中选择应用项目的基本应用模块
    2. 指定模块名称。IDE 使用此名称将模块标识为 Gradle 设置文件中的 Gradle 子项目。当构建应用捆绑包时,Gradle 使用子项目名称的最后一个元素来注入<manifest split>属性,该属性位于功能模块清单中。
    3. 指定模块的包名。默认情况下,Android Studio 建议一个包名,该包名将基本模块的根包名与您在上一步中指定的模块名称组合在一起。
    4. 选择模块要支持的最低 API 级别。此值应与基本模块的值匹配。
  5. 单击下一步
  6. 模块下载选项部分,完成以下操作

    1. 使用最多 50 个字符指定模块标题。平台使用此标题在用户确认是否要下载模块(例如)时向用户标识模块。因此,应用的基本模块必须包含模块标题作为字符串资源,您可以对其进行翻译。使用 Android Studio 创建模块时,IDE 会将字符串资源添加到基本模块中,并在功能模块的清单中注入以下条目

      <dist:module
          ...
          dist:title="@string/feature_title">
      </dist:module>
      
    2. 安装时包含下的下拉菜单中,选择安装时不包含模块。Android Studio 会在模块的清单中注入以下内容以反映您的选择

      <dist:module ... >
        <dist:delivery>
            <dist:on-demand/>
        </dist:delivery>
      </dist:module>
      
    3. 如果希望该模块可用于运行 Android 4.4(API 级别 20)及更低版本的设备并包含在多 APK 中,请选中融合旁边的框。这意味着可以为该模块启用按需行为并禁用融合以将其从不支持下载和安装拆分 APK 的设备中省略。Android Studio 会在模块的清单中注入以下内容以反映您的选择

      <dist:module ...>
          <dist:fusing dist:include="true | false" />
      </dist:module>
      
  7. 单击完成

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。请注意,SplitCompat 对于 Android Instant Apps 来说不是必需的。

延迟安装按需模块

如果不需要应用立即下载和安装按需模块,可以延迟安装,以便在应用处于后台时进行安装。例如,如果要预加载一些促销素材,以便在以后启动应用时使用。

可以使用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 商店等问题导致的。有关如何从用户的角度优雅地处理这些情况的建议,请查看我们的按需交付的 UX 指南

从代码角度来看,应使用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 负责处理请求的服务已停止。 重试请求。

您的SplitInstallStateUpdatedListener将收到一个具有此错误代码、状态FAILED和会话 ID-1SplitInstallSessionState

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 Instant App,则需要使用 splitInstallHelper 使用新模块更新应用组件

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() 加载可选模块中定义的库,它将无法使用相对库路径。最佳解决方案是从 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 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 商店

本页介绍如何将应用的分包 APK 部署到您的测试设备,以便 Play 功能交付库自动使用这些 APK 来模拟从 Play 商店请求、下载和安装模块。

虽然您无需对应用的逻辑进行任何更改,但您需要满足以下要求

构建一组 APK

如果您尚未这样做,请按照以下步骤构建应用的分包 APK

  1. 使用以下方法之一为您的应用构建应用捆绑包
  2. 使用 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 标志 构建一组 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);