Google Play 要求用户下载的压缩 APK 不得超过 100 MB。对于大多数应用来说,这足以容纳所有应用的代码和资源。但是,某些应用需要更多空间来存储高保真图形、媒体文件或其他大型资源。以前,如果您的应用的压缩下载大小超过 100 MB,则必须在用户打开应用时自行托管和下载其他资源。托管和提供额外文件可能成本高昂,并且用户体验通常不理想。为了让您更轻松地执行此过程并为用户提供更愉快的体验,Google Play 允许您附加两个大型扩展文件来补充您的 APK。
Google Play 托管您的应用的扩展文件,并免费提供给设备。扩展文件保存在设备的共享存储位置(SD 卡或可安装 USB 分区;也称为“外部”存储),您的应用可以在此处访问它们。在大多数设备上,Google Play 会在下载 APK 的同时下载扩展文件,因此您的应用在用户首次打开它时拥有所需的一切。但是,在某些情况下,您的应用必须在应用启动时从 Google Play 下载文件。
如果您想避免使用扩展文件,并且您的应用的压缩下载大小大于 100 MB,则应改为使用 Android 应用包 上传您的应用,这允许压缩下载大小最大为 200 MB。此外,由于使用应用包会将 APK 生成和签名推迟到 Google Play,因此用户仅下载运行您的应用所需的代码和资源的优化 APK。您无需构建、签名和管理多个 APK 或扩展文件,用户可以获得更小、更优化的下载。
概述
每次您使用 Google Play Console 上传 APK 时,可以选择向 APK 添加一个或两个扩展文件。每个文件的大小最多可达 2GB,并且可以使用您选择的任何格式,但我们建议您使用压缩文件以节省下载期间的带宽。从概念上讲,每个扩展文件都扮演着不同的角色。
- **主**扩展文件是应用所需附加资源的主要扩展文件。
- **补丁**扩展文件是可选的,用于对主扩展文件进行少量更新。
虽然您可以根据需要使用这两个扩展文件,但我们建议主扩展文件提供主要资源,并且应很少更新;补丁扩展文件应更小,并用作“补丁载体”,在每次主要版本发布或必要时更新。
但是,即使您的应用更新只需要一个新的补丁扩展文件,您仍然必须上传一个新的 APK,并在清单中更新 versionCode
。(Play Console 不允许您将扩展文件上传到现有的 APK。)
注意:补丁扩展文件在语义上与主扩展文件相同——您可以根据需要使用每个文件。
文件名格式
您上传的每个扩展文件都可以使用您选择的任何格式(ZIP、PDF、MP4 等)。您还可以使用 JOBB 工具封装和加密一组资源文件及其后续补丁。无论文件类型如何,Google Play 都将其视为不透明的二进制 Blob,并使用以下方案重命名文件。
[main|patch].<expansion-version>.<package-name>.obb
此方案包含三个组件
main
或patch
- 指定文件是主扩展文件还是补丁扩展文件。每个 APK 只能有一个主文件和一个补丁文件。
<expansion-version>
- 这是一个整数,与扩展文件**首次**关联的 APK 的版本代码匹配(它与应用的
android:versionCode
值匹配)。之所以强调“首次”,是因为尽管 Play Console 允许您将上传的扩展文件与新的 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>/
<shared-storage>
是共享存储空间的路径,可从getExternalStorageDirectory()
获取。<package-name>
是您的应用的 Java 样式包名,可从getPackageName()
获取。
对于每个应用,此目录中最多只有两个扩展文件。一个是主扩展文件,另一个是补丁扩展文件(如果需要)。当您使用新的扩展文件更新应用时,先前版本会被覆盖。从 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 扩展 Zip 库 的库项目,该项目直接从 ZIP 文件中读取您的数据。
注意:与 APK 文件不同,共享存储上保存的任何文件都可以被用户和其他应用读取。
提示:如果将媒体文件打包到 ZIP 中,则可以在具有偏移量和长度控制的文件上使用媒体播放调用(例如 MediaPlayer.setDataSource()
和 SoundPool.load()
),而无需解压缩 ZIP。为了使此功能正常工作,在创建 ZIP 包时,您不得对媒体文件执行其他压缩。例如,当使用 zip
工具时,您应该使用 -n
选项指定不应压缩的文件后缀
zip -n .mp4;.ogg main_expansion media_files
下载过程
大多数情况下,Google Play 会在将 APK 下载到设备的同时下载并保存您的扩展文件。但是,在某些情况下,Google Play 无法下载扩展文件,或者用户可能已删除了以前下载的扩展文件。为了处理这些情况,您的应用必须能够在主活动启动时通过 Google Play 提供的 URL 自行下载文件。
从高级别来看,下载过程如下所示
- 用户从 Google Play 选择安装您的应用。
- 如果 Google Play 能够下载扩展文件(大多数设备都是这种情况),则会与 APK 一起下载。
如果 Google Play 无法下载扩展文件,则仅下载 APK。
- 当用户启动您的应用时,您的应用必须检查扩展文件是否已保存到设备上。
注意:至关重要的是,您必须包含必要的代码以在应用启动时设备上不存在文件的情况下从 Google Play 下载扩展文件。如以下关于 下载扩展文件 的部分所述,我们为您提供了一个库,该库极大地简化了此过程,并从服务中执行下载,而您只需编写少量代码。
开发清单
以下是您应执行的任务摘要,以便在应用中使用扩展文件
- 首先确定应用的压缩下载大小是否需要超过 100MB。空间非常宝贵,您应该尽可能减小总下载大小。如果您的应用使用超过 100MB 来为多个屏幕密度提供图形资源的多个版本,请考虑改为发布 多个 APK,其中每个 APK 只包含其目标屏幕所需的资源。为了获得在 Google Play 上发布的最佳效果,请上传一个 Android 应用包,其中包含应用的所有已编译代码和资源,但将 APK 生成和签名延迟到 Google Play。
- 确定哪些应用资源需要与 APK 分离并将其打包到文件中以用作主扩展文件。
通常,您应该只在对主扩展文件执行更新时使用第二个补丁扩展文件。但是,如果您的资源超过了主扩展文件的 2GB 限制,则可以使用补丁文件存储其余资源。
- 开发您的应用,使其使用设备 共享存储位置 中扩展文件中的资源。
请记住,您不得删除、移动或重命名扩展文件。
如果您的应用不需要特定的格式,我们建议您为扩展文件创建 ZIP 文件,然后使用 APK 扩展 Zip 库 读取它们。
- 向应用的主活动添加逻辑,以便在启动时检查设备上是否存在扩展文件。如果设备上不存在文件,请使用 Google Play 的 应用许可 服务请求扩展文件的 URL,然后下载并保存它们。
为了大大减少您必须编写的代码量并确保在下载过程中获得良好的用户体验,我们建议您使用 下载程序库 来实现您的下载行为。
如果您构建了自己的下载服务而不是使用库,请注意,您不得更改扩展文件的文件名,并且必须将其保存到正确的 存储位置。
完成应用开发后,请按照指南 测试扩展文件。
规则和限制
添加 APK 扩展文件是使用 Play Console 上传应用时可用的功能。首次上传应用或更新使用扩展文件的应用时,必须注意以下规则和限制
- 每个扩展文件的大小不得超过 2GB。
- 为了从 Google Play 下载扩展文件,**用户必须从 Google Play 获取您的应用**。如果应用通过其他方式安装,Google Play 将不会提供扩展文件的 URL。
- 在应用内执行下载时,Google Play 为每个文件提供的 URL 对于每次下载都是唯一的,并且每个 URL 在提供给您的应用后不久就会过期。
- 如果您使用新的 APK 更新应用或上传同一应用的 多个 APK,则可以选择您为以前的 APK 上传的扩展文件。**扩展文件的文件名不会更改**——它保留了最初关联的文件的 APK 收到的版本。
- 如果您将扩展文件与 多个 APK 结合使用以为了为不同的设备提供不同的扩展文件,则您仍然必须为每个设备上传单独的 APK 以提供唯一的
versionCode
值并为每个 APK 声明不同的 筛选器。 - 您不能仅通过更改扩展文件来发布应用更新——**您必须上传新的 APK**才能更新应用。如果您的更改仅涉及扩展文件中的资源,则只需更改
versionCode
(以及可能还有versionName
)即可更新 APK。 - 不要将其他数据保存到您的
obb/
目录中。如果您必须解压某些数据,请将其保存到由getExternalFilesDir()
指定的位置。 - 不要删除或重命名
.obb
扩展文件(除非您正在执行更新)。这样做会导致 Google Play(或您的应用本身)重复下载扩展文件。 - 手动更新扩展文件时,必须删除之前的扩展文件。
下载扩展文件
在大多数情况下,Google Play 在安装或更新 APK 的同时下载并保存扩展文件到设备。这样,扩展文件在您的应用首次启动时即可使用。但是,在某些情况下,您的应用必须通过从 Google Play 的 应用许可 服务的响应中提供的 URL 请求扩展文件来自行下载它们。
下载扩展文件所需的基本逻辑如下
- 当您的应用启动时,在 共享存储位置(在
Android/obb/<package-name>/
目录中)查找扩展文件。
如果您的应用是免费的(不是付费应用),那么您可能没有使用 应用许可 服务。它主要用于您执行应用的许可策略,并确保用户有权使用您的应用(他或她在 Google Play 上合法地支付了费用)。为了促进扩展文件功能,许可服务已得到增强,可以向您的应用提供包含您的应用扩展文件 URL 的响应,这些文件托管在 Google Play 上。因此,即使您的应用对用户免费,您也需要包含许可验证库 (LVL) 以使用 APK 扩展文件。当然,如果您的应用是免费的,则无需执行许可验证,您只需使用该库执行返回扩展文件 URL 的请求即可。
注意:无论您的应用是否免费,Google Play 仅当用户从 Google Play 获取您的应用时才会返回扩展文件 URL。
除了 LVL 之外,您还需要一组代码,这些代码通过 HTTP 连接下载扩展文件并将它们保存到设备共享存储上的正确位置。当您将此过程构建到您的应用中时,您应该考虑几个问题
- 设备可能没有足够的存储空间来容纳扩展文件,因此您应该在开始下载之前进行检查,并在存储空间不足时警告用户。
- 文件下载应在后台服务中进行,以避免阻塞用户交互并允许用户在下载完成时离开您的应用。
- 在请求和下载过程中可能会发生各种错误,您必须优雅地处理这些错误。
- 网络连接在下载过程中可能会发生变化,因此您应该处理此类变化,并在中断时尽可能恢复下载。
- 在后台下载时,您应该提供一个通知,指示下载进度,在下载完成后通知用户,并在选择时将用户带回您的应用。
为了简化您的工作,我们构建了 下载器库,它通过许可服务请求扩展文件 URL,下载扩展文件,执行上面列出的所有任务,甚至允许您的活动暂停和恢复下载。通过将下载器库和一些代码挂钩添加到您的应用中,几乎所有下载扩展文件的工作都已为您编码。因此,为了以最少的精力为您提供最佳的用户体验,我们建议您使用下载器库下载扩展文件。以下各节介绍如何将库集成到您的应用中。
如果您更愿意开发自己的解决方案来使用 Google Play URL 下载扩展文件,则必须遵循 应用许可 文档执行许可请求,然后从响应额外信息中检索扩展文件名、大小和 URL。您应该使用 APKExpansionPolicy
类(包含在许可验证库中)作为您的许可策略,该策略从许可服务中捕获扩展文件名、大小和 URL。
关于下载器库
要将 APK 扩展文件与您的应用一起使用,并以最少的精力为您提供最佳的用户体验,我们建议您使用 Google Play APK 扩展库包中包含的下载器库。此库在后台服务中下载扩展文件,显示带有下载状态的用户通知,处理网络连接丢失,并在可能的情况下恢复下载等等。
要使用下载器库实现扩展文件下载,您需要执行的操作是
- 扩展一个特殊的
Service
子类和BroadcastReceiver
子类,每个子类只需要您编写几行代码。 - 在您的主活动中添加一些逻辑,以检查扩展文件是否已下载,如果未下载,则调用下载过程并显示进度 UI。
- 在您的主活动中实现一个回调接口,其中包含一些方法,用于接收有关下载进度的更新。
以下各节介绍如何使用下载器库设置您的应用。
准备使用下载器库
要使用下载器库,您需要从 SDK 管理器下载两个包并将相应的库添加到您的应用中。
首先,打开 Android SDK 管理器(工具 > SDK 管理器),然后在“外观和行为 > 系统设置 > Android SDK”下,选择“SDK 工具”选项卡以选择并下载
- Google Play 许可库包
- Google Play APK 扩展库包
为许可验证库和下载器库创建一个新的库模块。对于每个库
- 选择文件 > 新建 > 新建模块。
- 在“创建新模块”窗口中,选择Android 库,然后选择下一步。
- 指定一个应用/库名称,例如“Google Play 许可库”和“Google Play 下载器库”,选择最低 SDK 级别,然后选择完成。
- 选择文件 > 项目结构。
- 选择“属性”选项卡,并在“库存储库”中,输入来自
<sdk>/extras/google/
目录的库(对于许可验证库为play_licensing/
,对于下载器库为play_apk_expansion/downloader_library/
)。 - 选择确定以创建新模块。
注意:下载器库依赖于许可验证库。请确保将许可验证库添加到下载器库的项目属性中。
或者,从命令行更新您的项目以包含库
- 将目录更改为
<sdk>/tools/
目录。 - 使用
--library
选项执行android update project
以将 LVL 和下载器库都添加到您的项目中。例如android update project --path ~/Android/MyApp \ --library ~/android_sdk/extras/google/market_licensing \ --library ~/android_sdk/extras/google/market_apk_expansion/downloader_library
将许可验证库和下载器库都添加到您的应用后,您就可以快速集成从 Google Play 下载扩展文件的功能。您为扩展文件选择的格式以及您如何从共享存储中读取它们是您应该根据您的应用需求考虑的单独实现。
提示:Apk 扩展包包含一个示例应用,演示如何在应用中使用下载器库。该示例使用 Apk 扩展包中提供的名为 APK 扩展 Zip 库的第三库。如果您计划对扩展文件使用 ZIP 文件,我们建议您也向您的应用添加 APK 扩展 Zip 库。有关更多信息,请参阅下面有关 使用 APK 扩展 Zip 库 的部分。
声明用户权限
为了下载扩展文件,下载器库需要您必须在应用的清单文件中声明的几个权限。它们是
<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>
注意:默认情况下,下载器库需要 API 级别 4,但 APK 扩展 Zip 库需要 API 级别 5。
实现下载器服务
为了在后台执行下载,下载器库提供了自己的 Service
子类,称为 DownloaderService
,您应该扩展此子类。除了为您下载扩展文件之外,DownloaderService
还会
- 注册一个
BroadcastReceiver
,该接收器侦听设备网络连接的变化(CONNECTIVITY_ACTION
广播),以便在必要时暂停下载(例如由于连接丢失)并在可能时恢复下载(连接已获取)。 - 安排一个
RTC_WAKEUP
闹钟以重试下载,以防服务被终止。 - 构建一个自定义
Notification
,显示下载进度以及任何错误或状态更改。 - 允许您的应用手动暂停和恢复下载。
- 在下载扩展文件之前,验证共享存储是否已挂载并可用,文件是否不存在以及是否有足够的存储空间。然后在任何一个条件不满足时通知用户。
您只需在您的应用中创建一个扩展 DownloaderService
类的类,并覆盖三个方法以提供特定的应用详细信息
getPublicKey()
- 这必须返回一个字符串,该字符串是发布者帐户的 Base64 编码 RSA 公钥,可从 Play Console 的个人资料页面获得(请参阅 设置许可)。
getSALT()
- 这必须返回一个随机字节数组,许可
Policy
使用该数组创建Obfuscator
。salt 确保保存许可数据的混淆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
的值更新为属于您发布者帐户的公钥。您可以在开发者控制台中,您的个人资料信息下找到此密钥。即使在测试下载时,这步也是必要的。
请记住在您的清单文件中声明此服务
<app ...> <service android:name=".SampleDownloaderService" /> ... </app>
实现 Alarm Receiver
为了监控文件下载的进度并在必要时重新启动下载,DownloaderService
会调度一个 RTC_WAKEUP
闹钟,该闹钟将一个 Intent
传递到您应用中的一个 BroadcastReceiver
。您必须定义 BroadcastReceiver
以调用 Downloader 库中的 API,该 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()
方法中返回名称的类(请参阅上一节)。
请记住在您的清单文件中声明此 Receiver
<app ...> <receiver android:name=".SampleAlarmReceiver" /> ... </app>
启动下载
您应用中的主 Activity(由您的启动器图标启动的 Activity)负责验证扩展文件是否已存在于设备上,并在不存在时启动下载。
使用 Downloader 库启动下载需要以下步骤
- 检查文件是否已下载。
Downloader 库在
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,则应用必须开始下载。
- 通过调用静态方法
DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class<?> serviceClass)
启动下载。此方法采用以下参数
context
:您应用的Context
。notificationClient
:一个PendingIntent
用于启动您的主 Activity。这在DownloaderService
创建的显示下载进度的Notification
中使用。当用户选择通知时,系统会调用您在此处提供的PendingIntent
,并应打开显示下载进度的 Activity(通常是启动下载的相同 Activity)。serviceClass
:您对DownloaderService
的实现的Class
对象,需要启动服务并在必要时开始下载。
此方法返回一个整数,指示下载是否需要。可能的值为
NO_DOWNLOAD_REQUIRED
:如果文件已存在或下载已在进行中,则返回此值。LVL_CHECK_REQUIRED
:如果需要进行许可证验证才能获取扩展文件 URL,则返回此值。DOWNLOAD_REQUIRED
:如果扩展文件 URL 已知,但尚未下载,则返回此值。
对于
LVL_CHECK_REQUIRED
和DOWNLOAD_REQUIRED
,其行为基本相同,您通常无需关注它们。在调用startDownloadServiceIfRequired()
的主 Activity 中,您只需检查响应是否为NO_DOWNLOAD_REQUIRED
。如果响应为NO_DOWNLOAD_REQUIRED
以外的任何值,则 Downloader 库将开始下载,您应该更新 Activity UI 以显示下载进度(请参阅下一步)。如果响应为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 }
- 当
startDownloadServiceIfRequired()
方法返回NO_DOWNLOAD_REQUIRED
以外的任何值时,通过调用DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class<?> downloaderService)
创建IStub
的实例。IStub
在您的 Activity 和下载程序服务之间提供绑定,以便您的 Activity 接收有关下载进度的回调。为了通过调用
CreateStub()
实例化您的IStub
,您必须向其传递IDownloaderClient
接口的实现以及您的DownloaderService
实现。下一节关于 接收下载进度 将讨论IDownloaderClient
接口,您通常应在您的Activity
类中实现此接口,以便在下载状态更改时更新 Activity UI。我们建议您在 Activity 的
onCreate()
方法中调用CreateStub()
来实例化您的IStub
,在startDownloadServiceIfRequired()
启动下载之后。例如,在前面
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 库的 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 库捆绑的字符串之一的资源 ID。例如,字符串“由于您处于漫游状态,下载已暂停”对应于STATE_PAUSED_ROAMING
。 onDownloadProgress(DownloadProgressInfo progress)
- 下载服务会调用此方法来传递一个
DownloadProgressInfo
对象,该对象描述了有关下载进度的各种信息,包括估计剩余时间、当前速度、总体进度和总进度,以便您可以更新下载进度 UI。
提示:有关更新下载进度 UI 的这些回调的示例,请参阅 Apk Expansion 包中提供的示例应用中的 SampleDownloaderActivity
。
IDownloaderService
接口的一些您可能会发现有用的公共方法为
requestPauseDownload()
- 暂停下载。
requestContinueDownload()
- 恢复已暂停的下载。
setDownloadFlags(int flags)
- 设置用户对可以下载文件的网络类型的偏好设置。当前实现支持一个标志
FLAGS_DOWNLOAD_OVER_CELLULAR
,但您可以添加其他标志。默认情况下,此标志未启用,因此用户必须处于 Wi-Fi 状态才能下载扩展文件。您可能希望提供一个用户偏好设置以启用通过蜂窝网络下载。在这种情况下,您可以调用Kotlin
remoteService = DownloaderServiceMarshaller.CreateProxy(m).apply { ... setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR) }
Java
remoteService .setDownloadFlags(IDownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR);
使用 APKExpansionPolicy
如果您决定构建自己的下载程序服务而不是使用 Google Play Downloader 库,您仍然应该使用许可证验证库中提供的 APKExpansionPolicy
。 APKExpansionPolicy
类与 ServerManagedPolicy
(在 Google Play 许可证验证库中可用)几乎相同,但包含对 APK 扩展文件响应额外内容的其他处理。
注意:如果您确实使用了上一节中讨论的 Downloader 库,则该库将执行与 APKExpansionPolicy
的所有交互,因此您无需直接使用此类。
此类包含一些方法来帮助您获取有关可用扩展文件的信息
getExpansionURLCount()
getExpansionURL(int index)
getExpansionFileName(int index)
getExpansionFileSize(int index)
有关在不使用 Downloader 库 时如何使用 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
和所需的扩展文件版本来调用此方法。
您可以通过多种方式确定扩展文件版本号。一种简单的方法是在下载开始时将版本保存到SharedPreferences
文件中,方法是使用APKExpansionPolicy
类的getExpansionFileName(int index)
方法查询扩展文件名。然后,当您要访问扩展文件时,可以通过读取SharedPreferences
文件来获取版本代码。
有关从共享存储读取的更多信息,请参阅数据存储文档。
使用 APK 扩展 Zip 库
Google Play APK 扩展包包含一个名为 APK 扩展 Zip 库的库(位于<sdk>/extras/google/google_market_apk_expansion/zip_file/
)。这是一个可选库,可帮助您在扩展文件保存为 ZIP 文件时读取这些文件。使用此库,您可以轻松地将 ZIP 扩展文件中的资源作为虚拟文件系统读取。
APK 扩展 Zip 库包含以下类和 API
APKExpansionSupport
- 提供一些用于访问扩展文件名和 ZIP 文件的方法
getAPKExpansionFiles()
- 与上面显示的相同方法,返回两个扩展文件的完整文件路径。
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
- 返回一个表示主文件和补丁文件之和的
ZipResourceFile
。也就是说,如果您同时指定mainVersion
和patchVersion
,则此方法将返回一个ZipResourceFile
,该文件提供对所有数据的读取访问权限,并将补丁文件的数据合并到主文件之上。
ZipResourceFile
- 表示共享存储上的 ZIP 文件,并执行所有工作以基于您的 ZIP 文件提供虚拟文件系统。您可以使用
APKExpansionSupport.getAPKExpansionZipFile()
或通过将扩展文件的路径传递给ZipResourceFile
来获取实例。此类包含各种有用的方法,但通常您不需要访问其中的大多数方法。几个重要的方法是getInputStream(String assetPath)
- 提供一个
InputStream
来读取 ZIP 文件中的文件。assetPath
必须是相对于 ZIP 文件内容根目录的目标文件的路径。 getAssetFileDescriptor(String assetPath)
- 提供一个
AssetFileDescriptor
用于 ZIP 文件中的文件。assetPath
必须是相对于 ZIP 文件内容根目录的目标文件的路径。这对于某些需要AssetFileDescriptor
的 Android API(例如某些MediaPlayer
API)很有用。
APEZProvider
- 大多数应用不需要使用此类。此类定义了一个
ContentProvider
,该提供程序通过内容提供程序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 扩展 Zip 库时,从 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 之前,您应该测试应用读取共享存储中文件的能力。您只需将文件添加到设备共享存储上的相应位置并启动应用即可。
- 在您的设备上,在共享存储上创建 Google Play 将保存文件的相应目录。
例如,如果您的包名为
com.example.android
,则需要在共享存储空间上创建目录Android/obb/com.example.android/
。(将测试设备连接到计算机以挂载共享存储并手动创建此目录。) - 手动将扩展文件添加到该目录。确保您将文件重命名为与 Google Play 将使用的文件名格式匹配。
例如,无论文件类型如何,
com.example.android
应用的主扩展文件应为main.0300110.com.example.android.obb
。版本代码可以是您想要的任何值。请记住- 主扩展文件始终以
main
开头,补丁文件以patch
开头。 - 包名始终与 Google Play 上附加文件的 APK 的包名匹配。
- 主扩展文件始终以
- 现在扩展文件已在设备上,您可以安装并运行您的应用来测试您的扩展文件。
以下是一些关于处理扩展文件的提醒
- 请勿删除或重命名
.obb
扩展文件(即使您将数据解压缩到其他位置)。这样做会导致 Google Play(或您的应用本身)重复下载扩展文件。 - 不要将其他数据保存到您的
obb/
目录中。如果您必须解压某些数据,请将其保存到由getExternalFilesDir()
指定的位置。
测试文件下载
因为您的应用有时必须在首次打开时手动下载扩展文件,所以您必须测试此过程以确保您的应用能够成功查询 URL、下载文件并将它们保存到设备上。
要测试应用中手动下载过程的实现,您可以将其发布到内部测试轨道,以便仅供授权测试人员使用。如果一切按预期工作,您的应用应在主活动启动后立即开始下载扩展文件。
注意:以前您可以通过上传未发布的“草稿”版本来测试应用。此功能不再受支持。相反,您必须将其发布到内部、封闭或公开测试轨道。有关更多信息,请参阅草稿应用不再受支持。
更新您的应用
使用 Google Play 上的扩展文件的一大优势在于能够更新您的应用,而无需重新下载所有原始资源。由于 Google Play 允许您为每个 APK 提供两个扩展文件,因此您可以将第二个文件用作提供更新和新资源的“补丁”。这样做避免了重新下载主扩展文件(这对于用户来说可能很大且成本很高)的需要。
补丁扩展文件在技术上与主扩展文件相同,Android 系统和 Google Play 都不会在您的主扩展文件和补丁扩展文件之间执行实际的修补。您的应用代码必须自行执行任何必要的修补。
如果您使用 ZIP 文件作为扩展文件,则 Apk 扩展包中包含的APK 扩展 ZIP 库包含将您的补丁文件与主扩展文件合并的功能。
注意:即使您只需要更改补丁扩展文件,也必须更新 APK 才能让 Google Play 执行更新。如果您不需要更改应用中的代码,则只需更新清单中的versionCode
。
只要您不更改 Play Console 中与 APK 关联的主扩展文件,之前安装过您应用的用户就不会下载主扩展文件。现有用户仅接收更新的 APK 和新的补丁扩展文件(保留以前的主扩展文件)。
以下是一些关于扩展文件更新需要注意的问题
- 您的应用一次只能有两个扩展文件。一个主扩展文件和一个补丁扩展文件。在更新文件期间,Google Play 会删除先前版本(因此,当执行手动更新时,您的应用也必须删除)。
- 添加补丁扩展文件时,Android 系统不会实际修补您的应用或主扩展文件。您必须设计您的应用以支持补丁数据。但是,Apk 扩展包包含一个用于将 ZIP 文件用作扩展文件的库,该库将补丁文件中的数据合并到主扩展文件中,以便您可以轻松读取所有扩展文件数据。