二十九、处理平台变更

自最初发布以来,Android 一直在快速发展,并将在未来几年继续发展。或许,随着时间的推移,变化的速度会下降一些。然而,就目前而言,你应该假设每 6 到 12 个月就会有重要的 Android 发布,并且可能的 Android 硬件阵容会不断变化。因此,虽然现在 Android 的重点是手机和平板电脑,但很快你就会看到 Android 上网本、Android 电视、Android 媒体播放器等等。

许多这些变化对您现有的代码几乎没有影响。但是,有些应用至少需要新一轮的测试,并且可能会根据测试结果对这些应用进行更改。

本章涵盖了随着 Android 的发展,未来可能会给你带来麻烦的几个问题,并提供了一些处理这些问题的建议。

让你兴奋的事情

Android 改变,不仅仅是在谷歌引入的方面,还包括设备制造商如何为他们自己的硬件调整 Android。如果您没有做好准备,本节将指出这些变化会影响您的应用的几个地方。

查看层次结构

Android 不是为处理任意复杂的视图层次而设计的。在这里,视图层次结构意味着容器包含容器,容器包含容器包含小部件。在后面的章节中描述的hierarchyviewer程序很好地描述了这样的视图层次结构。

Android 总是限制视图层次的深度。然而,在 Android 1.5 中,这个限制被降低了,所以一些在 Android 1.1 上运行良好的应用在新的 Android 中将会崩溃。当然,这对于开发人员来说是令人沮丧的,他们从来没有意识到视图层次深度的问题,然后被这种变化所困扰。

从中吸取的教训如下:

  • 保持你的视图层次浅。一旦进入两位数深度,就越有可能耗尽堆栈空间。
  • 如果您遇到一个StackOverflowException,并且堆栈跟踪看起来像是在绘制小部件的中间,那么您的视图层次结构可能太复杂了。

改变资源

Android 核心团队可能会随着 Android 升级而改变资源,这些可能会对您的应用产生意想不到的影响。例如,在 Android 1.5 中,Android 团队改变了股票Button背景,以允许更小的按钮。然而,隐式依赖于前一个更大的最小尺寸的应用最终会崩溃,需要一些 UI 调整。

类似地,应用可以重用 Android 内部的公共资源,比如图标。虽然这样做可以节省一些存储空间,但是这些资源中有许多是公共的,不被视为 SDK 的一部分。例如,硬件制造商可能会更改图标以适应一些可选的 UI 外观和感觉。依赖现有的总是看起来像他们做的有点危险。将这些资源从 Android 开源项目复制到您自己的代码库中会更好。

处理 API 变更

Android 核心团队在保持 API 稳定方面做得很好,当他们改变 API 时,支持一个弃用模型。在 Android 中,当一个特性被弃用时,这并不意味着这个特性正在消失,只是不鼓励继续使用它。当然,每次新的 Android 更新都会发布新的 API。通过 API 差异报告,API 的变更在每个版本中都有很好的文档记录。

不幸的是,Android Market——Android 应用的主要发布渠道——只允许你为每个应用上传一个 Android 包(APK)文件。因此,你需要一个 APK 文件来处理尽可能多的 Android 版本。很多时候,你的代码会“正常工作”,不需要修改。但是,其他时候,您将需要进行调整,特别是如果您希望在新版本上支持新的 API,同时又不破坏旧版本。让我们研究一些处理这些情况的技术。

最小、最大、目标和构建版本

Android 不遗余力地帮助你处理这样一个事实,即在任何时间点,市场上会有许多 Android 操作系统版本。不幸的是,Android 提供的工具给了我们一组有些混乱的重叠概念,比如目标和 SDK 版本。本节试图澄清这些概念。

目标与 SDK 版本与操作系统版本

目标的概念是在本书开头介绍的。定义 avd 时使用目标来确定这些 avd 支持什么类型的设备。创建新项目时也会使用目标,主要是为了确定将使用哪个版本的 SDK 构建工具来构建您的项目。

目标将 API 级别与该目标是否包括谷歌 API(例如,谷歌地图支持)的指示符相结合。

API 级别是表示 Android API 版本的整数。每个对 Android API 进行修改的 Android OS 版本都会触发一个新的 API 级别。以下是 API 级别:

  • 3:安卓 1.5r1,1.5r2,1.5r3
  • 4:安卓 1.6r1 和 1.6r2
  • 5 : Android 2.0
  • 6 : Android 2.0.1
  • 7 : Android 2.1.x
  • 8 : Android 2.2.x
  • 9:安卓 2.3,2.3.1,2.3.2
  • Android 2.3.3 和 2.3.4
  • 11 : Android 3.0.x
  • 12 : Android 3.1.x
  • 13 : Android 3.2
  • 14 : Android 4.0

谷歌维护着一个网页,根据对 Android Market 的请求,概述了目前使用的 Android 版本。

最低 SDK 版本

在您的AndroidManifest.xml文件中,您应该添加一个<uses-sdk>元素。该元素描述了您的应用如何与各种 SDK 版本相关联。

<uses-sdk>中最关键的属性是android:minSdkVersion。这表明您的应用支持的最低 API 级别。运行与较低 API 级别相关联的 Android 操作系统版本的设备将无法安装您的应用。如果您选择通过该分销商发布,您的应用甚至可能不会出现在 Android Market 列表中的那些设备上。

如果你跳过这个属性,Android 假设你的应用可以在所有的 Android API 版本上工作。那可能是真的,但是如果你没有测试过,就这样假设是相当危险的。因此,将android:minSdkVersion设置为您正在测试并且愿意支持的最低级别。

目标 SDK 版本

另一个<uses-sdk>属性是android:targetSdkVersion。这代表了您主要开发的 Android API 的版本。任何运行新版操作系统的 Android 设备都可以选择应用一些兼容性设置,这将有助于像你这样针对旧 API 的应用在新版操作系统上运行。

大多数情况下,您应该将它设置为当前的 Android API 版本,即您发布应用时的版本。

特别是对于冰激凌三明治,您需要指定一个目标1415来获得新的外观和感觉。

最高 SDK 版本

第三个<uses-sdk>属性是android:maxSdkVersion。任何运行比该 API 等级所指示的更新的 Android 操作系统的 Android 设备将被禁止运行您的应用。

从好的方面来说,这确保了您的应用不会在您没有测试过的 API 级别上使用,特别是如果您将它设置为截至您发布之日的当前 Android API 版本。

然而,请记住,您的应用将被过滤出这些新设备的 Android 市场。随着时间的推移,如果您不发布具有更高 SDK 最高版本的更新,这将限制您的应用的范围。

Android 核心团队建议您不要使用这个选项,而是依靠 Android 固有的向后兼容性——特别是利用您的android:targetSdkVersion值——来允许您的应用继续在新的 Android OS 版本上运行。

检测版本

如果您只是想基于版本在代码中采用不同的分支,最简单的方法就是检查android.os.Build.VERSION.SDK_INT。这个公共静态整数值将反映您在创建 avd 和在清单中指定 API 级别时使用的相同 API 级别。因此,你可以将这个值与android.os.Build.VERSION_CODES.DONUT进行比较,看看你运行的是 Android 1.6 还是更新版本。

包装 API

只要您尝试使用的 API 存在于您支持的所有 Android 版本中,只需分支就足够了。当 API 发生变化时,事情就会变得麻烦,比如当方法有新参数、新方法甚至新类时。您需要不管 Android 版本如何都可以工作的代码,同时还允许您利用新的可用 API。

挑战在于,如果你试图加载虚拟机代码,而这些代码引用了设备运行的 Android 版本中不存在的类、方法等,那么你的应用将会因VerifyError而崩溃。你需要针对包含你正在尝试使用的最新 API 的 Android 版本编译——你不能将代码加载到一个旧的 Android 设备上。

请注意,这里的关键词是“加载该代码”您不一定会因为应用中存在一个使用比现有 API 更新的类而遇到问题。只有当你执行的代码触发 Android 将那个类加载到你的运行进程中,你才会遇到VerifyError

记住这一点,有三个主要的技巧来处理这种情况,在下面的章节中概述。

检测类别

也许你需要做的就是禁用你的应用中的一些功能,这些功能会导致在给定的设备上不可能发生的事情。例如,假设您有一个使用片段特性的活动。您无法在 3.0 之前的设备上成功启动该活动。停止该活动可能只是禁用一个菜单选项或Button之类的事情。

要查看某个类(比方说,ListFragment)是否对您可用,可以调用Class.forName()。这将返回一个代表所请求的类的Class对象,或者抛出一个Exception,如果它不可用的话。您可以使用异常处理程序来禁用 UI 路径,这将导致您的应用尝试启动使用不可用类的活动。

反思

如果您需要对旧版本 Android 上不存在的类进行有限的访问,您可以使用一点反射。

例如,在关于旋转的章节中,我们使用了一系列允许用户选择联系人的示例应用。这依赖于一个ACTION_PICKIntent,使用特定的Uri作为联系人的内容提供者。在这些示例中,我们特别使用了ContactsContract,这是 Android 2.0 及更高版本中提供的修订版联系人 API。这意味着这些项目无法在旧版本的 Android 上运行。

然而,我们真正需要的是这个神奇的Uri值。如果我们能设计出一种方法,在不引起问题的情况下,为旧版本的 Android 获得正确的Uri,以及为新版本的 Android 获得正确的Uri,我们就能更好地向后兼容。

幸运的是,通过一些反射,这很容易做到:

`static {   intsdk=new Integer(Build.VERSION.SDK).intValue();

if (sdk>=5) {     try {       Class clazz=Class.forName("android.provider.ContactsContract$Contacts");

CONTENT_URI=(Uri)clazz.getField("CONTENT_URI").get(clazz);     }     catch (Throwable t) {       Log.e("PickDemo", "Exception when determining CONTENT_URI", t);     }   }   else {     CONTENT_URI=android.provider.Contacts.People.CONTENT_URI;   } }`

在这里,我们通过查看Build.VERSION.SDK来检查设备的 API 级别(我们可以使用Build.VERSION.SDK_INT,但这是在 Android 1.6 之前添加的——这里显示的代码也适用于 Android 1.5)。如果我们在 Android 2.0 (API 级别5)或更高,我们使用Class.forName()来获得新的ContactsContract.Contacts类,然后使用反射来获得该类的CONTENT_URI静态数据成员。如果我们在旧版本的 Android 上,我们简单地使用旧的Contacts.People类发布的Uri

因为我们没有在代码中直接引用ContactsContract.Contacts,所以我们可以安全地执行它,即使是在旧版本的 Android 上。

条件类加载

反思是有用的,但对任何复杂的事物来说都是痛苦的。而且,它比直接调用代码要慢。

因此,最强大的技术是简单地组织您的代码,使您拥有使用较新 API 的常规类,但是您不在较旧的设备上加载这些类。我们将在本书的后面部分研究这种技术。

图案为冰淇淋三明治和蜂窝

随着 Honeycomb (Android 3.0)和现在的冰激凌三明治(Android 4.0)的出现,支持多个 Android 版本现在是一个重大挑战。在许多情况下,支持不同 UI 所需的 UI 更改需要您采取措施来确保您的应用仍然可以在旧版本的 Android 上成功运行。本节概述了处理向后兼容性领域的一些模式。

操作栏

正如在第 27 章中提到的,动作栏的许多基本特性将以向后兼容的方式工作。例如,指示选项菜单项可以显示在动作栏中只需要菜单资源 XML 中的一个属性,这个属性在旧版本的 Android 中将被忽略。支持 Honeycomb 的设备会将该项目放在操作栏中,而运行早期 Android 版本的设备不会。

然而,并不是动作栏的所有功能都是向后兼容的。在第 27 章的中的Menus/ActionBar示例应用中,我们添加了一个自定义的View到动作栏,允许人们在不处理菜单和对话框的情况下添加单词到我们的列表中。然而,这需要一些只在 API 级别11 (Android 3.0)和更高级别的代码。更高级的动作栏功能——超出了本书的范围——也有类似的需求。

您需要安排只在运行 API 级别11或更高的设备上使用那些动作栏方法。本章前面概述的条件类加载就是这样一种技术,也是在Menus/ActionBarBC示例应用中使用的技术。让我们来看看这是如何工作的。

检查 API 级别

我们最初的onCreateOptionsMenu()是这样的:

`@Override public boolean onCreateOptionsMenu(Menu menu) {   new MenuInflater(this).inflate(R.menu.option, menu);

EditText add=(EditText)menu                          .findItem(R.id.add)                          .getActionView()                          .findViewById(R.id.title);

add.setOnEditorActionListener(onSearch);

return(super.onCreateOptionsMenu(menu)); }`

这很好,但是它将只在 API 级别11和更高的级别上起作用,因为getActionView()只从那个 API 级别开始存在。因此,在没有获得VerifyError的情况下,我们无法在旧版本的 Android 上运行这段代码,甚至无法加载这个类。

新版本的onCreateOptionsMenu()隐藏了违规代码,并检查 API 级别:

`@Override public boolean onCreateOptionsMenu(Menu menu) {   new MenuInflater(this).inflate(R.menu.option, menu);

EditText add=null;

if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB) {     View v=ICSHCHelper.getAddActionView(menu);

if (v!=null) {       add=(EditText)v.findViewById(R.id.title);     }   }

if (add!=null) {     add.setOnEditorActionListener(onSearch);   }

return(super.onCreateOptionsMenu(menu)); }`

我们只隐藏检索理论上放在动作栏中的View的代码。如果我们在一个旧版本的 Android 上,HONEYCOMB检查将失败,我们将以一个nullView结束,所以我们跳过将OnEditorActionListener添加到那个View内的EditText

这还有另一个好处:如果 Android 设备运行 API 级别11或更高,但没有空间容纳我们的自定义 APIView,它也能工作。Android 平板电脑将有一个动作栏和足够的空间,但未来支持蜂窝的手机可能会有一个动作栏,但缺乏足够的空间。在这种情况下,手机会保留添加选项菜单项,我们仍然会以一个nullView结束。这段代码处理这种情况;原始代码没有。

隔离冰淇淋三明治/蜂巢代码

我们的 Honeycomb 特定代码保存在一个单独的ICSHCHelper类中(ICS 用于冰激凌三明治,HC 用于 Honeycomb),该类仅用于 API 级别11(或更高)的设备:

`packagecom.commonsware.android.inflation;

importandroid.view.Menu; importandroid.view.View;

classICSHCHelper {   static View getAddActionView(Menu menu) {    return(menu.findItem(R.id.add).getActionView());   } }`

ICSHCHelper有一个单独的getAddActionView()静态方法,如果有添加动作栏条目的话,这个静态方法会找到它的View

因为我们不试图在这个类上执行任何代码,除了在HONEYCOMB检查中,在旧版本的 Android 上有这个类是安全的。Menus/ActionBarHC应用可以在 Android 1.6 及更高版本上运行。

编写平板电脑专用应用

理想情况下,您的 Android 应用可以在所有形式的设备上运行:手机、平板电脑等等。然而,你可能想创建一个不能在手机上使用的应用。理想情况下,你应该让你的应用远离小屏幕设备,这样用户才不会失望。

要做到这一点,你可以利用这样一个事实,即 Android 将扩大应用,但不会缩小应用。换句话说,如果你指定你的应用不支持一些更大的屏幕尺寸(例如,android:xlargeScreens="false"出现在你的AndroidManifest.xml文件的<supports-screens>元素中),Android 仍然允许你的应用在这样的屏幕上运行,并采取措施帮助你的应用在额外的屏幕空间上运行。但是,如果您指定您的应用不支持一些较小的屏幕尺寸(例如,android:smallScreens="false"出现在您的<supports-screens>元素中),Android 将不会运行您的应用,您将被过滤出此类设备的 Android 市场。

因此,如果您的应用只能在大屏幕设备上运行良好,请使用如下的<supports-screens>元素:

<supports-screens android:xlargeScreens="true"                  android:largeScreens="true"                  android:normalScreens="false"                  android:smallScreens="false"                  android:anyDensity="true"/>