为不同 GL 纹理创建多个 APK

如果您将应用发布到 Google Play,则应构建并上传 Android App Bundle。这样做时,Google Play 会自动为每个用户的设备配置生成并提供经过优化的 APK,因此他们只需下载运行您的应用所需的代码和资源。如果您不是发布到 Google Play,则发布多个 APK 很有用,但您必须自行构建、签名和管理每个 APK。

在开发 Android 应用程序以利用 Google Play 上的多个 APK 功能时,从一开始就采取一些良好的实践非常重要,这样可以防止在开发过程后期出现不必要的麻烦。本课将向您展示如何为您的应用创建多个 APK,每个 APK 支持不同子集的 OpenGL 纹理格式。您还将获得使维护多个 APK 代码库尽可能简单所需的工具。

确认您需要多个 APK

当试图创建一个适用于所有 Android 设备的应用程序时,您自然希望您的应用程序在每台设备上都能呈现最佳效果,尽管它们并不都支持相同的一组 GL 纹理。起初,多个 APK 支持似乎是最佳解决方案,但事实往往并非如此。多个 APK 开发者指南中的改用单个 APK 部分包含了一些关于如何通过单个 APK 实现此目的的有用信息,包括如何在运行时检测支持的纹理格式。根据您的情况,将所有格式与应用程序捆绑在一起,并在运行时简单地选择使用哪一种,可能会更容易。

如果您能做到,将应用限制为单个 APK 有几个优点,包括:

  • 发布和测试更简单
  • 只有一个代码库需要维护
  • 您的应用可以适应设备配置的变化
  • 跨设备的应用恢复功能可直接使用
  • 您不必担心市场偏好、从一个 APK “升级”到下一个 APK 时的行为,或者哪个 APK 对应哪类设备

本课程的其余部分假设您已经研究过该主题,认真吸收了链接资源中的材料,并确定多个 APK 是适合您应用的最佳途径。

规划您的需求

Android 开发者指南在 supports-gl-texture 页面上提供了一些常见支持纹理的便捷参考。该页面还包含有关哪些手机(或手机系列)支持特定纹理格式的提示。请注意,通常建议您的其中一个 APK 支持 ETC1,因为所有支持 OpenGL ES 2.0 规范的 Android 设备都支持该纹理格式。

由于大多数 Android 设备支持多种纹理格式,因此您需要建立优先顺序。创建一个包含您的应用程序将要支持的所有格式的图表。最左侧的单元格优先级最低(这通常是 ETC1,在性能和兼容性方面是一个非常稳妥的默认选择)。然后给图表着色,使每个单元格代表一个 APK。

ETC1 ATI PowerVR

给图表着色的意义不仅在于让本指南不那么单调——它还可以简化团队内部的沟通。现在,您可以简单地将每个 APK 称为“蓝色”、“绿色”或“红色”,而不是“那个支持 ETC1 纹理格式的 APK”等。

将所有公共代码和资源放入库项目中

无论您是在修改现有的 Android 应用还是从头开始创建一个新应用,这都是您应该对代码库做的第一件事,也是最重要的一件事。进入库项目的所有内容只需要更新一次(例如语言本地化字符串、颜色主题、共享代码中的错误修复),这可以缩短您的开发时间,并减少本可轻易避免的错误发生的可能性。

注意:虽然关于如何创建和包含库项目的实现细节超出了本课程的范围,但您可以阅读创建 Android 库来快速上手。

如果您正在转换现有的应用以使用多个 APK 支持,请仔细检查代码库中的每一个本地化字符串文件、值列表、主题颜色、菜单图标和布局(这些在各 APK 间不会改变),并将它们全部放入库项目中。不会有太大变化的代码也应该放入库项目中。您可能会发现自己为了从一个 APK 到另一个 APK 添加一两个方法而扩展了这些类。

另一方面,如果您是从零开始创建应用,请尽可能先在库项目中编写代码,只有在必要时才将其向下移动到单个 APK 中。从长远来看,这比将其添加到一个 APK,然后再添加到另一个,几个月后再试图弄清楚这个块是否可以在不破坏任何东西的情况下移动到库部分要容易管理得多。

创建新的 APK 项目

您要发布的每个 APK 都应有一个单独的 Android 项目。为了方便组织,请将库项目和所有相关的 APK 项目放在同一个父文件夹下。还要记住,每个 APK 都必须具有相同的包名,尽管它们不一定需要与库共享包名。如果您按照前面描述的方案拥有 3 个 APK,那么您的根目录可能如下所示

alexlucas:~/code/multi-apks-root$ ls
foo-blue
foo-green
foo-lib
foo-red

项目创建完成后,将库项目作为引用添加到每个 APK 项目中。如果可能,请在库项目中定义您的启动 Activity,并在 APK 项目中扩展该 Activity。在库项目中定义启动 Activity 让您有机会将所有应用初始化放在一个地方,这样每个单独的 APK 就不必重新实现“通用”任务,例如初始化分析、运行许可检查以及任何其他在各 APK 间变化不大的初始化过程。

调整清单文件 (Manifest)

当用户通过 Google Play 下载使用多个 APK 的应用程序时,会根据一些简单的规则选择要使用的正确 APK

  • 清单文件必须显示该特定 APK 是合格的
  • 在符合条件的 APK 中,版本号最高的胜出
  • 如果设备支持您 APK 中列出的任何纹理格式,则该设备被视为符合条件

关于 GL 纹理,最后一条规则非常重要。这意味着,例如,您应该非常小心地在同一个应用程序中使用不同的 GL 格式。如果您 99% 的时间都在使用 PowerVR,但例如在启动画面上使用了 ETC1……那么您的清单文件将不得不声明支持这两种格式。仅支持 ETC1 的设备会被视为兼容,您的应用将会下载,用户会看到一些令人激动的崩溃消息。常见的情况是,如果您专门为了根据 GL 纹理支持定位不同设备而使用多个 APK,那么每个 APK 应该只对应一种纹理格式。

这实际上使得纹理支持与其他两个多 APK 维度(API 级别和屏幕尺寸)略有不同。任何给定的设备只有一个 API 级别和一个屏幕尺寸,由 APK 来支持其中的一个范围。对于纹理,APK 通常只支持一种纹理,而设备支持多种。在某些设备支持多个 APK 的情况下,往往会出现重叠,但解决方案是相同的:版本代码 (version codes)。

以几个设备为例,看看前面定义的 APK 中有多少适合每台设备。

FooPhone Nexus S Evo
ETC1 ETC1 ETC1
PowerVR ATI TC

假设在可用时,PowerVR 和 ATI 格式的优先级都高于 ETC1,那么根据“版本号最高者胜”的规则,如果我们设置每个 APK 中的 versionCode 属性,使得红 ≥ 绿 ≥ 蓝,那么在支持这些格式的设备上,红色和绿色总是会优于蓝色被选中;如果有设备同时支持红色和绿色,则会选择红色。

为了让所有 APK 保持在独立的“轨道”上,拥有良好的版本代码方案非常重要。推荐的方案可以在我们开发者指南的版本代码区域找到。由于示例中的 APK 集只处理 3 个维度中的一个,因此将每个 APK 间隔 1000 并在此基础上递增就足够了。这可能看起来像

蓝色:1001, 1002, 1003, 1004...
绿色:2001, 2002, 2003, 2004...
红色:3001, 3002, 3003, 3004...

综上所述,您的 Android 清单文件可能看起来像下面这样:

蓝色

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1001" android:versionName="1.0" package="com.example.foo">
    <supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
    ...

绿色

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="2001" android:versionName="1.0" package="com.example.foo">
    <supports-gl-texture android:name="GL_AMD_compressed_ATC_texture" />
    ...

红色

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="3001" android:versionName="1.0" package="com.example.foo">
    <supports-gl-texture android:name="GL_IMG_texture_compression_pvrtc" />
    ...

检查您的发布前清单

在上传到 Google Play 之前,请仔细检查以下项目。请记住,这些内容专门与多个 APK 相关,并不代表所有上传到 Google Play 的应用的完整清单。

  • 所有 APK 必须具有相同的包名
  • 所有 APK 必须使用相同的证书进行签名
  • 仔细检查清单过滤器中是否存在冲突信息(一个仅支持 XLARGE 屏幕上的 cupcake 版本的 APK 将不会被任何人看到)。
  • 每个 APK 的清单文件在支持的屏幕、OpenGL 纹理或平台版本中至少有一个维度必须是唯一的
  • 尽量在至少一台设备上测试每个 APK。如果不行,您的开发机器上就有业内最可定制的设备模拟器之一。尽情使用吧!

在推向市场之前检查编译后的 APK 也是值得的,以确保没有任何意外可能导致您的应用在 Google Play 上被隐藏。使用 "aapt" 工具实际上非常简单。Aapt(Android 资源打包工具)是创建和打包 Android 应用的构建过程的一部分,也是检查它们的非常方便的工具。

>aapt dump badging
package: name='com.example.hello' versionCode='1' versionName='1.0'
sdkVersion:'11'
uses-permission:'android.permission.SEND_SMS'
application-label:'Hello'
application-icon-120:'res/drawable-ldpi/icon.png'
application-icon-160:'res/drawable-mdpi/icon.png'
application-icon-240:'res/drawable-hdpi/icon.png'
application: label='Hello' icon='res/drawable-mdpi/icon.png'
launchable-activity: name='com.example.hello.HelloActivity'  label='Hello' icon=''
uses-feature:'android.hardware.telephony'
uses-feature:'android.hardware.touchscreen'
main
supports-screens: 'xlarge'
supports-any-density: 'true'
locales: '--_--'
densities: '120' '160' '240'

当您检查 aapt 输出时,请务必检查 supports-screens 和 compatible-screens 是否存在冲突的值,并检查是否没有因您在清单中设置的权限而意外添加了 "uses-feature" 值。在上面的示例中,该 APK 对大多数(如果不是全部)设备都是不可见的。

为什么?通过添加所需的权限 SEND_SMS,隐式添加了 android.hardware.telephony 的功能要求。由于大多数(如果不是全部)超大屏幕设备都是没有电话硬件的平板电脑,因此 Google Play 会在这些情况下过滤掉此 APK,直到未来出现既足够大可以报告为超大屏幕尺寸又具备电话硬件的设备。

幸运的是,通过在您的清单中添加以下内容可以轻松修复此问题:

<uses-feature android:name="android.hardware.telephony" android:required="false" />

android.hardware.touchscreen 要求也会被隐式添加。如果您希望您的 APK 在非触摸屏设备的电视上可见,您应该在清单中添加以下内容:

<uses-feature android:name="android.hardware.touchscreen" android:required="false" />

完成发布前检查清单后,将您的 APK 上传到 Google Play。应用程序在浏览 Google Play 时显示可能需要一点时间,但当它显示时,请进行最后一次检查。将应用程序下载到您拥有的任何测试设备上,以确保 APK 针对的是预期的设备。恭喜,大功告成!