APK 扩展文件

Google Play 要求,用户下载的压缩后 APK 大小不能超过 100 MB。 对于大多数应用而言,这个空间足够存放应用的所有代码和资产。 不过,有些应用需要更多空间存放高保真图形、媒体文件及其他大型资产。 以前,如果应用的压缩后下载大小超过 100 MB,您必须自行托管这些额外的资源并在用户打开应用时下载这些资源。托管和传送额外文件的成本可能并不低,而且用户体验通常也不太理想。为简化这一过程并改善用户体验,Google Play 现在允许您附加两个大型扩展文件以补充您的 APK。

Google Play 可以免费为您的应用托管这些扩展文件并将这些文件传送到设备。扩展文件保存在设备的共享存储位置(SD 卡或 USB 挂载分区;也称为“外部”存储空间),您的应用可以在此处访问扩展文件。在大多数设备上,Google Play 会同时下载扩展文件和 APK,因此当用户首次打开您的应用时,所需的一切都已准备就绪。不过在某些情况下,您的应用必须在启动时从 Google Play 下载这些文件。

如果您不想使用扩展文件且应用的压缩后下载大小超过 100 MB,则应改为使用 Android App Bundle 上传您的应用,这种方式允许的压缩后下载大小上限为 200 MB。此外,由于使用 app bundle 会将 APK 的生成和签名工作转到 Google Play 上完成,因此用户只需下载经过优化的 APK,其中仅包含运行您的应用所需的代码和资源。您无需再构建、签署和管理多个 APK 或扩展文件,而用户也可以获得更小、更优化的下载文件包。

概览

每当您使用 Google Play 管理中心上传 APK 时,您都可以选择向 APK 添加一个或两个扩展文件。每个文件的大小上限为 2 GB,文件格式可以是您选择的任意格式。不过,为了节约下载时的带宽,我们建议您使用压缩格式的文件。从概念上讲,每个扩展文件扮演着不同的角色:

  • 扩展文件是应用必需的额外资源所在的主要扩展文件。
  • 补丁扩展文件是可选的,用于为主扩展文件提供小规模的更新。

尽管您可以按照您需要的方式使用这两个扩展文件,但我们建议您按如下方式使用:主扩展文件提供主要的资源,尽量不要更新;补丁扩展文件应该比较小,用作“补丁载体”,在每次发布重要版本时或根据需要进行更新。

不过,即使应用更新只需要新的补丁扩展文件,您仍然必须上传新的 APK 并在清单中更新 versionCode。(Play 管理中心不允许您将扩展文件上传到现有 APK 中。)

注意:补丁扩展文件在语义特征上与主扩展文件相同,您可以根据需要使用这两个文件。

文件名格式

您上传的每个扩展文件可以是您选择的任意格式(ZIP、PDF、MP4 等)的文件。您还可以使用 JOBB 工具封装并加密一组资源文件及其后续补丁。无论文件类型是什么,Google Play 都将其视为 Opaque Binary Blob 文件,并根据如下结构重命名这些文件:

[main|patch].<expansion-version>.<package-name>.obb

此结构包含三个组成部分:

mainpatch
指定文件是主扩展文件还是补丁扩展文件。每个 APK 只能有一个主扩展文件和一个补丁扩展文件。
<expansion-version>
这是一个整数,与扩展文件“首次”关联的 APK 的版本号一致(与应用的 android:versionCode 值一致)。

强调“首次”是因为,尽管 Play 管理中心允许您将已上传的扩展文件再次用于新的 APK,但该扩展文件的名称不会更改,而是保留您首次上传该文件时所采用的版本。

<package-name>
您应用的 Java 风格的软件包名称。

例如,假设您的 APK 版本为 314159,且您的软件包名称为 com.example.app。如果您上传主扩展文件,该文件就会重命名为:

main.314159.com.example.app.obb

存储位置

当 Google Play 将扩展文件下载到设备时,会将其保存到系统的共享存储位置。为确保应用的正常运行,请勿删除、移动或重命名扩展文件。如果您的应用必须自行从 Google Play 进行下载,那么您必须将文件保存到相同的位置。

getObbDir() 方法会返回扩展文件的具体位置,格式如下:

<shared-storage>/Android/obb/<package-name>/

对每个应用来说,该目录下最多只能包含 2 个扩展文件。 一个是主扩展文件,另一个是补丁扩展文件(若需要)。使用新的扩展文件更新应用时,旧版文件将被覆盖。自 Android 4.4(API 级别 19)起,应用无需外部存储权限即可读取 OBB 扩展文件。不过,某些 Android 6.0(API 级别 23)及更高版本的实现仍然需要权限,因此您需要在应用清单中声明 READ_EXTERNAL_STORAGE 权限并在运行时请求权限,如下所示:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

对于 Android 6 及更高版本,需要在运行时请求外部存储权限。 不过,Android 的某些实现无需权限即可读取 OBB 文件。以下代码段展示了如何在请求外部存储权限之前检查读取权限:

Kotlin

val obb = File(obb_filename)
var open_failed = false

try {
    BufferedReader(FileReader(obb)).also { br ->
        ReadObbFile(br)
    }
} catch (e: IOException) {
    open_failed = true
}

if (open_failed) {
    // request READ_EXTERNAL_STORAGE permission before reading OBB file
    ReadObbFileWithPermission()
}

Java

File obb = new File(obb_filename);
 boolean open_failed = false;

 try {
     BufferedReader br = new BufferedReader(new FileReader(obb));
     open_failed = false;
     ReadObbFile(br);
 } catch (IOException e) {
     open_failed = true;
 }

 if (open_failed) {
     // request READ_EXTERNAL_STORAGE permission before reading OBB file
     ReadObbFileWithPermission();
 }

如果您必须解压缩扩展文件的内容,请不要在随后删除 OBB 扩展文件,也不要将解压缩后的数据保存在同一目录中。您应将解压缩后的文件保存到由 getExternalFilesDir() 指定的目录中。不过,如果可能,您最好使用能够直接读取而不用解压缩数据的扩展文件。例如,我们提供了一个库项目 (APK Expansion Zip Library),它可以直接读取 ZIP 文件中的数据。

注意:与 APK 文件不同,保存在共享存储空间中的所有文件均可以供用户和其他应用读取。

提示:如果您将媒体文件打包成一个 ZIP 文件,您可以通过偏移和长度控制(例如 MediaPlayer.setDataSource()SoundPool.load())对这些文件使用媒体播放调用,而无需解压缩该 ZIP 文件。为了使此功能正常运行,请不要在创建 ZIP 包时对媒体文件执行其他压缩操作。例如,使用 zip 工具时,您应使用 -n 选项指定不应压缩的文件后缀:
zip -n .mp4;.ogg main_expansion media_files

下载流程

大多数情况下,Google Play 在将 APK 下载到设备上的同时会下载并保存您的扩展文件。不过,在某些情况下,Google Play 可能无法下载扩展文件,或者用户删除了以前下载的扩展文件。为应对这些情况,您的应用必须能够在主 Activity 启动时,使用 Google Play 提供的网址自行下载这些文件。

下载流程大致如下所示:

  1. 用户选择通过 Google Play 安装您的应用。
  2. 如果 Google Play 能够下载扩展文件(在大部分设备上都是如此),就会同时下载扩展文件和 APK。

    如果 Google Play 无法下载扩展文件,就会仅下载 APK。

  3. 当用户启动您的应用时,您的应用必须检查扩展文件是否已保存在设备上。
    1. 如果已保存,就说明应用已准备就绪。
    2. 如果未保存,您的应用必须通过 HTTP 协议从 Google Play 下载扩展文件。在这种情况下,您的应用必须使用 Google Play 的应用许可服务向 Google Play 客户端发送请求,以获得包含每个扩展文件的名称、文件大小和网址的响应。有了这些信息,您就可以下载文件并将其保存到适当的存储位置

注意:请务必加入必要的代码,以便在您的应用已启动而设备上没有扩展文件的情况下从 Google Play 下载扩展文件。如下文中的下载扩展文件一节所述,我们为您提供了一个库,它可以大幅简化此流程并通过一项服务执行下载,从而尽可能减少您需提供的代码。

开发核对清单

下面简要说明了需为应用使用扩展文件所应执行的任务:

  1. 首先,确定应用的压缩后下载大小是否需要超过 100 MB。空间非常宝贵,您应尽可能减少总下载大小。如果您的应用需要 100 MB 以上的空间是为了提供多个版本的图片资产以适应多种屏幕密度,不妨考虑发布多个 APK,每个 APK 中只包含针对相应屏幕的资产。为了在发布到 Google Play 时获得最佳效果,请上传一个 Android App Bundle,其中包含您应用的所有已编译代码和资源,但它会将 APK 的生成和签名转到 Google Play 上完成。
  2. 确定应将哪些应用资源从 APK 中分离出来,并将其打包到一个文件中以用作主扩展文件。

    通常情况下,对主扩展文件执行更新时,您只能使用第二个扩展文件(即补丁扩展文件)。不过,如果您的资源很多,超出针对主扩展文件的 2 GB 限制,就可以使用补丁文件放置剩余的资产。

  3. 开发您的应用,使其使用设备共享存储位置中扩展文件中的资源。

    请注意,切勿删除、移动或重命名扩展文件。

    如果您的应用不要求特定格式,我们建议您为扩展文件创建 ZIP 文件,然后使用 APK Expansion Zip Library 读取相应文件。

  4. 向应用的主 Activity 中添加以下逻辑:在设备启动时检查扩展文件是否在设备上。如果扩展文件不在设备上,请使用 Google Play 的应用许可服务请求扩展文件的网址,然后下载并保存它们。

    如需尽量减少必须编写的代码量并确保下载过程中获得良好的用户体验,我们建议您使用 Downloader Library 实施下载行为。

    如果您构建自己的下载服务,而不使用 Downloader Library,请注意不要更改扩展文件的名称,同时务必将其保存到适当的存储位置

应用开发完成后,请按照相应的指导测试扩展文件

规则和限制

当您使用 Play 管理中心上传应用时,可以使用“添加 APK 扩展文件”功能。首次上传应用或更新使用扩展文件的应用时,您必须了解以下规则和限制:

  1. 每个扩展文件的大小不能超过 2 GB。
  2. 为了从 Google Play 下载您的扩展文件,用户必须已通过 Google Play 获取您的应用。如果应用是通过其他方式安装的,Google Play 将不会提供您的扩展文件的网址。
  3. 在您的应用中执行下载任务时,Google Play 针对每个文件提供的网址对于每次下载都是独一无二的,且每个网址在提供给您的应用后不久即会失效。
  4. 如果您为应用提供新的 APK 或为同一应用上传多个 APK,就可以选择使用为旧版 APK 上传的扩展文件。扩展文件的名称不会更改,而且它会保留最初与该文件关联的 APK 的版本信息。
  5. 如果您将扩展文件与多个 APK 结合使用,以便为不同设备提供不同的扩展文件,那么您仍必须针对每个设备上传单独的 APK,以便提供唯一的 versionCode 值,并为每个 APK 声明不同的过滤条件
  6. 您无法通过只更改扩展文件更新应用,而是必须上传新的 APK 更新应用。如果您的更改仅涉及扩展文件中的资源,就只需更改 versionCode(或许还需更改 versionName)即可更新 APK。

  7. 请勿将其他数据保存到您的obb/目录中。如果您必须解压缩某些数据,请将其保存到 getExternalFilesDir() 指定的位置。
  8. 请勿删除或重命名 .obb 扩展文件(除非您正在执行更新)。一旦删除或重命名,Google Play(或您的应用自身)就会反复下载扩展文件。
  9. 在手动更新扩展文件时,您必须删除旧版本的扩展文件。

下载扩展文件

在大多数情况下,Google Play 会在设备安装或更新 APK 的同时将您的扩展文件下载并保存到该设备上。这样,当您的应用首次启动时,扩展文件就已准备就绪。不过在某些情况下,您的应用必须请求从提供给应用的网址(包含在 Google Play 的应用许可服务的响应中)自行下载扩展文件。

下载扩展文件所需的基本逻辑如下所示:

  1. 当应用启动时,在共享存储位置Android/obb/<package-name>/ 目录中)查找扩展文件。
    1. 如果此位置存在扩展文件,则意味着一切准备就绪,您的应用可以继续运行。
    2. 如果此位置没有扩展文件:
      1. 使用 Google Play 的应用许可执行请求,以获取应用的扩展文件名、大小和网址。
      2. 使用 Google Play 提供的网址下载扩展文件并保存。请务必将扩展文件保存到共享存储位置 (Android/obb/<package-name>/),并且确保文件名与 Google Play 响应中提供的名称完全一致。

        注意:Google Play 提供的扩展文件下载网址对于每次下载都是独一无二的,且每个网址在提供给应用后不久即会失效。

如果您的应用是免费的(非付费应用),那么您可能未使用过应用许可服务。此服务的主要作用是针对您的应用实施许可政策,确保已在 Google Play 上支付正当费用的用户拥有使用应用的权利。为了方便您使用扩展文件功能,我们改进了这项许可服务,使其能够向您的应用提供响应(其中包含您应用的扩展文件在 Google Play 上的托管位置的网址)。因此,即使您的应用面向用户免费提供,您也需要添加 License Verification Library (LVL) 才能使用 APK 扩展文件。当然,如果您的应用是免费的,您就不需要实施许可验证,只需要利用 LVL 执行可返回扩展文件网址的请求即可。

注意:无论您的应用是否免费,只有当用户从 Google Play 获取您的应用时,Google Play 才会返回该扩展文件网址。

除 LVL 之外,您还需要提供一组代码,用于通过 HTTP 连接下载扩展文件并将其保存到设备的共享存储空间中的适当位置。将这一过程构建到您的应用中时,请注意以下几个问题:

  • 设备可能没有足够的空间放置扩展文件,因此在开始下载之前,请确保先进行检查,若空间不足,应提醒用户。
  • 文件下载应在后台服务中进行,以免妨碍用户互动,并应允许用户在下载完成时退出应用。
  • 请求和下载过程中更可能会发生各种错误,请务必妥善处理。
  • 网络连接在下载过程中可能会有变动,因此您应妥善处理此类变动。如果下载中断,请适时恢复下载。
  • 尽管下载是在后台进行的,但您应该提供指明下载进度的通知,在完成时通知用户,并根据用户的选择使其返回您的应用。

为帮助您简化这项工作,我们构建了 Downloader Library,它能够通过许可服务请求扩展文件网址、下载扩展文件、执行上述的所有任务,甚至允许 Activity 暂停和恢复下载。在应用中加入 Downloader Library 和少量代码之后,下载扩展文件所需的几乎所有工作就编入您的应用了。因此,我们建议您使用 Downloader Library 下载扩展文件,以提供最佳的用户体验并尽量减少您的工作量。以下部分中的信息说明了如何将 Downloader Library 集成到您的应用中。

如果您希望开发自己的解决方案以使用 Google Play 网址下载扩展文件,请务必按照应用许可文档的规定发送许可请求,然后从响应额外项中检索扩展文件名称、大小和网址。您应使用 APKExpansionPolicy 类(包含在 License Verification Library 中)作为许可政策,该类可从许可服务中获取扩展文件名称、大小和网址。

Downloader Library 简介

如需为您的应用使用 APK 扩展文件,并在尽量减少您的工作量的同时提供最佳用户体验,我们建议您使用 Google Play APK Expansion Library 软件包中的 Downloader Library。这个库能够在后台服务中下载您的扩展文件、向用户显示指明下载状态的通知、应对网络连接中断、适时恢复下载等等。

如需使用 Downloader Library 实施扩展文件下载,您只需执行以下操作:

  • 扩展一个特殊的 Service 子类和一个 BroadcastReceiver 子类,只需为每个子类提供几行代码。
  • 向您的主 Activity 中添加一些逻辑,以检查扩展文件是否已经下载,如果没有,请启动下载流程并显示下载进度界面。
  • 在主 Activity 中实现一个回调接口(包含几种方法),以接收有关下载进度的更新信息。

如需了解如何使用 Downloader Library 设置您的应用,请参阅下文中的几节内容。

使用 Downloader Library 的准备工作

如需使用 Downloader Library,您需要从 SDK 管理器下载两个软件包,并将相应的库添加到您的应用中。

首先,打开 Android SDK Manager (Tools > SDK Manager),然后在“Appearance & Behavior > System Settings > Android SDK”下选择“SDK Tools”标签页,以选择并下载以下软件包:

  • Google Play Licensing Library 软件包
  • Google Play APK Expansion Library 软件包

为 License Verification Library 和 Downloader Library 创建新的库模块。在每个库中:

  1. 依次选择 File > New > New Module
  2. 在“Create New Module”窗口中,选择 Android Library,然后选择 Next
  3. 指定“app/Library name”(例如“Google Play License Library”和“Google Play Downloader Library”),选择“Minimum SDK level”,然后选择 Finish
  4. 依次选择 File > Project Structure
  5. 选择“Properties”标签页,并在“Library Repository”中,输入 <sdk>/extras/google/ 目录中的相应库(针对 License Verification Library 的 play_licensing/ 或者针对 Downloader Library 的 play_apk_expansion/downloader_library/)。
  6. 选择 OK 创建新模块。

注意:Downloader Library 依赖于 License Verification Library。请务必将 License Verification Library 添加到 Downloader Library 的项目属性中。

或者,通过命令行更新您的项目,使其包括这两个库,具体方法如下:

  1. 将目录更改为 <sdk>/tools/ 目录。
  2. 使用 --library 选项执行 android update project,以将 LVL 和 Downloader Library 添加到项目中。例如:
    android update project --path ~/Android/MyApp \
    --library ~/android_sdk/extras/google/market_licensing \
    --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
    

将 License Verification Library 和 Downloader Library 添加到您的应用之后,您可以快速在应用中加入从 Google Play 下载扩展文件的功能。扩展文件采用什么格式以及如何从共享存储空间读取扩展文件是需要您另外实施的任务,您应该根据应用的具体需要做出相应决定。

提示:Apk Expansion 软件包中有一个示例应用,展示了如何在应用中使用 Downloader Library。该示例应用使用的是 Apk Expansion 软件包提供的第三方库,称为 APK Expansion Zip Library。如果您打算为扩展文件使用 ZIP 文件格式,我们建议您也将 APK Expansion Zip Library 添加到您的应用中。如需了解详情,请参阅下文的使用 APK Expansion Zip Library

声明用户权限

为了下载扩展文件,Downloader Library 需要一些权限。请务必在应用的清单文件中声明这些权限,这些权限分别是:

<manifest ...>
    <!-- Required to access Google Play Licensing -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />

    <!-- Required to download files from Google Play -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Required to keep CPU alive while downloading files
        (NOT to keep screen awake) -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- Required to poll the state of the network connection
        and respond to changes -->
    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- Required to check whether Wi-Fi is enabled -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

    <!-- Required to read and write the expansion files on shared storage -->
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

注意:默认情况下,Downloader Library 要求的 API 级别为 4,而 APK Expansion Zip Library 要求的 API 级别为 5。

实施下载程序服务

为了在后台执行下载任务,Downloader Library 提供了自己的名为 DownloaderServiceService 子类,您应该扩展该子类以实施下载程序服务。除了为您下载扩展文件外,DownloaderService 还具有以下功能:

  • 注册 BroadcastReceiver 以监听设备网络连接的更改情况(CONNECTIVITY_ACTION 广播),以便根据需要暂停下载(例如在连接中断时)并适时恢复下载(连接恢复后)。
  • 安排 RTC_WAKEUP 警报,以便在服务被终结时,重新尝试下载。
  • 创建自定义 Notification,显示下载进度以及所有错误或状态更改信息。
  • 允许应用手动暂停和恢复下载。
  • 在下载扩展文件之前,确认以下所有事项:共享存储空间已装载并准备就绪;扩展文件尚不存在;有足够的空间。如有任一方面不符合条件,即通知用户。

您需要做的就是在应用中创建一个类,以便扩展 DownloaderService 类并覆盖三个方法,从而提供特定应用的详细信息:

getPublicKey()
此方法必须返回一个字符串,也就是您发布商帐号的 Base64 编码的 RSA 公钥,可通过 Play 管理中心的个人资料页面获取(请参阅设置许可)。
getSALT()
此方法必须返回随机字节的数组,许可 Policy 会使用该数组生成 Obfuscator。此加密盐可确保用于保存许可数据的已混淆 SharedPreferences 文件是唯一的,不会被发现。
getAlarmReceiverClassName()
此方法必须返回应用中的 BroadcastReceiver 的类名称,该类应收到关于重启下载的警报(如果下载程序服务意外停止,就可能会发生这种情况)。

例如,以下是 DownloaderService 的完整实现:

Kotlin

// You must use the public key belonging to your publisher account
const val BASE64_PUBLIC_KEY = "YourLVLKey"
// You should also modify this salt
val SALT = byteArrayOf(
        1, 42, -12, -1, 54, 98, -100, -12, 43, 2,
        -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
)

class SampleDownloaderService : DownloaderService() {

    override fun getPublicKey(): String = BASE64_PUBLIC_KEY

    override fun getSALT(): ByteArray = SALT

    override fun getAlarmReceiverClassName(): String = SampleAlarmReceiver::class.java.name
}

Java

public class SampleDownloaderService extends DownloaderService {
    // You must use the public key belonging to your publisher account
    public static final String BASE64_PUBLIC_KEY = "YourLVLKey";
    // You should also modify this salt
    public static final byte[] SALT = new byte[] { 1, 42, -12, -1, 54, 98,
            -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84
    };

    @Override
    public String getPublicKey() {
        return BASE64_PUBLIC_KEY;
    }

    @Override
    public byte[] getSALT() {
        return SALT;
    }

    @Override
    public String getAlarmReceiverClassName() {
        return SampleAlarmReceiver.class.getName();
    }
}

注意:请务必将 BASE64_PUBLIC_KEY 值更新为发布商账号的公钥。您可以在 Developer Console 中您的个人资料信息下找到该密钥。即使在测试下载时,此操作也不可缺少。

请务必在您的清单文件中声明以下服务:

<app ...>
    <service android:name=".SampleDownloaderService" />
    ...
</app>

实施警报接收程序

为了监控文件下载进度并根据需要重新开始下载,DownloaderService 安排了一个 RTC_WAKEUP 警报,可向应用中的 BroadcastReceiver 传递 Intent。您必须定义 BroadcastReceiver 才能从 Downloader Library 调用 API,从而检查下载情况并根据需要重新开始下载。

您只需覆盖 onReceive() 方法,以调用 DownloaderClientMarshaller.startDownloadServiceIfRequired()

例如:

Kotlin

class SampleAlarmReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        try {
            DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    context,
                    intent,
                    SampleDownloaderService::class.java
            )
        } catch (e: PackageManager.NameNotFoundException) {
            e.printStackTrace()
        }
    }
}

Java

public class SampleAlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        try {
            DownloaderClientMarshaller.startDownloadServiceIfRequired(context,
                intent, SampleDownloaderService.class);
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}

请注意,该类就是您必须在服务的 getAlarmReceiverClassName() 方法中返回其名称的类(请参阅上一节内容)。

请务必在您的清单文件中声明接收程序:

<app ...>
    <receiver android:name=".SampleAlarmReceiver" />
    ...
</app>

开始下载

应用中的主 activity(由启动器图标启动的 activity)应负责确认设备上是否已存在扩展文件,若没有,就启动下载服务。

使用 Downloader Library 启动下载服务的流程如下:

  1. 查看文件是否已经下载。

    Downloader Library 的 Helper 类中包含一些可简化这一过程的 API:

    • getExpansionAPKFileName(Context, c, boolean mainFile, int versionCode)
    • doesFileExist(Context c, String fileName, long fileSize)

    例如,Apk Expansion 软件包中的示例应用会在 Activity 的 onCreate() 方法中调用以下方法,检查设备上是否已经存在扩展文件:

    Kotlin

    fun expansionFilesDelivered(): Boolean {
        xAPKS.forEach { xf ->
            Helpers.getExpansionAPKFileName(this, xf.isBase, xf.fileVersion).also { fileName ->
                if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false))
                    return false
            }
        }
        return true
    }
    

    Java

    boolean expansionFilesDelivered() {
        for (XAPKFile xf : xAPKS) {
            String fileName = Helpers.getExpansionAPKFileName(this, xf.isBase,
                xf.fileVersion);
            if (!Helpers.doesFileExist(this, fileName, xf.fileSize, false))
                return false;
        }
        return true;
    }
    

    在这种情况下,每个 XAPKFile 对象都保存了已知扩展文件的版本号和文件大小,以及一个表明该文件是否是主扩展文件的布尔值。(如需了解详情,请参阅示例应用的 SampleDownloaderActivity 类。)

    如果此方法返回 false,应用必须开始下载文件。

  2. 通过调用静态方法 DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class<?> serviceClass) 开始下载。

    此方法采用以下参数:

    • context:应用的 Context
    • notificationClient:用于启动主 Activity 的 PendingIntentDownloaderService 创建的 Notification 中会用到它,以显示下载进度。用户选择通知后,系统会调用您在此处提供的 PendingIntent 并且应该会打开显示下载进度的 Activity(通常就是启动下载的 Activity)。
    • serviceClassDownloaderService 的实现代码对应的 Class 对象。必须提供,这样才能启动服务并根据需要开始下载。

    此方法会返回一个整数,指明是否需要下载文件。可能的值为:

    • NO_DOWNLOAD_REQUIRED:如果文件已经存在或下载已在进行中,就返回此值。
    • LVL_CHECK_REQUIRED:如果需要许可验证以获取扩展文件网址,就返回此值。
    • DOWNLOAD_REQUIRED:如果扩展文件网址已知,但尚未下载文件,就返回此值。

    LVL_CHECK_REQUIREDDOWNLOAD_REQUIRED 的行为基本相同,您通常不需要担心这两个值。在调用 startDownloadServiceIfRequired() 的主 Activity 中,您只需查看返回的值是否为 NO_DOWNLOAD_REQUIRED。如果不是 NO_DOWNLOAD_REQUIRED,Downloader Library 就会开始下载文件,同时您应更新 Activity 界面以显示下载进度(参见下一步)。如果返回的值为 NO_DOWNLOAD_REQUIRED,表示文件已经存在,您的应用可以启动了。

    例如:

    Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        // Check if expansion files are available before going any further
        if (!expansionFilesDelivered()) {
            val pendingIntent =
                    // Build an Intent to start this activity from the Notification
                    Intent(this, MainActivity::class.java).apply {
                        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
                    }.let { notifierIntent ->
                        PendingIntent.getActivity(
                                this,
                                0,
                                notifierIntent,
                                PendingIntent.FLAG_UPDATE_CURRENT
                        )
                    }
    
    
            // Start the download service (if required)
            val startResult: Int = DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    this,
                    pendingIntent,
                    SampleDownloaderService::class.java
            )
            // If download has started, initialize this activity to show
            // download progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // This is where you do set up to display the download
                // progress (next step)
                ...
                return
            } // If the download wasn't necessary, fall through to start the app
        }
        startApp() // Expansion files are available, start the app
    }
    

    Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // Check if expansion files are available before going any further
        if (!expansionFilesDelivered()) {
            // Build an Intent to start this activity from the Notification
            Intent notifierIntent = new Intent(this, MainActivity.getClass());
            notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                                    Intent.FLAG_ACTIVITY_CLEAR_TOP);
            ...
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                    notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    
            // Start the download service (if required)
            int startResult =
                DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
                            pendingIntent, SampleDownloaderService.class);
            // If download has started, initialize this activity to show
            // download progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // This is where you do set up to display the download
                // progress (next step)
                ...
                return;
            } // If the download wasn't necessary, fall through to start the app
        }
        startApp(); // Expansion files are available, start the app
    }
    
  3. startDownloadServiceIfRequired() 方法返回的不是 NO_DOWNLOAD_REQUIRED 时,请通过调用 DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService) 创建 IStub 实例。IStub 可将 Activity 与下载程序服务进行绑定,以便 Activity 接收与下载进度有关的回调。

    为了通过调用 CreateStub() 实例化您的 IStub,您必须向其传递 IDownloaderClient 接口的实现代码和 DownloaderService 实现代码。关于接收下载进度信息的下一部分中介绍了 IDownloaderClient 接口(通常情况下,您应该在 Activity 类中实现该接口,以便在下载状态改变后更新 Activity 界面)。

    我们建议您在 startDownloadServiceIfRequired() 启动下载后,在 Activity 的 onCreate() 方法执行过程中调用 CreateStub() 实例化您的 IStub

    例如,在之前的 onCreate() 代码示例中,您可以通过如下方式响应 startDownloadServiceIfRequired() 结果:

    Kotlin

            // Start the download service (if required)
            val startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(
                    this@MainActivity,
                    pendingIntent,
                    SampleDownloaderService::class.java
            )
            // If download has started, initialize activity to show progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // Instantiate a member instance of IStub
                downloaderClientStub =
                        DownloaderClientMarshaller.CreateStub(this, SampleDownloaderService::class.java)
                // Inflate layout that shows download progress
                setContentView(R.layout.downloader_ui)
                return
            }
    

    Java

            // Start the download service (if required)
            int startResult =
                DownloaderClientMarshaller.startDownloadServiceIfRequired(this,
                            pendingIntent, SampleDownloaderService.class);
            // If download has started, initialize activity to show progress
            if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
                // Instantiate a member instance of IStub
                downloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
                        SampleDownloaderService.class);
                // Inflate layout that shows download progress
                setContentView(R.layout.downloader_ui);
                return;
            }
    

    onCreate() 方法返回后,您的 Activity 会收到 onResume() 的调用,然后您应在该方法中对 IStub 调用 connect(),并传入应用的 Context。反之,您应该在 Activity 的 onStop() 回调中调用 disconnect()

    Kotlin

    override fun onResume() {
        downloaderClientStub?.connect(this)
        super.onResume()
    }
    
    override fun onStop() {
        downloaderClientStub?.disconnect(this)
        super.onStop()
    }
    

    Java

    @Override
    protected void onResume() {
        if (null != downloaderClientStub) {
            downloaderClientStub.connect(this);
        }
        super.onResume();
    }
    
    @Override
    protected void onStop() {
        if (null != downloaderClientStub) {
            downloaderClientStub.disconnect(this);
        }
        super.onStop();
    }
    

    IStub 调用 connect() 可将 Activity 与 DownloaderService 进行绑定,以便 Activity 通过 IDownloaderClient 接口接收与下载状态更改相关的回调。

接收下载进度信息

如需接收有关下载进度的更新信息并与 DownloaderService 互动,您必须实现 Downloader Library 的 IDownloaderClient 接口。 通常情况下,用于启动下载的 Activity 应该会实现此接口,以便显示下载进度并向服务发送请求。

IDownloaderClient 必需的接口方法是:

onServiceConnected(Messenger m)
在 Activity 中实例化 IStub 后,您将收到对此方法的调用,其中传递了与 DownloaderService 的实例关联的 Messenger 对象。如需向服务发送请求(例如暂停或恢复下载),您必须调用 DownloaderServiceMarshaller.CreateProxy() 以接收与服务关联的 IDownloaderService 接口。

建议的实现代码如下所示:

Kotlin

private var remoteService: IDownloaderService? = null
...

override fun onServiceConnected(m: Messenger) {
    remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply {
        downloaderClientStub?.messenger?.also { messenger ->
            onClientUpdated(messenger)
        }
    }
}

Java

private IDownloaderService remoteService;
...

@Override
public void onServiceConnected(Messenger m) {
    remoteService = DownloaderServiceMarshaller.CreateProxy(m);
    remoteService.onClientUpdated(downloaderClientStub.getMessenger());
}

初始化 IDownloaderService 对象后,您可以向下载程序服务发送命令,例如暂停或恢复下载(requestPauseDownload()requestContinueDownload())。

onDownloadStateChanged(int newState)
当下载状态发生变化(例如下载开始或完成)时,下载服务会调用此方法。

newState 值是由 IDownloaderClient 类的其中一个 STATE_* 常量指定的可能值之一。

为了向用户提供有用的信息,您可以调用 Helpers.getDownloaderStringResourceIDFromState() 为每个状态请求相应的字符串。这会返回一个与 Downloader Library 绑定的其中一个字符串对应的资源 ID。例如,字符串“Download paused because you are roaming”(当前在漫游状态,下载停止)对应于 STATE_PAUSED_ROAMING

onDownloadProgress(DownloadProgressInfo progress)
下载服务会调用此方法来提供 DownloadProgressInfo 对象,该对象说明了有关下载进度的各种信息,包括预计剩余时间、当前速度、总体进度和文件总数,以便您更新下载进度界面。

提示:如需了解更新下载进度界面的这些示例回调,请参阅 Apk Expansion 软件包随附的示例应用中的 SampleDownloaderActivity

IDownloaderService 接口的下列公开方法可能对您很有用:

requestPauseDownload()
暂停下载。
requestContinueDownload()
恢复已暂停的下载。
setDownloadFlags(int flags)
设置用户首选的文件下载可用网络类型。当前的实现代码仅支持一个标志,即 FLAGS_DOWNLOAD_OVER_CELLULAR,不过您可以添加其他标志。默认情况下,此标志是处于停用状态的,因此用户必须使用 WLAN 网络才能下载扩展文件。如果您希望用户能够选择通过移动网络启动下载服务,则可以调用:

Kotlin

remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply {
    ...
    setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR)
}

Java

remoteService
    .setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);

使用 APKExpansionPolicy

如果您决定自行构建下载程序服务,而不使用 Google Play Downloader Library,仍应使用 License Verification Library 中提供的 APKExpansionPolicyAPKExpansionPolicy 类几乎与 ServerManagedPolicy 类(位于 Google Play License Verification Library)是相同的,不过前者还包含对 APK 扩展文件响应额外项的额外处理。

注意:如果您确实是按照上一节内容所述使用 Downloader Library,那么这个库会执行与 APKExpansionPolicy 的所有互动,因此您不需要直接使用这个类。

这个类包含以下方法,可帮助您获取现有扩展文件的必要信息:

  • getExpansionURLCount()
  • getExpansionURL(int index)
  • getExpansionFileName(int index)
  • getExpansionFileSize(int index)

如需详细了解如何在不使用 Downloader Library 的情况下使用 APKExpansionPolicy,请参阅将许可添加至您的应用一文,其中介绍了如何实现包括该类在内的许可策略。

读取扩展文件

APK 扩展文件保存到设备之后,读取这些文件的方式取决于您所用的文件类型。正如概览中所述,您的扩展文件可以是您需要的任何文件类型,但系统会使用特定的文件名格式对其重命名,并保存到 <shared-storage>/Android/obb/<package-name>/

不论您如何读取文件,都请务必先检查外部存储是否可以读取。因为用户有可能通过 USB 将存储装到了计算机上,也有可能已经移除了 SD 卡。

注意:在应用启动时,请务必调用 getExternalStorageState() 以检查外部存储空间是否可用、是否可读取。此函数会返回几个可能的字符串之一,这些字符串分别代表外部存储的状态。返回值必须是 MEDIA_MOUNTED,应用才能读取外部存储。

获取文件名

概览中所述,您的 APK 扩展文件会采用特定的文件名格式保存到设备:

[main|patch].<expansion-version>.<package-name>.obb

如需获取扩展文件的位置和名称,您应使用 getExternalStorageDirectory()getPackageName() 方法构建文件路径。

您可以在应用中使用以下方法,获取包含两个扩展文件的完整路径的数组:

Kotlin

fun getAPKExpansionFiles(ctx: Context, mainVersion: Int, patchVersion: Int): Array<String> {
    val packageName = ctx.packageName
    val ret = mutableListOf<String>()
    if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
        // Build the full path to the app's expansion files
        val root = Environment.getExternalStorageDirectory()
        val expPath = File(root.toString() + EXP_PATH + packageName)

        // Check that expansion file path exists
        if (expPath.exists()) {
            if (mainVersion > 0) {
                val strMainPath = "$expPath${File.separator}main.$mainVersion.$packageName.obb"
                val main = File(strMainPath)
                if (main.isFile) {
                    ret += strMainPath
                }
            }
            if (patchVersion > 0) {
                val strPatchPath = "$expPath${File.separator}patch.$mainVersion.$packageName.obb"
                val main = File(strPatchPath)
                if (main.isFile) {
                    ret += strPatchPath
                }
            }
        }
    }
    return ret.toTypedArray()
}

Java

// The shared path to all app expansion files
private final static String EXP_PATH = "/Android/obb/";

static String[] getAPKExpansionFiles(Context ctx, int mainVersion,
      int patchVersion) {
    String packageName = ctx.getPackageName();
    Vector<String> ret = new Vector<String>();
    if (Environment.getExternalStorageState()
          .equals(Environment.MEDIA_MOUNTED)) {
        // Build the full path to the app's expansion files
        File root = Environment.getExternalStorageDirectory();
        File expPath = new File(root.toString() + EXP_PATH + packageName);

        // Check that expansion file path exists
        if (expPath.exists()) {
            if ( mainVersion > 0 ) {
                String strMainPath = expPath + File.separator + "main." +
                        mainVersion + "." + packageName + ".obb";
                File main = new File(strMainPath);
                if ( main.isFile() ) {
                        ret.add(strMainPath);
                }
            }
            if ( patchVersion > 0 ) {
                String strPatchPath = expPath + File.separator + "patch." +
                        mainVersion + "." + packageName + ".obb";
                File main = new File(strPatchPath);
                if ( main.isFile() ) {
                        ret.add(strPatchPath);
                }
            }
        }
    }
    String[] retArray = new String[ret.size()];
    ret.toArray(retArray);
    return retArray;
}

在调用此方法时,您可以向其传递应用 Context 和所需扩展文件的版本。

确定扩展文件版本号的方法有很多种。其中一个简单的方法是,在下载开始时,使用 APKExpansionPolicy 类的 getExpansionFileName(int index) 方法查询扩展文件名,然后将版本保存在 SharedPreferences 文件中。这样,在需要访问扩展文件时,通过读取 SharedPreferences 文件即可获得版本代码。

如需详细了解如何从共享存储空间读取文件,请参阅数据存储文档。

使用 APK Expansion Zip Library

Google Market Apk Expansion 软件包中有一个名为 APK Expansion Zip Library 的库(位于 <sdk>/extras/google/google_market_apk_expansion/zip_file/)。您可以根据需要使用这个库,以帮助您读取保存为 ZIP 文件的扩展文件。借助这个库,您可以把 ZIP 扩展文件当做虚拟文件系统,从而轻松地从中读取资源。

APK Expansion Zip Library 包括以下类和 API:

APKExpansionSupport
提供一些访问扩展文件名和 ZIP 文件的方法:
getAPKExpansionFiles()
上述方法可返回两个扩展文件的完整路径。
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
返回包含主扩展文件和补丁扩展文件的 ZipResourceFile。也就是说,如果您同时指定 mainVersionpatchVersion,此方法会返回使您能够读取所有数据的 ZipResourceFile,并且补丁扩展文件的数据合并到主扩展文件中。
ZipResourceFile
表示共享存储空间中的 ZIP 文件,可执行所有工作以提供基于 ZIP 文件的虚拟文件系统。您可以使用 APKExpansionSupport.getAPKExpansionZipFile() 获得实例,或者通过向 ZipResourceFile 传递扩展文件的路径而获得实例。这个类包括各种有用的方法,不过其中大部分是您用不到的。一些很重要的方法包括:
getInputStream(String assetPath)
提供 InputStream 以读取 ZIP 文件中的文件。assetPath 必须是所需文件的路径,并且是 ZIP 文件内容的根目录的相对路径。
getAssetFileDescriptor(String assetPath)
为 ZIP 文件中的文件提供 AssetFileDescriptorassetPath 必须是所需文件的路径,并且是 ZIP 文件内容的根目录的相对路径。这对于某些需要 AssetFileDescriptor 的 Android API 来说十分有用,比如一些 MediaPlayer API。
APEZProvider
大多数应用不需要使用此类。它会定义一个 ContentProvider,其中通过 content provider Uri 编组了 ZIP 文件中的数据,以便为某些需要通过 Uri 访问媒体文件的 Android API 提供文件访问权限。例如,如果您想通过 VideoView.setVideoURI() 播放视频,那么这个类就很有用。

跳过媒体文件的 ZIP 压缩

如果您使用扩展文件存储媒体文件,那么对于 ZIP 文件,您可以使用提供偏移和长度控制的 Android 媒体播放调用(例如 MediaPlayer.setDataSource()SoundPool.load())。为了使此功能正常运行,请不要在创建 ZIP 压缩包时对媒体文件执行其他压缩操作。例如,使用 zip 工具时,您应使用 -n 选项指定不应压缩的文件后缀:

zip -n .mp4;.ogg main_expansion media_files

从 ZIP 文件中读取

在使用 APK Expansion Zip Library 时,从 ZIP 中读取文件通常需要以下代码:

Kotlin

// Get a ZipResourceFile representing a merger of both the main and patch files
val expansionFile =
        APKExpansionSupport.getAPKExpansionZipFile(appContext, mainVersion, patchVersion)

// Get an input stream for a known file inside the expansion file ZIPs
expansionFile.getInputStream(pathToFileInsideZip).use {
    ...
}

Java

// Get a ZipResourceFile representing a merger of both the main and patch files
ZipResourceFile expansionFile =
    APKExpansionSupport.getAPKExpansionZipFile(appContext,
        mainVersion, patchVersion);

// Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

您可以利用上述代码读取主扩展文件和补丁扩展文件中所有文件的合并图,从而能够访问两个文件中存在的任何文件。您只需向 getAPKExpansionFile() 方法提供应用 android.content.Context 以及主扩展文件和补丁扩展文件的版本号即可。

如果您想从具体的扩展文件中读取数据,可以使用 ZipResourceFile 构造函数以及所需扩展文件的路径:

Kotlin

// Get a ZipResourceFile representing a specific expansion file
val expansionFile = ZipResourceFile(filePathToMyZip)

// Get an input stream for a known file inside the expansion file ZIPs
expansionFile.getInputStream(pathToFileInsideZip).use {
    ...
}

Java

// Get a ZipResourceFile representing a specific expansion file
ZipResourceFile expansionFile = new ZipResourceFile(filePathToMyZip);

// Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);

如需详细了解如何针对扩展文件使用这个库,请查看示例应用的 SampleDownloaderActivity 类,其中包含使用 CRC 验证已下载文件的附加代码。请注意,如果您在此示例的基础上构建您自己的实现代码,则必须在 xAPKS 数组中声明扩展文件的字节大小

测试扩展文件

在发布应用之前,您应该测试两项内容:读取扩展文件和下载扩展文件。

测试文件读取

将应用上传到 Google Play 之前,您应测试应用读取共享存储空间中文件的功能。测试过程很简单,您只需将文件添加到设备共享存储空间的相应位置,然后启动应用即可。具体步骤如下:

  1. 在设备的共享存储空间中创建相应目录,Google Play 会将您的文件保存到该目录中。

    例如,如果您的软件包名称为 com.example.android,就需要在共享存储空间中创建目录Android/obb/com.example.android/。(将测试设备插入计算机以装载共享存储空间,然后手动创建此目录。)

  2. 手动将扩展文件添加到该目录。请务必重命名文件以匹配 Google Play 将采用的文件名格式

    例如,无论文件类型如何,com.example.android 应用的主扩展文件应为 main.0300110.com.example.android.obb。 版本代码可以是任意值。请注意以下几点:

    • 主扩展文件必须以 main 开头,补丁扩展文件以 patch 开头。
    • 软件包名称务必与 Google Play 上将附加该文件的 APK 名称一致。
  3. 扩展文件现已保存到设备上,您可以安装并运行应用以测试扩展文件。

以下是一些有关处理扩展文件的注意事项:

  • 请勿删除或重命名 .obb 扩展文件,即使您将数据解压缩到其他位置也是如此。一旦删除或重命名,Google Play(或您的应用自身)就会反复下载扩展文件。
  • 请勿将其他数据保存到您的obb/目录中。如果您必须解压缩某些数据,请将其保存到 getExternalFilesDir() 指定的位置。

测试文件下载

因为在某些情况下,您的应用必须在首次启用时手动下载扩展文件,因此请务必测试此流程,以确保应用能够成功查询网址、下载文件并将其保存到设备。

如需测试应用的手动下载程序的实现情况,您可以将应用发布到内部测试轨道,这样一来只有获得授权的测试人员能够访问应用。如果一切正常,只要主 Activity 启动,您的应用就会开始下载扩展文件。

注意:过去,您可以通过上传未发布的“草稿”版本来测试应用。但现在,此功能已不再受支持。您必须将应用发布到内部封闭式或开放式测试轨道。如需了解详情,请参阅草稿应用不再受支持

更新应用

在 Google Play 上使用扩展文件的最大好处是:应用更新时用户无需重复下载所有原始资产。因为 Google Play 允许您为每个 APK 提供两个扩展文件,所以您可以将第二个文件用作提供更新和新资产的“补丁”。这样一来,用户就不需要重复下载主扩展文件了(主扩展文件可能很大,而且下载需要成本)。

补丁扩展文件从技术上讲与主扩展文件相同,Android 系统或 Google Play 平台都不会在主扩展文件和补丁扩展文件之间执行实际的补丁操作。您的应用代码必须自行执行必要的补丁操作。

如果您使用 ZIP 文件作为扩展文件,Apk Expansion 软件包中的 APK Expansion Zip Library 就可以将您的补丁扩展文件和主扩展文件合并到一起。

注意:即使只需要对补丁扩展文件做出一些更改,您也必须更新 APK,以便 Google Play 执行更新。如果您不需要在应用中对代码进行更改,则只需更新清单中的 versionCode

只要您不更改在 Play 管理中心中与 APK 关联的主扩展文件,那么之前安装过应用的用户就不需要下载主扩展文件。现有用户只接收更新的 APK 和新的补丁扩展文件(保留之前的主扩展文件)。

有关扩展文件的更新问题,请注意以下事项:

  • 您的应用同时只能拥有两个扩展文件:主扩展文件和补丁扩展文件。在更新其中某个文件时,Google Play 会将之前的版本删除(应用在执行手动更新时也必须如此)。
  • 添加补丁扩展文件后,Android 系统不会对您的应用或主扩展文件执行实际的补丁操作。您必须将应用设计为支持补丁数据。 不过,Apk Expansion 软件包中有一个库,如果您将 ZIP 文件作为扩展文件使用,可以使用这个库,它能将补丁扩展文件的数据合并到主扩展文件,这样您就可以轻松读取所有扩展文件数据。