Play for On-device AI(Beta 版)

简介

Play for On-device AI 融合了 Android App Bundle 和 Google Play 分发的优势,可用于自定义 ML 模型分发,让您在不增加设备生态系统复杂性的前提下,免费提升模型性能。借助它,您可以将包含代码、资源和机器学习模型的单个制品发布到 Play,并从多种分发模式和定位选项中进行选择。

优势

  • 只需将单个发布制品上传到 Google Play,即可将托管、分发、更新和定位工作委托给 Play,无需支付额外费用。
  • 在安装时、快速跟进或按需交付机器学习模型。
    • 安装时交付可确保在应用打开时存在一个非常大的模型。您的模型将以 APK 形式安装。
    • 在应用安装完毕后,快速跟进式分发会在后台自动进行。用户可以在模型完全下载完毕之前打开您的应用。模型将下载到应用的内部存储空间。
    • 借助按需交付功能,您可以在运行时请求模型,如果模型仅在某些用户流程中需要,此功能会非常有用。模型将下载到应用的内部存储空间。
  • 交付针对特定设备(基于设备型号、系统属性或 RAM)的机器学习模型变体。
  • 借助 Play 的自动修补功能,应用更新可以保持较小的大小并经过优化,这意味着只需下载文件中的差异部分。

注意事项

  • 使用 Play On-device AI 即表示您同意 Google Play 开发者分发协议Play Core 软件开发套件服务条款中的条款。
  • Play 为设备端 AI 下载的模型应仅供您的应用使用。不应向其他应用提供模型。
  • 单个 AI 包的大小上限为 1.5GB,具体取决于其压缩下载大小。从应用包生成的任何应用版本的累计应用大小上限为 4GB。
  • 大小超过 1 GB 的应用必须将最低 SDK 级别设置为 21 或更高。

如何使用 Play for On-device AI

Play for On-device AI 使用 AI 包。您可以在应用软件包中将准备好分发的自定义模型打包到 AI 包中。您可以选择 AI 包应以安装时分发、快速跟进式分发还是按需分发的方式进行分发。

通过将 AI 包与应用 bundle 打包在一起,您可以使用 Play 的所有现有测试和发布工具(例如测试轨道和分阶段发布),通过自定义模型来管理应用的发布。

AI 包会与应用二进制文件一起更新。如果您的新应用版本未对 AI 包进行更改,Play 的自动修补流程将确保用户无需重新下载该 AI 包。Play 在更新应用时只会下载更改的内容。

AI 包仅包含模型。不允许使用 Java/Kotlin 和原生库。 如果您需要提供库或代码来运行机器学习模型,请将其移至基本模块或功能模块。您可以配置功能模块,使其具有与 AI 包相同的下载和定位设置。

将 LiteRT 和 MediaPipe 与 AI 包搭配使用

您可以将 LiteRT 和 MediaPipe 与 AI 包搭配使用。将模型打包到 AI 包中,然后按照安装时分发的包快速跟进式分发的包和按需分发的包的说明访问该模型。

更多阅读材料:

AI 包使用入门

从总体上讲,您可以按以下方式开始使用 Play for On-device AI:

  1. 将模型打包到 Android App Bundle 中的 AI 包中,并指定应如何交付 AI 包。
  2. [可选] 如果您想向不同设备分发不同模型,可以为 AI 包配置设备定位。例如,您可以向特定设备型号交付 AI 包 A,向 RAM 至少为 6GB 的设备交付 AI 包 B,而所有其他设备则不接收任何模型。
  3. [可选] 如果您使用的是按需分发或快速跟进式分发,请将 Play AI Delivery 库集成到您的应用中,以便根据需要下载 AI 包。
  4. 测试您的 app bundle 并将其发布到 Google Play。

检查 Android Gradle 插件版本

如需使用 AI 包,请确保您的 Android Gradle 插件 (AGP) 版本至少为 8.8。此版本随 Android Studio Ladybug 2 一起打包。

将模型提取到 AI 包中

以下步骤不需要使用 Android Studio。

  1. 在项目的顶级目录中,为 AI 包创建一个目录。此目录名称将用作 AI 包名称。AI 包名称必须以字母开头,且只能包含字母、数字和下划线。
  2. 在 AI 包目录中,创建一个 build.gradle 文件并添加以下代码。请务必指定 AI 包的名称,并且仅指定一种分发类型:

    // In the AI pack's build.gradle file:
    plugins {
      id 'com.android.ai-pack'
    }
    
    aiPack {
        packName = "ai-pack-name" // Directory name for the AI pack
        dynamicDelivery {
            deliveryType = "[ install-time | fast-follow | on-demand ]"
        }
    }
    
  3. 在项目的应用 build.gradle 文件中,添加项目中每个 AI 包的名称,如下所示:

    // In the app build.gradle file:
    android {
        ...
        assetPacks = [":ai-pack-name", ":ai-pack2-name"]
    }
    
  4. 在项目的 settings.gradle 文件中,添加项目中的所有 AI 包,如下所示:

    // In the settings.gradle file:
    include ':app'
    include ':ai-pack-name'
    include ':ai-pack2-name'
    
  5. 在 AI 包中,创建一个 src/main/assets/ 目录。

  6. 将模型放置在 src/main/assets 目录中。您也可以在此处创建子目录。应用的目录结构现在应如下所示:

    • build.gradle
    • settings.gradle
    • app/
    • ai-pack-name/build.gradle
    • ai-pack-name/src/main/assets/your-model-directories
  7. 添加用于加载和运行模型的代码。具体的操作方法取决于 AI 包的交付模式。请参阅下文中的安装时快速跟进/按需说明。

  8. [可选] 配置设备定位,以便向不同设备分发不同的模型。

  9. 使用 Gradle 构建 Android App Bundle。在生成的 app bundle 中,根级目录现在包含以下内容:

    • ai-pack-name/manifest/AndroidManifest.xml:此目录用于配置 AI 包的标识符和分发模式
    • ai-pack-name/assets/your-model-directories:此目录包含作为 AI 包的一部分分发的所有资源

    Gradle 会为每个 AI 包生成清单,并为您输出 assets/ 目录。

配置安装时分发

配置为安装时分发的 AI 包可以在应用启动后立即使用。使用 Java AssetManager API 获取在此模式下提供的 AI 包:

import android.content.res.AssetManager;
...
Context context = createPackageContext("com.example.app", 0);
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("model-name");

配置快速跟进式分发和按需分发

如需使用快速跟进式分发或按需分发模式下载 AI 包,请使用 Play AI Delivery 库。

声明对 Play AI Delivery 库的依赖项

在应用的 build.gradle 文件中,声明对 Play AI Delivery 库的依赖项:

dependencies {
  ...
  implementation "com.google.android.play:ai-delivery:0.1.1-alpha01"
}

查看状态

每个 AI 包都存储于应用的内部存储空间内单独的文件夹中。使用 getPackLocation() 方法确定 AI 包的根文件夹。此方法会返回以下值:

返回值 状态
有效的 AiPackLocation 对象 AI 包根文件夹位于 assetsPath(),现在可直接访问
null 未知 AI 包或 AI 包无法使用

获取有关 AI 包的下载信息

使用
getPackStates() 方法确定下载内容的大小,以及资源包是否已在下载。

Task<AiPackStates> getPackStates(List<String> packNames)

getPackStates() 是一个返回 Task<AiPackStates> 的异步方法。AiPackStates 对象的 packStates() 方法会返回一个 Map<String, AiPackState>。此映射包含所请求的每个 AI 包的状态,按其名称进行键控:

Map<String, AiPackState> AiPackStates#packStates()

最终请求如下所示:

final String aiPackName = "myAiPackName";

aiPackManager
    .getPackStates(Collections.singletonList(aiPackName))
    .addOnCompleteListener(new OnCompleteListener<AiPackStates>() {
        @Override
        public void onComplete(Task<AiPackStates> task) {
            AiPackStates aiPackStates;
            try {
                aiPackStates = task.getResult();
                AiPackState aiPackState =
                    aiPackStates.packStates().get(aiPackName);
            } catch (RuntimeExecutionException e) {
                Log.d("MainActivity", e.getMessage());
                return;
            });

以下 AiPackState 方法提供了 AI 包的大小、截至目前已下载的数据量(如已请求),以及已传输到应用的数据量:

如需获取 AI 包的状态,请使用 status() 方法,该方法以整数形式返回与 AiPackStatus 类中某个常量字段相对应的状态。尚未安装的 AI 包的状态为 AiPackStatus.NOT_INSTALLED

如果请求失败,请使用 errorCode() 方法,该方法的返回值与 AiPackErrorCode 类中的某个常量字段相对应。

安装

使用 fetch() 方法首次下载 AI 包,或要求进行 AI 包更新以完成操作:

Task<AiPackStates> fetch(List<String> packNames)

此方法会返回一个 AiPackStates 对象,其中包含资源包列表及其初始下载状态和大小。如果通过 fetch() 请求的 AI 包已经在下载,就会返回下载状态,并且不会启动其他下载。

监控下载状态

您应实现 AiPackStateUpdateListener 以跟踪 AI 包的安装进度。状态更新按资源包细分,以支持跟踪各 AI 资源包的状态。在请求的其他所有下载完成之前,您就可以开始使用可用的 AI 包。

void registerListener(AiPackStateUpdateListener listener)
void unregisterListener(AiPackStateUpdateListener listener)
下载内容较大

如果下载内容超过 200 MB 并且用户未连接到 Wi-Fi,那么在用户明确同意使用移动网络连接继续下载前,下载不会开始。同样,如果下载内容较大并且用户与 WLAN 的连接断开,下载会暂停,需要用户明确同意才能使用移动网络连接继续下载。已暂停的 Asset Pack 状态为 WAITING_FOR_WIFI。如需触发界面流程以提示用户同意,请使用 showConfirmationDialog() 方法。

请注意,如果应用不调用此方法,下载会暂停,并且只有当用户重新连接到 Wi-Fi 时才会自动恢复下载。

需要用户确认

如果包的状态为 REQUIRES_USER_CONFIRMATION,则在用户接受显示 showConfirmationDialog() 的对话框之前,下载不会继续进行。如果 Play 无法识别应用(例如,应用是旁加载的),则可能会出现此状态。请注意,在这种情况下调用 showConfirmationDialog() 会导致应用更新。更新后,您需要再次请求 AI 包。

以下是监听器的一个实现示例:

AiPackStateUpdateListener aiPackStateUpdateListener = new AiPackStateUpdateListener() {
    private final ActivityResultLauncher<IntentSenderRequest> activityResultLauncher =
      registerForActivityResult(
          new ActivityResultContracts.StartIntentSenderForResult(),
          new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
              if (result.getResultCode() == RESULT_OK) {
                Log.d(TAG, "Confirmation dialog has been accepted.");
              } else if (result.getResultCode() == RESULT_CANCELED) {
                Log.d(TAG, "Confirmation dialog has been denied by the user.");
              }
            }
          });

    @Override
    public void onStateUpdate(AiPackState aiPackState) {
      switch (aiPackState.status()) {
        case AiPackStatus.PENDING:
          Log.i(TAG, "Pending");
          break;

        case AiPackStatus.DOWNLOADING:
          long downloaded = aiPackState.bytesDownloaded();
          long totalSize = aiPackState.totalBytesToDownload();
          double percent = 100.0 * downloaded / totalSize;

          Log.i(TAG, "PercentDone=" + String.format("%.2f", percent));
          break;

        case AiPackStatus.TRANSFERRING:
          // 100% downloaded and assets are being transferred.
          // Notify user to wait until transfer is complete.
          break;

        case AiPackStatus.COMPLETED:
          // AI pack is ready to use. Run the model.
          break;

        case AiPackStatus.FAILED:
          // Request failed. Notify user.
          Log.e(TAG, aiPackState.errorCode());
          break;

        case AiPackStatus.CANCELED:
          // Request canceled. Notify user.
          break;

        case AiPackStatus.WAITING_FOR_WIFI:
        case AiPackStatus.REQUIRES_USER_CONFIRMATION:
          if (!confirmationDialogShown) {
            aiPackManager.showConfirmationDialog(activityResultLauncher);
            confirmationDialogShown = true;
          }
          break;

        case AiPackStatus.NOT_INSTALLED:
          // AI pack is not downloaded yet.
          break;
        case AiPackStatus.UNKNOWN:
          Log.wtf(TAG, "AI pack status unknown")
          break;
      }
    }
}

或者,您也可以使用 getPackStates() 方法获取当前下载的状态。AiPackStates 包含下载进度、下载状态和任何失败的错误代码。

使用 AI 包

在下载请求达到 COMPLETED 状态后,您可以使用文件系统调用来获取 AI 包。使用 getPackLocation() 方法获取 AI 包的根文件夹。

AI 包存储在 AI 包根目录内的 assets 目录中。您可以使用便捷方法 assetsPath() 获取 assets 目录的路径。使用以下方法获取特定资源的路径:

private String getAbsoluteAiAssetPath(String aiPack, String relativeAiAssetPath) {
    AiPackLocation aiPackPath = aiPackManager.getPackLocation(aiPack);

    if (aiPackPath == null) {
        // AI pack is not ready
        return null;
    }

    String aiAssetsFolderPath = aiPackPath.assetsPath();
    // equivalent to: FilenameUtils.concat(aiPackPath.path(), "assets");
    String aiAssetPath = FilenameUtils.concat(aiAssetsFolderPath, relativeAiAssetPath);
    return aiAssetPath;
}

配置设备定位

您可以按照设备定位说明指定应接收 AI 包的设备或设备群组。

其他 Play AI Delivery API 方法

以下是您可能希望在应用中使用的一些其他 API 方法。

取消请求

您可以使用 cancel() 取消有效的 AI 包请求。请注意,系统只能尽量满足您的此项请求。

移除 AI 包

您还可以使用 removePack() 安排移除 AI 包。

获取多个 AI 包的位置

使用 getPackLocations() 批量查询多个 AI 包的状态,此方法将返回 AI 包与其位置的映射。getPackLocations() 返回的映射包含当前已下载且为最新状态的每个 Asset Pack 的条目。

设备定位

借助设备定位,您可以更精细地控制应用包的哪些部分会交付给特定设备。例如,您可以确保大型模型仅分发给具有高 RAM 的设备,也可以将不同版本的模型分发给不同的设备。

您可以定位设备属性,例如:

所需步骤概览

如需启用设备定位,必须执行以下步骤:

  1. 在 XML 文件中定义设备组。
  2. 指定套装中的哪些部分应发送到哪些设备组。
  3. [可选] 在本地测试配置。
  4. 将您的 bundle(包含 XML 文件)上传到 Google Play。

检查 Android Gradle 插件版本

如需使用设备定位功能,请确保您的 Android Gradle 插件 (AGP) 版本至少为 8.10.0。该工具随 Android Studio(Meerkat 2 及更高版本)一起打包提供。 下载最新的稳定版 Android Studio

在 Android Gradle 插件中启用此功能

您必须在 gradle.properties 文件中明确启用设备定位:

android.experimental.enableDeviceTargetingConfigApi=true

创建设备定位配置 XML 文件

设备定位配置文件是一个 XML 文件,您可以在其中定义自定义设备组。例如,您可以定义一个名为 qti_v79 的设备组,其中包含所有搭载 Qualcomm SM8750 片上系统的设备:

<config:device-targeting-config
    xmlns:config="http://schemas.android.com/apk/config">

    <config:device-group name="qti_v79">
        <config:device-selector>
            <config:system-on-chip manufacturer="QTI" model="SM8750"/>
        </config:device-selector>
    </config:device-group>

</config:device-targeting-config>

一个设备组最多可包含 5 个设备选择器。如果设备满足某个设备组的任一设备选择器,则该设备会纳入相应设备组。

一个设备选择器可以包含一个或多个设备属性。如果设备符合选择器的所有设备属性,则会被选中。

如果设备与多个组匹配,系统将向其提供 XML 文件中首先定义的组的内容。您在 XML 文件中定义群组的顺序就是优先级顺序。

如果设备与任何群组都不匹配,则会归入默认的“其他”群组。此群组由系统自动生成,不应明确定义。

可用的设备属性

  • device_ram:设备 RAM 要求
    • min_bytes(含):要求的最低 RAM(单位:字节)
    • max_bytes(不含):最高 RAM 要求(字节)
  • included_device_ids:此选择器中要包含的设备型号(每组最多 10000 个 device_ids)。如果设备与列表中的任何 device_id 匹配,则满足此属性。
    • build_brand:设备制造商
    • build_device:设备型号代码
  • excluded_device_ids:此选择器中要排除的设备型号(每组最多 10,000 个 device_ids)。如果设备与列表中的任何 device_id 都不匹配,则满足此属性。
    • build_brand:设备制造商
    • build_device:设备型号代码
  • required_system_features:设备想要包含在此选择器中所需要具备的功能。(每组最多 100 项功能)。设备需要具备此列表中的所有系统功能才能满足此属性。

    系统功能参考

    • name:系统功能
  • forbidden_system_features:设备想要包含在此选择器中就不能具备的功能(每组最多 100 项功能)。如果设备具备此列表中的任何系统功能,则不满足此属性。

    系统功能参考

    • name:系统功能
  • system-on-chip:要包含在此选择器中的系统芯片。设备需要具备此列表中的任何芯片才能满足此属性。

以下示例展示了所有可能的设备属性:

<config:device-targeting-config
    xmlns:config="http://schemas.android.com/apk/config">

    <config:device-group name="myCustomGroup1">
      <config:device-selector ram-min-bytes="8000000000">
        <config:included-device-id brand="google" device="redfin"/>
        <config:included-device-id brand="google" device="sailfish"/>
        <config:included-device-id brand="good-brand"/>
        <config:excluded-device-id brand="google" device="caiman"/>
        <config:system-on-chip manufacturer="Sinclair" model="ZX80"/>
        <config:system-on-chip manufacturer="Commodore" model="C64"/>
      </config:device-selector>
      <config:device-selector ram-min-bytes="16000000000"/>
    </config:device-group>

    <config:device-group name="myCustomGroup2">
      <config:device-selector ram-min-bytes="4000000000" ram-max-bytes="8000000000">
        <config:required-system-feature name="android.hardware.bluetooth"/>
        <config:required-system-feature name="android.hardware.location"/>
        <config:forbidden-system-feature name="android.hardware.camera"/>
        <config:forbidden-system-feature name="mindcontrol.laser"/>
      </config:device-selector>
    </config:device-group>

</config:device-targeting-config>

官方设备制造商和设备型号代码

您可以通过以下任一种方式,使用 Google Play 管理中心的设备目录查找设备制造商和型号代码的正确格式:

  • 使用设备目录检查各个设备,在下例中所示的位置找到制造商和型号代码(以 Google Pixel 4a 为例,制造商为“Google”,型号代码为“sunfish”)

    设备目录中的 Pixel 4a 页面

    设备目录中的 Pixel 4a 页面

  • 下载受支持的设备的 CSV 文件,build_brand 和 build_device 字段分别表示制造商和型号代码。

在应用 bundle 中添加设备定位配置文件

将以下内容添加到主模块的 build.gradle 文件中:

android {
  ...
  bundle {
    deviceTargetingConfig = file('device_targeting_config.xml')
    deviceGroup {
      enableSplit = true   // split bundle by #group
      defaultGroup = "other"  // group used for standalone APKs
    }
  }
  ...
}

device_targeting_config.xml 是相对于主模块的配置文件的路径。这样可确保配置文件与应用 bundle 一起打包。

deviceGroup 子句可确保通过您的 bundle 生成的 APK 按设备组进行拆分。

为 AI 包使用设备定位

您可以仅向能够运行大型模型的设备提供这些模型,从而在设备上保持优化的大小。

通过以下方式按设备组细分 AI 包:获取上一步中创建的现有 AI 包目录,并在相应文件夹后添加后缀 #group_myCustomGroup1、#group_myCustomGroup2 等(如下所述)。在应用中使用 AI 包时,您无需通过后缀来寻址文件夹(换句话说,在构建过程中,系统会自动剥离后缀)。

执行完上一步操作后,目录可能如下所示:

...
.../ai-pack-name/src/main/assets/image-classifier#group_myCustomGroup1/
.../ai-pack-name/src/main/assets/image-classifier#group_myCustomGroup2/
...

在此示例中,您将引用 ai-pack-name/assets/image-classifier/,而不添加任何后缀。

myCustomGroup1 中的设备将接收 image-classifier#group_myCustomGroup1/ 下的所有素材资源,而 myCustomGroup2 中的设备将接收 image-classifier#group_myCustomGroup2/ 下的所有素材资源。

不属于 myCustomGroup1myCustomGroup2 的设备将收到空的 ai-pack-name 包。

这是因为,不属于任何设备组的设备将收到 AI 包的默认变体。这包括不在包含 #group_suffix 的目录中的任何内容。

下载 AI 包后,您可以使用 AssetManager(对于安装时分发的包)或 AiPackManager(对于快速跟进式分发和按需分发的包)检查模型是否存在。示例应用中展示了所有交付模式的实现示例。

为功能模块使用设备定位

您还可以将设备定位用于功能模块。您无需按设备组细分功能模块,只需根据设备组会员资格指定是否应交付整个模块。

如需将功能模块分发给属于 myCustomGroup1myCustomGroup2 的设备,请修改其 AndroidManifest.xml

<manifest ...>
  ...
  <dist:module dist:title="...">
    <dist:delivery>
      <dist:install-time>
        <dist:conditions>
          <dist:device-groups>
            <dist:device-group dist:name="myCustomGroup1"/>
            <dist:device-group dist:name="myCustomGroup2"/>
          </dist:device-groups>
          ...
        </dist:conditions>
      </dist:install-time>
    </dist:delivery>
  </dist:module>
  ...
</manifest>

在本地测试

在为新 bundle 创建发布版本之前,您可以使用内部应用分享或 Bundletool 在本地进行测试。

内部应用分享

借助内部应用分享功能,您可以使用 app bundle 快速生成一个网址,您可以在本地设备上点按该网址来安装 Google Play 会为相应设备安装的内容(如果相应版本的应用在测试轨道或正式版轨道中处于有效状态)。

请参阅内部应用分享说明

bundletool

或者,您可以使用 bundletool(1.18.0 或更高版本)生成 APK,然后将它们旁加载到您的设备上。请按以下步骤使用 bundletool 在本地测试您的应用:

  1. 使用 Android Studio 或 bundletool 构建 app bundle。

  2. 生成带 --local-testing 标记的 APK:

    java -jar bundletool-all.jar build-apks --bundle=path/to/your/bundle.aab \
      --output=output.apks --local-testing
    
  3. 连接设备并运行 bundletool 以旁加载 APK:

    # Example without Device Targeting Configuration
    java -jar bundletool.jar install-apks --apks=output.apks
    
    # Example with Device Targeting Configuration (you must specify which groups the connected device belongs to)
    java -jar bundletool.jar install-apks --apks=output.apks --device-groups=myCustomGroup1,myCustomGroup2
    

使用 bundletool 进行本地测试的限制

以下是使用 bundletool 进行本地测试的限制:

  • fast-follow 软件包的行为方式类同于 on-demand 软件包。也就是说,在应用旁加载过程中,不会自动提取这些软件包。应用启动时,开发者需要手动请求这些软件包;此操作无需对您的应用进行任何代码更改。
  • 软件包从外部存储空间(而非 Play)提取,因此您无法测试代码在出现网络连接错误时的行为。
  • 本地测试不涵盖 wait-for-Wi-Fi 场景。
  • 不支持更新。在安装新版本的 build 之前,请手动卸载先前版本。

验证是否正在安装正确的 APK

请使用以下方法来确保仅在设备上安装正确的 APK

adb shell pm path {packageName}

您应该会看到类似如下内容:

package:{...}/base.apk
package:{...}/split_config.en.apk
package:{...}/split_config.xxhdpi.apk
package:{...}/split_main_ai-pack-name.apk
package:{...}/split_main_ai-pack-name.config.group_myCustomGroup1.apk

请注意,您只会在此列表中看到由功能模块和安装时 AI 包生成的 APK。按需分发和快速跟进式分发 AI 包不会以 APK 的形式安装。

在 Google Play 上测试和发布

建议您使用内部测试轨道在 Google Play 上对应用进行端到端测试。

完成此操作后,您可以使用分阶段发布功能,逐步向正式版发布应用更新。

使用 Play for On-device AI 的示例应用

下载示例应用

本部分演示了如何使用每种投放模式以及设备定位配置。如需开始使用,请参阅本地测试部分。

详细了解 Android App Bundle,并阅读 AI Delivery SDK 的参考资料。