Android-Studio-学习手册-三-

170 阅读1小时+

Android Studio 学习手册(三)

原文:Learn Android Studio

协议:CC BY-NC-SA 4.0

九、货币 Lab:第一部分

这一章,以及下一章,将向你展示如何在构建一个名为货币的应用的环境中使用 Android Studio。货币的目的是提供一种在外币和用户本国货币之间进行转换的便捷方式。典型的用例是用户在国外旅行,需要用外币兑换货币或购买一些东西。货币汇率总是在波动,甚至可能一分钟一分钟地变化,所以用户能够获得最新的数据是很重要的。货币应用从由openexchangerates.org托管的网络服务中获取最新的汇率。

不仅货币会波动,交易所上市的活跃货币代码也会变化。例如,比特币(BTC)最近被添加到openexchangerates.org的交易货币列表中。如果我们不久前开发了货币应用并硬编码了活跃的货币代码,我们可能会错过比特币,或者更糟的是,为用户提供了从不再交易的失败国家选择货币的选项。为了解决这个问题,我们需要在填充主活动布局中使用的微调器之前,获取由openexchangerates.org发布的活动货币代码。如果你把浏览器指向 openexchangerates.org/api/currencies.json ,你就能看到 JSON 格式的现行货币代码,谢天谢地,这些代码是机器可读和人工可读的。货币应用涵盖的 Android 功能和技术包括高级布局、素材、共享偏好、风格、web 服务、并发性和对话框。

Note

我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果你的电脑上没有安装 Git,参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端),发出以下 Git 命令:git clone https://bitbucket.org/csgerber/currencies.git_Currencies

货币规格

为了解决前面描述的活动货币代码问题,我们将使用一个典型的 Android 惯例,称为闪屏。当应用 firsts 启动时(见图 9-1 ,用户会看到一个活动,其中只包含一张各种货币的照片。当这个闪屏活动可见时,后台线程获取活动货币代码。当后台线程成功终止时,闪屏活动调用主活动并将活动货币代码传递给它。然后,主活动使用活动货币代码来填充其微调器。即使假设连接性相对较差,闪屏活动也只能在大约一秒钟内可见。

A978-1-4302-6602-0_9_Fig1_HTML.jpg

图 9-1。

Currency splash screen

如果用户先前选择了本币和外币,则从用户的共享首选项中获取这些值,并将适当的值应用于微调器(参见图 9-2 )。例如,如果上次使用的货币组合是 HKD 作为外币,美元作为本币,那么下次用户启动应用时,这些相同的值将应用于微调器。在极端情况下,存储在共享首选项中的本币和/或外币值中的一个或两个不再交易。在这种情况下,受影响的微调器将只显示列出的第一个货币代码。

A978-1-4302-6602-0_9_Fig2_HTML.jpg

图 9-2。

The input currency amount

一旦主活动可见,焦点就被设置到位于主活动顶层的EditText控件上。此EditText控件仅接受数字数据,并代表要转换的外币金额。在从微调器中选择外币和本币,并输入想要转换的金额之后,用户单击 Calculate 按钮,这将触发一个后台线程来获取当前汇率。当后台线程激活时,用户看到一个对话框显示“请稍等”(见图 9-3);此对话框允许用户通过单击取消按钮来中止操作。一旦后台线程成功终止,就会从openexchangerates.org返回一个 JSON 对象,其中包含所有活动货币代码相对于美元的汇率。然后提取适当的值,并计算结果。结果格式化为五位小数,并显示在主活动底层的TextView控件中,如图 9-4 所示。

A978-1-4302-6602-0_9_Fig4_HTML.jpg

图 9-4。

Returning the result

A978-1-4302-6602-0_9_Fig3_HTML.jpg

图 9-3。

Calculating the result

货币应用的操作栏有一个带有三个选项的菜单:查看活动代码、反转代码和退出(见图 9-5 )。查看活动代码选项启动浏览器并指向 openexchangerates.org/api/currencies.json 。“反转代码”选项将微调器中显示的值转换为本币和外币。例如,如果外币是 CNY,本币是美元,则在激活反转代码菜单选项后,外币将是美元,本币将是 CNY。退出选项只是退出应用。图 9-4 和 9-5 中获得的结果(72.39314 美元和 72.44116 美元)略有不同,尽管我们使用了相同的输入值 450。这种差异的有趣原因是openexchangerates.org上的汇率每分钟都在波动,我们计算这两张截图的结果只相差几分钟。

A978-1-4302-6602-0_9_Fig5_HTML.jpg

图 9-5。

The Options menuUsing the New Project Wizard

现在,您已经了解了货币应用应该如何运行,让我们通过选择文件➤新建项目来创建一个新项目。(新项目向导及其屏幕包含在第一章中。)说出你的应用货币。我们选择使用 gerber.apress.com 作为域名,但是你可以输入你喜欢的域名。Android(和 Java)中的约定是反域名为包名。您会注意到包名是反向域名加上全部小写字母的项目。与本书中的其他实验和练习一样,您可以将该实验存储在C:\androidBook\Currencies目录中。如果您运行的是 Mac,请将货币应用放在 labs 的父目录中。单击下一步。

向导的下一步是选择一个目标 API 级别。在使你的应用兼容尽可能多的设备(通过将你的目标 API 设置得较低)和增加你作为开发者可用的特性数量(通过将你的目标 API 设置得较高)之间有一个权衡。然而,这种权衡严重偏向于将您的目标 API 级别设置得较低,因为 Google 提供了优秀的兼容性库,这些库提供了您将在以后的 API 中找到的大多数功能。在 Android 中开发商业应用的最佳实践是选择最高的目标 API 级别,该级别仍然允许您的应用在大约 100%的设备上运行。目前,目标 API 级别是 API 8。请注意,Android Studio 会自动为您导入适当的兼容性库(也称为支持库)。API 8:默认应该选择 Android 2.2 (Froyo)。如果尚未选择,请选择 API 8: Android 2.2 (Froyo),然后单击下一步。

向导的下一步是选择将为您生成的活动类型。选择空白活动,然后单击下一步。如果默认值与图 9-6 中显示的不同,则按此设置。点击 Finish,Android Studio 会为你生成一个新的项目。grad le(Android Studio 附带的构建工具,在第十三章中有介绍)将开始下载任何依赖项,比如兼容性库。请留意状态栏以查看这些进程的状态。一旦这些过程完成,您应该有一个没有错误的新项目。

A978-1-4302-6602-0_9_Fig6_HTML.jpg

图 9-6。

The Create New Project dialog box

初始化 Git 储存库

Git 已经成为 Android 应用开发不可或缺的工具,这向您展示了如何为您的 Android 项目初始化 Git 存储库。关于如何使用 Git 的更全面的教程,请参见第七章。选择 VCS ➤导入到版本控制➤创建 Git 资源库,如图 9-7 所示。当提示选择将创建新 Git 存储库的目录时,确保 Git 项目将在根项目目录中初始化,该目录名为Currencies,在本例中位于C:\androidBook\Currencies,如图 9-8 所示。如果你运行的是 Mac 电脑,在你的 Lab 父目录中选择Currencies目录。单击确定。

A978-1-4302-6602-0_9_Fig8_HTML.jpg

图 9-8。

Selecting the directory for Git initialization

A978-1-4302-6602-0_9_Fig7_HTML.jpg

图 9-7。

Initializing the Git repository

请确保将项目工具窗口切换到项目视图。视图组合框位于项目工具窗口的顶部,默认设置为 Android。如果您在 Project tool 窗口中检查这些文件,您将会注意到这些文件中的大部分都变成了棕色,这意味着它们正在被 Git 跟踪,但是没有被计划添加到存储库中。要添加它们,在项目工具窗口中选择Currencies目录并按 Ctrl+Alt+A | Cmd+Alt+A 或选择 Git ➤添加。棕色文件应该变成绿色,这意味着它们已经被添加到 Git 中的 staging 索引中,现在可以提交了。如果这个添加然后转移素材的过程看起来很乏味,请记住,您只需要这样做一次;从现在开始,Android Studio 将自动为您管理文件的添加和升级。

按 Ctrl+K | Cmd+K 调用提交修改对话框,如图 9-9 所示。作者组合框用于覆盖当前的默认提交者。您应该将作者组合框留空,Android Studio 将简单地使用您在 Git 安装期间最初设置的默认值。在“提交前”部分,取消选择所有复选框选项。在提交消息字段中键入以下消息:使用新建项目向导进行初始提交。单击提交按钮两次。通过按 Alt+9 | Cmd+9 检查“更改”工具窗口,以查看您的提交。

A978-1-4302-6602-0_9_Fig9_HTML.jpg

图 9-9。

Committing initial changes with the Commit Changes dialog box

修改主活动的布局

在本节中,我们将修改MainActivity的布局。新建项目向导为我们创建了一个名为activity_main.xml的文件。打开该文件,参考图 9-2 (之前显示)和清单 9-1 。图 9-2 中的视图是垂直排列的,所以垂直方向的LinearLayout对于我们的根ViewGroup来说似乎是个不错的选择。我们视图的宽度将填充父视图ViewGroup,所以只要有可能layout_width就应该设置为fill_parentfill_parentmatch_parent设置可以互换使用。为了在我们的布局中表达视图的高度,我们希望尽可能避免硬编码dp(与密度无关的像素)值。相反,我们将使用一个名为layout_weight的属性来指示 Android Studio 以其父视图ViewGroup的百分比来呈现视图的高度。

layout_weight属性被计算为任何给定父ViewGroup的子视图的layout_weight值总和的一部分。例如,假设我们有一个TextView和一个Button嵌套在一个方向为垂直的LinearLayout中。如果TextViewlayout_weight为 30,而Buttonlayout_weight为 70,那么TextView将占据其父布局高度的 30%,而Button将占据其父布局高度的 70%。为了使我们的任务更容易,让我们假设 100 为layout_weight总和,这样每个layout_weight值将表示为百分比。使用这种技术的唯一问题是layout_height是 Android 视图中的一个必需属性,所以我们必须将layout_height的值设置为0dp。通过将layout_height设置为0dp,你实际上是在告诉 Android 忽略layout_height而使用layout_weight

当您检查这个布局中包含的视图时,请注意其中一些视图有 ID,而另一些没有。只有在 Java 代码中引用视图时,为视图分配 ID 才有用。如果一个视图在整个用户体验中保持静态,就没有理由给它分配一个 ID。当你从清单 9-1 重新创建布局时,注意id的使用,以及layout_weightlayout_height的使用。选择 activity_main.xml 选项卡后,您会在底部看到另外两个选项卡,Design 和 Text。点击文本选项卡,然后键入清单 9-1 中包含的代码,或者如果你正在阅读这本书的电子版,复制并粘贴。确保完全替换activity_main.xml中任何现有的 XML 代码。

Listing 9-1. activity_main.xml Code

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="#000"

android:orientation="vertical">

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="20"

android:orientation="vertical">

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="30"

android:gravity="bottom"

android:text="Foreign Currency"

android:textColor="#ff22e9ff"/>

<Spinner

android:id="@+id/spn_for"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="55"

android:gravity="top"/>

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="15"

android:gravity="bottom"

android:text="Enter foreign currency amount here:"

android:textColor="#666"

android:textSize="12sp"/>

</LinearLayout>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="20"

android:background="#222">

<EditText

android:id="@+id/edt_amount"

android:layout_width="fill_parent"

android:layout_height="50dp"

android:layout_gravity="center_vertical"

android:layout_marginLeft="5dp"

android:layout_marginRight="5dp"

android:background="#111"

android:digits="0123456789."

android:gravity="center_vertical"

android:inputType="numberDecimal"

android:textColor="#FFF"

android:textSize="30sp">

<requestFocus/>

</EditText>

</LinearLayout>

<Button

android:id="@+id/btn_calc"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="10"

android:text="Calculate"

android:textColor="#AAA"/>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="20"

android:orientation="vertical">

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="30"

android:gravity="bottom"

android:text="Home Currency"

android:textColor="#ff22e9ff"/>

<Spinner

android:id="@+id/spn_hom"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="55"

android:gravity="top"/>

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="15"

android:gravity="bottom"

android:text="Calculated result in home currency:"

android:textColor="#666"

android:textSize="12sp"/>

</LinearLayout>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="20"

android:background="#222">

<TextView

android:id="@+id/txt_converted"

android:layout_width="fill_parent"

android:layout_height="50dp"

android:layout_gravity="center_vertical"

android:layout_marginLeft="5dp"

android:layout_marginRight="5dp"

android:background="#333"

android:gravity="center_vertical"

android:textSize="30sp"

android:typeface="normal"/>

</LinearLayout>

</LinearLayout>

一旦创建了这个布局,按 Ctrl+K | Cmd+K 并提交一条修改 activity_main 布局的消息。

定义颜色

当您检查清单 9-1 中的 XML 源代码时,请注意我们已经硬编码了像textColorbackground这样的属性。将颜色值外部化到资源文件是一个好主意,特别是当颜色重复时。一旦您将一种颜色具体化,您就可以通过简单地改变资源文件中的一个值来改变整个应用中的颜色。在第五章中,我们展示了如何使用IntelliSense创建颜色定义。这里,我们将从颜色定义开始,替换硬编码的值。使用对你来说最容易的方法。右键单击(在 Mac 上按住 Ctrl 键)目录并选择“新➤值资源文件”。命名文件颜色,然后单击确定。如果提示将文件添加到 Git,选择记住,不要再问复选框,并选择是。修改清单 9-2 中的colors文件。

Listing 9-2. Define Some Colors in colors.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

<color name="white">#FFF</color>

<color name="black">#000</color>

<color name="grey_very_dark">#111</color>

<color name="grey_dark">#222</color>

<color name="grey_med_dark">#333</color>

<color name="grey_med">#666</color>

<color name="grey_light">#AAA</color>

<color name="turquoise">#ff22e9ff</color>

<color name="flat_blue">#ff1a51f4</color>

</resources>

在 Android 中,颜色用十六进制数字表示。十六进制数字可以使用以下字母数字值:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E 和 F。从 0 到 9 的十进制和十六进制数字是相同的,但要用十六进制表示 10、11、12、13、14 和 15,则分别使用 A、B、C、D、E 和 F。十六进制数字不区分大小写,所以 F 与 F 相同。

在 Android 中,你可以用四种格式中的一种来表达颜色:#ARGB、#RGB、# AARRGGBB 或# RRGGBB 每个字母是一个十六进制数字。#ARGB 格式代表 Alpha,红色,绿色,蓝色通道,Alpha 是透明通道。该配色方案中可能的颜色数量是 16 个可能的透明度值乘以 16×16×16 个可能的颜色组合。#RGB 格式代表红色、绿色、蓝色,Alpha 通道自动设置为完全不透明。# AARRGGBB 和#RRGGBB 格式使用 8 位通道,而不是#ARGB 和#RGB 格式中使用的 4 位通道。# AARRGGBB 格式中可能的颜色组合数量是 256 个可能的透明度级别乘以 256×256×256 个可能的颜色组合。#RRGGBB 格式类似于前者,只是透明度级别自动设置为完全不透明。

我们的colors.xml文件中的<color name="grey_med">#666</color>条目使用#RGB 格式。显然,具有等量红色、绿色和蓝色的颜色将是灰色。我们的colors.xml文件中的<color name="turquoise">#ff22e9ff</color>条目使用# AARRGGBB 格式。我们可以看到,我们的绿松石被定义为大量的蓝色和绿色,很少红色。如果我们在任何 XML 文件的装订线中单击任何颜色样本,我们可以看到一个对话框,允许我们定义我们想要的任何颜色,尽管从“选择颜色”对话框返回的字符串总是采用最精确的格式#AARRGGBB。见图 9-10 。一旦你定义了你的颜色,按 Ctrl+K | Cmd+K 提交一条定义一些颜色的消息。

A978-1-4302-6602-0_9_Fig10_HTML.jpg

图 9-10。

The Choose Color dialog box

将颜色应用于布局

现在您已经在colors.xml文件中定义了您的颜色,您可以将它们应用到您的布局中。一种方法是使用 Android Studio 的查找/替换功能。参见第五章了解创建颜色值的另一种方法。在编辑器中将activity_main.xml和 colors.xml 文件作为选项卡打开。右键单击(在 Mac 上按住 Ctrl 键)?? 标签并选择向右移动,这样你就可以并排看到两个文件。将光标置于 activity_main.xml 选项卡中,然后按 Ctrl+R | Cmd+R。在查找字段中键入#FFF,在替换字段中键入@color/white。选中单词复选框,然后单击全部替换。对我们定义的所有颜色重复这个步骤,除了flat_blue,我们稍后会用到它。你可以在图 9-11 中看到这个过程。一旦你应用了你的颜色,按 Ctrl+K | Cmd+K 确认并显示一条消息“应用颜色到布局”。然后关闭colors.xml选项卡。

A978-1-4302-6602-0_9_Fig11_HTML.jpg

图 9-11。

Replacing hard-coded color values with named references in the colors.xml file

创建和应用样式

风格可以大大提高你的生产力。短期内对创建风格的少量投资可能会在长期内为您节省大量时间,并且提供很大的灵活性。在这一节中,我们将为activity_main.xml布局中的一些视图创建样式,并向您展示如何应用它们。

我们使用的布局适合样式,因为许多属性在视图中是重复的。例如,两个蓝绿色的TextView控件共享除文本之外的所有相同属性。我们可以将这些复制的属性提取到一个样式中,然后将该样式应用于适当的TextView元素。如果我们以后想改变样式,我们只需简单地改变一次样式,所有使用该样式的视图也会改变。样式是有用的,但是没有理由对样式感到高兴,并把样式应用到你所有的视图中。例如,为 Calculate 按钮创建一个样式没有多大意义,因为只有一个样式。

我们的第一个任务是为在activity_main.xml布局中使用的标签(TextViews)创建样式。将您的光标放在我们的第一个TextView的定义内的任何地方,这个定义具有文本属性Foreign Currency。从主菜单中,选择重构➤提取➤风格。

在提取 Android 样式对话框中,选择如图 9-12 所示的复选框。在样式名称字段中输入 label。确保选中启动复选框,然后单击确定。在随后的可能使用样式对话框中,如图 9-13 所示,选择文件单选按钮,然后点击确定。现在,在“查找工具”窗口中单击“执行重构”(位于 IDE 底部),将该样式应用于共享这些属性的其他三个视图。

A978-1-4302-6602-0_9_Fig13_HTML.jpg

图 9-13。

The Use Style Where Possible dialog box

A978-1-4302-6602-0_9_Fig12_HTML.jpg

图 9-12。

Extracting the style called label

样式最好的属性之一是它们可以从你或 Android SDK 定义的父样式继承。将光标放在同一个TextView控件的同一个定义中,再次选择重构➤提取➤风格。

您会注意到提供给您的样式名称以label.开头。label后面的圆点表示这种新样式将继承其父样式label。将样式命名为 label.curr,如图 9-14 所示,点击确定。再次单击“执行重构”。

A978-1-4302-6602-0_9_Fig14_HTML.jpg

图 9-14。

Extracting the style called label.curr

activity_main.xml文件中,导航到标签为Enter foreign currency amount here:TextView。将光标放在该视图定义的括号内的任意位置,并从主菜单中选择“重构➤提取➤样式”。Android Studio 足够智能,能够意识到文本可能不会重复,并从提取 Android 样式对话框中将其忽略。将该样式重命名为 label.desc,点击确定,如图 9-15 所示。再次单击 IDE 底部的 Do Refactor,将样式应用于第二次出现的这个TextView

A978-1-4302-6602-0_9_Fig15_HTML.jpg

图 9-15。

Extracting the style called label.desc

让我们为布局再创建一种样式,为输入字段和输出字段提供灰色背景。将光标放在背景为@color/grey_darkLinearLayout的定义内的任何地方。从主菜单中,选择重构➤提取➤风格。调用你的新样式 layout_back,如图 9-16 所示,点击确定。

A978-1-4302-6602-0_9_Fig16_HTML.jpg

图 9-16。

Extracting the style called layout_back

从“尽可能使用样式”对话框中选择“文件”单选按钮,然后单击“确定”。现在单击 Do Refactor 将样式应用到第二次出现的布局。

按 Ctrl+Shift+N | Cmd+Shift+O,键入 styles,选择res/values/styles.xml文件,在编辑器中将其作为选项卡打开。你应该会得到一些看起来很像图 9-17 的东西。按 Ctrl-K | Cmd+K 提交,并显示创建样式并将样式应用于布局的消息。

A978-1-4302-6602-0_9_Fig17_HTML.jpg

图 9-17。

Styles created automatically for you in the styles.xml file

创建 JSONParser 类

为了从openexchangerates.org web 服务中读取数据,我们需要一种解析 JSON 的方法。JSON 代表 JavaScript 对象符号,已经成为 web 服务事实上的标准格式。我们已经创建了自己的 JSON 解析器,名为JSONParser。这个类使用DefaultHttpClient填充InputStream,使用BufferedReader解析数据,使用JSONObject构造并返回JSONObject。虽然这听起来很复杂,但却非常简单。顺便说一下,我们不是唯一提出 JSON 解析器的人;如果在您最喜欢的搜索引擎中搜索 JSON parser,您会发现这种基本模式的许多实现。

详细解释JSONParser如何工作超出了本书的范围。尽管如此,请将这个类添加到您的项目中,因为我们将需要它的全部功能。右键单击(在 Mac 上按住 Ctrl 键单击)这个com.apress.gerber.currencies包,然后选择 New ➤ Java Class。将您的类命名为 JSONParser。将清单 9-3 中的代码键入(或者复制粘贴)到这个类中。

Listing 9-3. The JSONParser.java Code

package com.apress.gerber.currencies;

import android.util.Log;

import org.apache.http.HttpEntity;

import org.apache.http.HttpResponse;

import org.apache.http.client.ClientProtocolException;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.impl.client.DefaultHttpClient;

import org.json.JSONException;

import org.json.JSONObject;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.io.UnsupportedEncodingException;

public class JSONParser {

static InputStream sInputStream = null;

static JSONObject sReturnJsonObject = null;

static String sRawJsonString = "";

public JSONParser() {}

public JSONObject getJSONFromUrl(String url) {

//attempt to get response from server

try {

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpPost httpPost = new HttpPost(url);

HttpResponse httpResponse = httpClient.execute(httpPost);

HttpEntity httpEntity = httpResponse.getEntity();

sInputStream = httpEntity.getContent();

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

} catch (ClientProtocolException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

//read stream into string-builder

try {

BufferedReader reader = new BufferedReader(new InputStreamReader(

sInputStream, "iso-8859-1"), 8);

StringBuilder stringBuilder = new StringBuilder();

String line = null;

while ((line = reader.readLine()) != null) {

stringBuilder.append(line + "\n");

}

sInputStream.close();

sRawJsonString = stringBuilder.toString();

} catch (Exception e) {

Log.e("Error reading from Buffer: " + e.toString(), this.getClass().getSimpleName());

}

try {

sReturnJsonObject = new JSONObject(sRawJsonString);

} catch (JSONException e) {

Log.e("Parser", "Error when parsing data " + e.toString());

}

//return json object

return sReturnJsonObject;

}

}

在您键入或粘贴了前面的代码之后,按 Ctrl+K | Cmd+K 提交您的更改,并显示一条“创建 JSONParser 类”的提交消息。

创建 Splash 活动

在本节中,我们将创建 splash 活动。这项活动的功能是为我们争取大约一秒钟的时间,以便获取有效的货币代码。当后台线程正在工作时,我们将显示一张货币的照片。如果这是一个商业应用,我们可能会显示一个带有公司标志的图像,也许还有应用的名称。

右键单击(在 Mac 上按住 Ctrl 并单击)该com.apress.gerber.currencies包,然后选择“新建➤活动”“➤空白活动”。将您的Activity命名为 SplashActivity,并选中启动器活动复选框,如图 9-18 所示。

A978-1-4302-6602-0_9_Fig18_HTML.jpg

图 9-18。

New ➤ Activity ➤ Blank Activity to create SplashActivity

在新创建的SplashActivity.java file中,修改类定义,使SplashActivity扩展Activity而不是ActionBarActivity。同样在onCreate()方法中插入this.requestWindowFeature(Window.FEATURE_NO_TITLE);,如清单 9-4 所示,并解析导入。

Listing 9-4. Modify the SplashActivity Class to Extend Activity and Remove the Title Bar

public class SplashActivity extends``Activity

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

this.requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.activity_splash);

}

...

按 Ctrl+Shift+N | Cmd+Shift+O,然后键入和。选择并打开app/src/main/AndroidManifest.xml文件。修改文件,使其看起来如清单 9-5 所示。

Listing 9-5. Modified AndroidManifest.xml File

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="``http://schemas.android.com/apk/res/android

package="com.apress.gerber.currencies" >

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

<application

android:allowBackup="true"

android:icon="@android:color/transparent"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:icon="@mipmap/ic_launcher"

android:name=".MainActivity"

android:label="@string/app_name" >

</activity>

<activity

android:name=".SplashActivity"

android:label="@string/title_activity_splash">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

我们添加到AndroidManifest.xml文件中的uses-permission行允许应用访问互联网。此外,我们已经将应用本身的icon属性设置为transparent,以确保在SplashActivity之前不显示任何内容。注意SplashActivity现在包含了主/启动器意图过滤器,而不是MainActivity。main/launcher intent-filter 告诉 Android OS 哪个活动将首先启动。

我们需要一些免版税的作品来显示在我们的闪屏上。将浏览器指向 google.com/advanced_image_search 。在所有这些单词字段中,键入货币。在“使用权”字段中,选择“免费使用、共享或修改,甚至商业使用”。单击高级搜索。找到一张你喜欢的图片并下载下来。将图像命名为 world_currencies.jpg(如果文件是 png,则命名为 world_currencies.png)。将world_currencies.jpg复制并粘贴到项目工具窗口中的res/drawable目录中。修改activity_splash.xml文件,结果如清单 9-6 所示。

Listing 9-6. Modified activity_splash.xml File to Display world_currencies as Background

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="@drawable/world_currencies"

android:orientation="vertical">

</LinearLayout>

按 Ctrl+K | Cmd+K 提交一条创建 splash 活动的消息,并使其成为已启动的活动。

以 JSON 形式提取活动货币代码

在上一节中,您使SplashActivity成为首先启动的活动,并修改了它的布局以显示world_currencies图像。在本节中,您将修改SplashActivity来触发一个后台线程,以便从 openexchangerates.org/api/currencies.json 获取活动货币代码。

按 Ctrl+N | Cmd+O,键入 Spl,选择SplashActivity.java文件。在我们的SplashActivity中没有菜单,所以我们可以移除那些引用菜单的方法。移除onCreateOptionsMenu()onOptionsItemSelected()方法。

我们需要创建一个AsyncTask,我们称之为FetchCodesTask,因为SplashActivity.java. AsyncTask的私有内部类是一个专门设计用于在 Android 中促进并发(线程)操作的类。我们在第十章的中讨论了AsyncTask的架构,所以在此期间只要相信AsyncTask是可行的。

首先将FetchCodesTask定义为onCreate()方法下SplashActivity.java类的私有内部类,如下所示:

private class FetchCodesTask extends AsyncTask<String, Void, JSONObject> {

}

通过将光标放在红色文本上,然后按 Alt+Enter 并选择 Import Class 来解决任何导入,如图 9-19 所示。

A978-1-4302-6602-0_9_Fig19_HTML.jpg

图 9-19。

Resolving JSONObject and AsyncTask imports

即使在解决了这些导入之后,类定义也应该用红色下划线标出,表明存在编译时错误。将光标放在这个新的内部类定义中,按 Alt+Insert | Cmd+N,然后选择 Override Methods。在弹出的对话框中,按住 Ctrl 键(Mac 上为 Cmd 键)并点击 OK,选择doInBackground()onPostExecute()两种方法,如图 9-20 所示。

A978-1-4302-6602-0_9_Fig20_HTML.jpg

图 9-20。

Selecting doInBackground and onPostExecute methods

请注意,您的方法参数是根据包含在内部类定义中的泛型定义的。修改您的SplashActivity.java类,使其最终如清单 9-7 所示,并解析任何导入。

Listing 9-7. Modify the SplashActivity.java file

public class SplashActivity extends Activity {

//url to currency codes used in this application

public static final String URL_CODES = "``http://openexchangerates.org/api/currencies.json

//ArrayList of currencies that will be fetched and passed into MainActivity

private ArrayList<String> mCurrencies;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

this.requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.activity_splash);

new FetchCodesTask().execute(URL_CODES);

}

private class FetchCodesTask extends AsyncTask<String, Void, JSONObject> {

@Override

protected JSONObject doInBackground(String... params) {

return new JSONParser().getJSONFromUrl(params[0]);

}

@Override

protected void onPostExecute(JSONObject jsonObject) {

try {

if (jsonObject == null) {

throw new JSONException("no data available.");

}

Iterator iterator = jsonObject.keys();

String key = "";

mCurrencies = new ArrayList<String>();

while (iterator.hasNext()) {

key = (String)iterator.next();

mCurrencies.add(key + " | " + jsonObject.getString(key));

}

finish();

} catch (JSONException e) {

Toast.makeText(

SplashActivity.this,

"There's been a JSON exception: " + e.getMessage(),

Toast.LENGTH_LONG

).show();

e.printStackTrace();

finish();

}

}

}

}

通过点击该行旁边的装订线,在第mCurrencies.add(key + " | " + jsonObject.getString(key));行设置断点。点按工具栏中的“调试”按钮(看起来像 bug 的按钮)。等待项目在调试器中生成和加载。当遇到断点时,单击几次 Resume 按钮(调试工具窗口中的绿色右箭头)。如果您在调试窗口中打开mCurrencies,您会注意到这些值并没有按照特定的顺序获取。见图 9-21 。在调试器窗口中,单击看起来像红色正方形的“停止”按钮。既然我们对正确获取值感到满意,那么按 Ctrl+K | Cmd+K 提交一条从 openexchangerates.org 获取 json 代码的消息。

A978-1-4302-6602-0_9_Fig21_HTML.jpg

图 9-21。

Debug window inspecting mCurrencies frame by frame

启动主活动

在上一节中,您通过使用一个AsyncTask成功地获取了活动货币代码。现在您需要启动MainActivity并向它传递当前的货币代码。

Android 的软件架构极其开放和模块化。模块化是一种福气,因为我们可以将任意数量的第三方应用集成到我们自己的应用中。然而,模块化也是一个诅咒,因为这些其他应用不共享相同的内存空间,因此我们不能简单地传递对象引用。Android 通过在每个活动周围创建一堵中国墙来加强这种模块化,任何对象引用都不能通过这堵墙。仅按值传递规则同样适用于应用间通信和应用内通信。即使我们的SplashActivityMainActivity位于同一个应用的同一个包中,我们仍然必须序列化这两个组件之间的任何通信,就好像每个活动位于不同的服务器上一样;这是我们为开发一个开放的模块化软件架构所付出的代价。

通过使用 Android 中一个名为Intent的专门类,可以方便地通过值传递数据。意图是发送给 Android 操作系统的消息。您不能将意图从一个活动直接发送到另一个活动;Android 操作系统必须总是调解活动之间的通信,这就是为什么您的活动必须总是列在您的AndroidManifest.xml文件中。一个意向也可能有一个称为 bundle 的有效负载。bundle 是键/值对的映射,其中键是字符串,值是 Java 原语或序列化对象。一旦 intent 的包完全装载了数据,就可以将 intent 分派给 Android OS,后者将 intent 及其有效负载传递给目的地活动。

我们想要从SplashActivity传递到MainActivity的数据只是一个字符串列表。幸运的是,ArrayList<String>已经实现了Serializable接口,所以我们可以将mCurrencies对象放入目的地为MainActivity的意图的bundle中,并将该意图分派给 Android 操作系统。打开SplashActivity.java文件。在while循环块之后,放置如图 9-22 所示的三行代码。

A978-1-4302-6602-0_9_Fig22_HTML.jpg

图 9-22。

Create and dispatch Intent

根据需要解决导入问题。在图 9-22 的第一行新代码中,我们正在构建一个意图,并传递一个上下文(SplashActivity.this)和一个目的地活动(MainActivity.class)。下一行用关键字"key_arraylist"mCurrencies对象添加到我们的 intent 包中。最后一行,startActivity(mainIntent);,将意图发送给 Android 操作系统,后者负责找到目的地并交付有效载荷。

将光标放在key_arraylist上,按 Ctrl+Alt+C | Cmd+Alt+C 提取一个常数。选择SplashActivity作为将要定义常量的类,如图 9-23 所示,从建议列表中选择KEY_ARRAYLIST,按回车键在该类中创建一个常量。

A978-1-4302-6602-0_9_Fig23_HTML.jpg

图 9-23。

Select SplashActivity to be the class in which constant will be defined

按 Ctrl+K | Cmd+K 并提交一条带有意图的 Fires-up MainActivity 消息,然后将 ArrayList 传递给 Bundle。

摘要

在这一章中,我们描述了货币应用规范,并着手实现它的一些特性。我们定义了布局,提取了颜色,创建并应用了样式。我们还介绍了 JSON,并创建了一个闪屏来获取活动货币代码,这些代码是填充主活动的微调器所必需的。我们引入了AsyncTask并从 web 服务中获取 JSON 数据。我们还使用意图在活动之间进行交流。在下一章中,我们将完成货币应用。

十、货币 Lab:第二部分

在前一章中,您通过在SplashActivity中使用一个AsyncTask来获取活动货币代码。您将货币代码加载到一个包中,并将该包附加到目的地为MainActivity的意向中。最后,您将意向发送到 Android 操作系统。

在本章中,您将继续开发货币应用,并专注于MainActivity的功能来完成应用。您将使用一个ArrayAdapter将一个字符串数组绑定到微调器。您将使用 Android Studio 将视图行为的处理委托给封装活动。您还将了解如何使用共享偏好设置以及资源。您将了解 Android 中的并发性,尤其是如何使用AsyncTask。最后,您将修改布局并使用 Android Studio 生成可绘制的资源。

定义主活动的成员

让我们从定义对应于activity_main.xml布局文件中视图的MainActivity类中的引用开始,然后给它们分配对象。打开MainActivity.javaactivity_main.xml文件,这样你可以参考这两个文件。右键单击activity_main.xml选项卡,选择右移,将activity_main.xml的模式改为文本。修改您的MainActivity.java文件,使其看起来如图 10-1 所示,并根据需要通过按 Alt+Enter 来解决任何导入。

A978-1-4302-6602-0_10_Fig1_HTML.jpg

图 10-1。

Define members and assign references to these members

注意,我们在MainActivity中只为那些在activity_main.xml中的视图定义了引用,我们之前已经为这些视图分配了一个 ID。setContentView (R.layout.activity_main);陈述夸大了activity_main.xml中包含的观点。在 Android 中,world inflate 意味着当 Android 遍历activity_main.xml布局中定义的视图时,Android 会将每个视图实例化为堆中的一个 Java 对象。如果那个View对象有一个 id,Android 会将那个对象的内存位置与其 ID 关联起来。这种关联可以在一个名为R.java的自动生成文件中找到,它在您的资源和 Java 源文件之间起到了桥梁的作用。

一旦布局和它的所有视图都被放入内存空间,我们就可以通过调用findViewById()方法并传递一个 ID 来将这些对象分配给我们之前定义的引用。findViewById()方法返回一个View对象,它是 Android 中所有ViewsViewGroups的层次祖先;这就是为什么我们需要将每个对findViewById()的调用转换到适当的View子类。按 Ctrl+K | Cmd+K 并提交,同时显示获取对布局中定义的视图的引用的消息。

从捆绑包中解包货币代码

在前一章中,我们将一个StringsArrayList传递到用于启动MainActivity的 intent 包中。虽然 Android OS 已经成功交付了它的有效载荷,但我们仍然需要拆开它。我们在SplashActivity中使用的数据结构是一个向量(ArrayList<String>,这意味着它可以根据需要增长和收缩。我们将在MainActivity中用来存储活动货币代码的数据结构将是一个长度固定的简单字符串数组。改变数据结构的原因是我们将使用ArrayAdapter作为微调器的控制器,而ArrayAdapter使用数组,而不是ArrayLists。修改MainActivity类,使其看起来如图 10-2 所示,并根据需要解析任何导入。

A978-1-4302-6602-0_10_Fig2_HTML.jpg

图 10-2。

Unpack currency codes from ArrayList

语句ArrayList<String> arrayList = ((ArrayList<String>) getIntent().getSerializableExtra(SplashActivity.KEY_ARRAYLIST));ArrayList<String>从用于启动此活动的意向包中解包。注意,我们使用同一个公共常量作为键(SplashActivity.KEY_ARRAYLIST)来对MainActivity中的ArrayList<String>进行解包,我们之前使用这个常量来对SplashActivity中的ArrayList<String>进行打包。还要注意,我们使用了Collections接口对数据进行排序,然后我们将ArrayList<String>转换成一个字符串数组。按 Ctrl+K | Cmd+K 并提交一条从包中解包货币代码的消息。

创建选项菜单

新建项目向导为我们创建了一个名为menu_main.xml的菜单。按 Ctrl+Shift+N | Cmd+Shift+O,键入 main,选择res/menu/menu_main.xml打开。修改menu_main.xml,使其看起来像清单 10-1 。

Listing 10-1. Modify the menu_main.xml File

<menu xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:app="``http://schemas.android.com/apk/res-auto

xmlns:tools="``http://schemas.android.com/tools

tools:context=".MainActivity">

<item

android:id="@+id/mnu_codes"

android:orderInCategory="100"

app:showAsAction="never"

android:title="search active codes"/>

<item

android:id="@+id/mnu_invert"

android:orderInCategory="200"

app:showAsAction="never"

android:title="invert codes"/>

<item

android:id="@+id/mnu_exit"

android:orderInCategory="300"

app:showAsAction="never"

android:title="exit"/>

</menu>

app:showAsAction属性决定了菜单项的位置。将该属性设置为never意味着该菜单项永远不会出现在动作栏上,而总是出现在溢出菜单中。溢出菜单由操作栏右侧的三个垂直圆点表示。

android:orderInCategory用于设置菜单项的顺序。Android 中的惯例是使用 100 的倍数,例如,我们可以使用 250 在 200 和 300 之间插入一个新的菜单项,使用 225 在 200 和 250 之间插入一个新的菜单项。orderInCategory属性值必须是一个整数,所以如果我们从 2 和 3 这样的连续值开始,将没有空间插入中间值,我们将不得不对整个集合重新排序。

请注意,我们为每个菜单项分配了一个 ID,这样我们以后就可以在 Java 代码中引用这些对象。打开MainActivity.java,改变onOptionsItemSelected()方法,如清单 10-2 所示。

Listing 10-2. Modify the onOptionsItemSelected( ) Method

public boolean onOptionsItemSelected(MenuItem item) {

int id = item.getItemId();

switch (id){

case R.id.mnu_invert:

//TODO define behavior here

break;

case R.id.mnu_codes:

//TODO define behavior here

break;

case R.id.mnu_exit:

finish();

break;

}

return true;

}

注意,除了 Exit 菜单项之外,我们用 TODOs 代替了实现代码。我们将在下一步实现其余选项菜单项的功能。按 Ctrl+K | Cmd+K 并提交一条创建选项菜单的消息。

实施选项菜单行为

在本节中,我们将编写需要权限的代码。如果你是一个 Android 用户,那么你可能很熟悉在安装一个应用之前你必须同意的一系列权限。一些应用比其他应用需要更多权限,但大多数应用至少需要一个权限。在前面的步骤中,我们请求用户允许我们访问互联网。在这一步中,我们将向用户请求访问设备网络状态的权限。很容易忽略权限,尤其是如果你是一个新手 Android 程序员。幸运的是,如果您忘记包含适当的权限,与这个问题相关的异常是简单明了的。

要打开AndroidManifest.xml文件,按 Ctrl+Shift+N | Cmd+Shift+O,键入和,按 Enter 选择AndroidManifest.xml打开。修改AndroidManifest.xml插入图 10-3 中高亮显示的线。

A978-1-4302-6602-0_10_Fig3_HTML.jpg

图 10-3。

Add permission to access the network state in the AndroidManifest.xml file

打开MainActivity.java类。定义清单 10-3 中的三种方法。isOnline()方法检查用户是否有互联网连接。这个方法使用的是 Android ConnectivityManager,这就是为什么我们需要将android.permission.ACCESS_NETWORK_STATE添加到AndroidManifest.xml文件中。launchBrowser()方法接受一个表示统一资源标识符(URI)的字符串。URI 是统一资源定位器(URL)的超集,因此任何定义为有效 HTTP 或 HTTPS 地址的字符串都可以作为参数使用。launchBrowser()方法启动设备上的默认浏览器,并打开我们传递给它的 URI。invertCurrencies()方法简单地交换本币和外币微调器的值。当然,如果包含计算结果的TextView先前已经填充了数据,我们也需要清除它以避免任何混淆。把你的新方法放在onCreate()方法下面。

Listing 10-3. Create Three Methods in MainActivity.java

public boolean isOnline() {

ConnectivityManager cm =

(ConnectivityManager)

getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo networkInfo = cm.getActiveNetworkInfo();

if (networkInfo != null && networkInfo.isConnectedOrConnecting()) {

return true;

}

return false;

}

private void launchBrowser(String strUri) {

if (isOnline()) {

Uri uri = Uri.parse(strUri);

//call an implicit intent

Intent intent = new Intent(Intent.ACTION_VIEW, uri);

startActivity(intent);

}

}

private void invertCurrencies() {

int nFor = mForSpinner.getSelectedItemPosition();

int nHom = mHomSpinner.getSelectedItemPosition();

mForSpinner.setSelection(nHom);

mHomSpinner.setSelection(nFor);

mConvertedTextView.setText("");

}

按照清单 10-4 用方法调用替换MainActivity.java文件的onOptionsItemSelected()方法中的 TODOs。按 Ctrl+K | Cmd+K 并提交一条实现选项菜单行为和修改清单文件的消息。

Listing 10-4. Replace TODOs in onOptionsItemSelected( ) Method with Calls to the Methods We Just Defined

case R.id.mnu_invert:

invertCurrencies();

break;

case R.id.mnu_codes:

launchBrowser(SplashActivity.URL_CODES);

break;

创建 spinner_closed 布局

当微调器处于关闭状态时,为其创建布局。右键单击(在 Mac 上按住 Ctrl 键)目录并选择“新建➤布局资源文件”。将文件命名为 spinner_closed,点击确定,如图 10-4 所示。

A978-1-4302-6602-0_10_Fig4_HTML.jpg

图 10-4。

Define the spinner_closed layout resource file

修改spinner_closed.xml文件,如清单 10-5 所示。

Listing 10-5. Definition of spinner_closed.xml

<TextView xmlns:android="``http://schemas.android.com/apk/res/android

android:id="@android:id/text1"

android:background="@color/grey_very_dark"

android:textColor="@color/grey_light"

android:singleLine="true"

android:textSize="18sp"

android:layout_width="match_parent"

android:layout_height="fill_parent"

android:gravity="center_vertical"

android:ellipsize="marquee"

/>

将货币绑定到旋转器

本币微调器和外币微调器将显示相同的项目。我们需要将mCurrencies数组绑定到两个微调器。为此,我们将使用一个名为ArrayAdapter的类。在MainActivity.javaonCreate()方法中,添加如图 10-5 所示的代码,解析导入。

A978-1-4302-6602-0_10_Fig5_HTML.jpg

图 10-5。

Bind mCurrencies to spinners

ArrayAdapter构造函数有三个参数:一个上下文、一个布局和一个数组。ArrayAdapter充当模型-视图-控制器设计模式中的控制器,并协调模型和视图之间的关系。在我们的例子中,模型是名为mCurrencies的字符串数组。mCurrencies中的每个元素都包含一个货币代码、一个提供视觉分隔的管道字符和一个货币描述。微调器有两个视图:一个视图在微调器打开时显示,另一个视图在微调器关闭时显示。最后两条语句将新构建的arrayAdapter对象分配给微调器。按 Ctrl+K | Cmd+K 并提交一条将数据绑定到微调器的消息。通过按下 Shift+F10 | Ctrl+R 运行您的应用,并与两个微调器进行交互,以确保它们正常工作。

将微调器行为委托给 MainActivity

Java 事件模型非常灵活。我们可以将事件的处理委托给任何实现适当侦听器接口的对象。如果一个视图是惟一的,比如 Calculate 按钮,那么将其行为的处理委托给一个匿名的内部类是有意义的。然而,如果我们的布局包含相同类型的多个视图,比如货币应用中的两个或更多微调器,那么通常更容易将这些视图行为的处理委托给封闭类。将两行代码添加到MainActivity.javaonCreate()方法的末尾,如图 10-6 所示。单词this将以红色下划线标出,表示编译时错误。

A978-1-4302-6602-0_10_Fig6_HTML.jpg

图 10-6。

Delegate the behavior of spinners to MainActivity

将光标放在任一单词this上的任意位置,按 Alt+Enter 调用 IntelliSense 代码完成。选择第二个选项(使' MainActivity '实现' Android . widget . adapter view . onitemselectedlistener '),如图 10-7 所示。在如图 10-8 所示的选择要实现的方法对话框中选择两种方法,然后点击确定。如果你向上滚动到类的顶部,你会注意到MainActivity现在实现了AdapterView.OnItemSelectedListener

A978-1-4302-6602-0_10_Fig8_HTML.jpg

图 10-8。

Select Methods to Implement dialog box

A978-1-4302-6602-0_10_Fig7_HTML.jpg

图 10-7。

Make ‘MainActivity’ implement OnItemSelectedListener

OnItemSelectedListener接口有两个任何实现类都必须覆盖的收缩方法:onItemSelected()onNothingSelected()。我们不会在onNothingSelected()方法的主体中提供任何实现代码。虽然onNothingSelected()是一个鼻涕虫,但是它必须出现在MainActivity里面才能满足接口契约。

onItemSelected()方法中,我们需要通过检查parent.getId()来确定选择了哪个微调器,然后添加一些条件逻辑来编程所选微调器的行为。修改onItemSelected()方法,如图 10-9 所示。

A978-1-4302-6602-0_10_Fig9_HTML.jpg

图 10-9。

Modify the onItemSelected( ) method

请注意,我们将占位符注释(//define behavior here)放在了我们期望实现代码所在的位置。我们将在后续步骤中实现 spinners 的行为。按 Ctrl+K | Cmd+K 并提交一条消息,将 spinners 行为的处理委托给 MainActivity。

创建首选项管理器

共享偏好设置提供了一种在应用退出时保留用户偏好设置的方法。如果我们试图将用户的偏好存储在内存中,这些数据将在用户退出应用后被刷新,应用的内存将被 Android 操作系统回收。为了解决这个问题,可以将共享偏好存储在用户设备上的文件中。这个文件是一个带有键/值对的序列化哈希映射,每个应用可能都有自己的共享首选项。

可以存储在共享首选项中的值的类型仅限于 Java 原语、字符串、序列化对象和序列化对象数组。与向 SQLite 数据库读写数据相比,共享首选项速度较慢。因此,您不应该考虑使用共享首选项作为记录管理的替代方案;您应该始终使用 SQLite 数据库进行记录管理,就像您在 Reminders 实验中看到的那样。然而,共享偏好是保持用户偏好的一个好方法。

我们希望保留本币和外币微调器中显示的货币代码。这是一个典型的场景。假设一个美国用户正在伊斯坦布尔度假,在露天市场使用货币应用争论一些珍贵的拜占庭古董。用户退出应用并返回酒店。第二天早上,他在当地一家餐馆吃早餐,然后启动货币应用查看账单。如果我们的用户在执行另一个计算之前不得不在微调器中重新选择 TRY 和 USD,这将是非常令人沮丧的。相反,微调器应该自动填充先前为本币和外币选择的代码。

我们将创建一个实用程序类,让我们能够访问共享的首选项。我们的实用程序类将具有公共静态方法,允许我们获取和设置用户为本国货币和外国货币选择的货币代码。右键单击(在 Mac 上按住 Ctrl 键)这个com.apress.gerber.currencies包并选择 New Java Class。将您的类命名为 PrefsMgr,并插入如图 10-10 所示的代码。

A978-1-4302-6602-0_10_Fig10_HTML.jpg

图 10-10。

Create the PrefsMgr class

setString()方法为国内或国外的特定地区设置货币代码。getString()方法将返回为特定地区存储的货币代码值,如果没有找到代码,那么默认情况下将返回null。按 Ctrl+K | Cmd+K 并提交一条创建我们自己的首选项管理器的消息。

查找给定代码的位置

微调器使用从零开始的整数来表示其当前位置的值。要将微调器设置为特定代码,我们需要找到元素的适当位置或索引。由于mCurrencies被用作微调器的模型,我们可以简单地将货币代码与存储在mCurrencies中的聚合字符串的前三个字符进行比较。如果找到匹配,我们返回索引位置。如果没有找到匹配,我们返回到零,这对应于微调器的第一个位置。ISO 4217 货币代码标准规定货币代码的长度总是三个字母。让我们编写一个方法,从包含货币代码、管道字符和货币描述的聚合字符串中提取三个字母的货币代码。我们知道这个聚合字符串的前三个字符是货币代码,所以我们可以使用 string 的 substring()方法来提取它。打开MainActivity.java,在 invertCurrencies()方法下定义 findPositionGivenCode()方法,如图 10-11 所示。按 Ctrl+K | Cmd+K 并提交一条创建查找位置给定代码方法的消息。

A978-1-4302-6602-0_10_Fig11_HTML.jpg

图 10-11。

Create the findPositionGivenCode() method

从货币中提取代码

从存储在mCurrencies的每个元素中的聚合字符串中提取三个字母的货币代码将不限于findPositionGivenCode()方法。与其在别处复制这些代码,不如提取一个方法,然后在需要它的功能时调用这个方法。在MainActivity.java中,高亮显示如图 10-12 所示的代码,按 Ctrl+Alt+M | Cmd+Alt+M 提取一个方法,选择第一个选项。

A978-1-4302-6602-0_10_Fig12_HTML.jpg

图 10-12。

Select the code that will be extracted as a method

在提取方法对话框中,将方法名称改为 extractCodeFromCurrency,如图 10-13 所示,点击确定。你最终应该得到类似图 10-14 的东西。按 Ctrl+K | Cmd+K 并提交名为 extractCodeFromCurrency 的消息提取方法。

A978-1-4302-6602-0_10_Fig14_HTML.jpg

图 10-14。

Resulting code from the extract method operation

A978-1-4302-6602-0_10_Fig13_HTML.jpg

图 10-13。

Create extractCodeFromCurrency( ) in the Extract Method dialog box

实施共享偏好设置

shared preferences 中的数据存储在一个 hash-map 中,其中的键总是字符串,因此这是一个将键定义为String常量的绝佳机会。打开MainActivity.java,定义如图 10-15 所示的两个String常量。

A978-1-4302-6602-0_10_Fig15_HTML.jpg

图 10-15。

Define two constants that will be used as keys

在你的MainActivity类的onCreate()方法的末尾插入如图 10-16 所示的if/else块。在前面的步骤中,我们对PrefsMgr类进行了编程,以便在找不到键的情况下返回nullif块检查本币和外币密钥是否都不存在。这种独特的情况只会发生一次,即首次在用户设备上使用该应用时,微调器将分别设置为 CNY 和美元作为外币和本币。如果不满足该唯一条件,微调器将被设置为存储在用户共享首选项中的值。

A978-1-4302-6602-0_10_Fig16_HTML.jpg

图 10-16。

Create the if/else block

使用共享偏好设置会对性能造成轻微影响,我们希望尽可能避免这种影响。我们在我们的if语句的括号内包含了savedInstanceState == null &&,以便在MainActivity简单地从中断或配置改变中恢复的情况下,这个块将会短路。

导航到我们之前定义的onItemSelected()方法。修改此方法,以便我们每次在一个微调器中选择一个项目时都设置共享首选项。此外,我们将清除mConvertedTextView以避免任何混淆。修改MainActivity.java如图 10-17 所示。

A978-1-4302-6602-0_10_Fig17_HTML.jpg

图 10-17。

Apply shared preferences to the onItemSelected method

最后,我们需要确保当用户从 options 菜单中选择 Invert Currencies 菜单项时,正确设置了共享的首选项。将如图 10-18 所示的两行代码添加到invertCurrencies()方法的末尾。按 Ctrl+K | Cmd+K 并提交一条实现共享首选项的消息。

A978-1-4302-6602-0_10_Fig18_HTML.jpg

图 10-18。

Apply shared preferences to the invertCurrencies method

按钮点击行为

我们的 app 里只有一个按钮。因此,将按钮行为的处理委托给一个匿名的内部类,而不是委托给封闭活动,这是有意义的,就像我们前面对两个 spinners 所做的那样。

onCreate()方法的末尾,但仍在它的大括号内,键入 mcalcbutton . setonclicklistener();现在将光标放在该方法的括号内,并键入 new On。如有必要,使用向下箭头键从代码完成提供给您的建议中选择onClickListener{...}选项,然后按 Enter。在onClick()方法中添加一些占位符文本如//define behavior,如图 10-19 所示。按 Ctrl+K | Cmd+K 并提交一条消息,提示创建一个内部类来处理按钮行为。

A978-1-4302-6602-0_10_Fig19_HTML.jpg

图 10-19。

Create an anonymous inner class to handle button click behavior

存储开发者密钥

在项目工具窗口中右键单击app,选择新建➤文件夹➤素材文件夹。在随后的对话框中,默认情况下,目标源集选项应该是 main。单击完成。

在项目工具窗口中右键单击新创建的assets目录,并选择新建➤文件。命名新的文件密钥。属性,如图 10-20 所示。

A978-1-4302-6602-0_10_Fig20_HTML.jpg

图 10-20。

Create the keys.properties file

将下面一行添加到keys.properties文件中:

open_key=9a894f5f4f5742e2897d20bdcac7706a

您需要通过将浏览器导航到 https://openexchangerates.org/signup/free 来注册您自己的免费密钥。这个过程很简单,大约需要 30 秒。用你自己的有效密钥代替我们在这里提供的假密钥。参见图 10-21 。按 Ctrl+K | Cmd+K 并提交一条定义 openexchangerates.org 键的消息。

A978-1-4302-6602-0_10_Fig21_HTML.jpg

图 10-21。

Define open_key in the keys.properties file. The key provided here is a placeholder and will not work Note

我们提供的密钥 9a 894 F5 F4 f 5742 e 2897d 20 bdcac 7706 a 无法工作;它只是一个占位符。您需要在浏览器中导航到 https://openexchangerates.org/signup/free 来注册您自己的密钥,然后用您自己的有效密钥替换假密钥。

获取开发者密钥

MainActivity.java中定义一个方法,在extractCodeFromCurrency()方法下获取存储在keys.properties中名为getKey()的密钥。注意,我们使用AssetManagerkeys.properties读取一个密钥。您将需要根据需要解析导入。见图 10-22 。

A978-1-4302-6602-0_10_Fig22_HTML.jpg

图 10-22。

Define the getKey( ) method

文件 I/O 是一项开销很大的操作。我们在上一步中定义的getKey()方法包含这样的操作,所以我们希望尽可能少地调用getKey()。我们将在onCreate()中调用一次这个调用,然后将这个值存储在MainActivity的一个名为mKey的成员中,而不是每次我们想从openexchangerates.org中获取利率时都调用getKey()。定义你的MainActivity类的成员,如图 10-23 所示。

A978-1-4302-6602-0_10_Fig23_HTML.jpg

图 10-23。

Define members to facilitate fetching key and formatting results

onCreate()方法的末尾,但仍在它的括号内,给mKey赋值,如下所示:mKey = getKey("open_key");。参见图 10-24 。按 Ctrl+K | Cmd+K 并提交一条获取键、定义成员和常量的消息。

A978-1-4302-6602-0_10_Fig24_HTML.jpg

图 10-24。

Assign the key as the last statement of the onCreate( ) method

CurrencyConverterTask

线程是一个轻量级进程,它可以与同一应用中的其他线程并发运行。Android 并发性的第一条规则是你不能阻塞 UI 线程,它也称为主线程。UI 线程是默认情况下在应用启动期间产生的线程,它驱动用户界面。如果 UI 线程被阻塞超过 5000 毫秒,Android 操作系统将显示应用不响应(ANR)错误,您的应用将崩溃。阻塞 UI 线程不仅会导致 ANR 错误,而且当 UI 线程被阻塞时,用户界面将完全没有响应。因此,如果一个操作可能需要几毫秒以上的时间,那么它可能会阻塞 UI 线程,应该在后台线程上完成。例如,尝试从远程服务器获取数据可能会持续几毫秒以上,应该在后台线程上完成。当在 Android 环境中使用时,术语后台线程是指除 UI 线程之外的任何线程。

Note

UI 线程有时被称为主线程。

Android 并发的第二个规则是 UI 线程是唯一有权限与用户界面交互的线程。如果你试图从后台线程更新任何视图,你的应用将立即崩溃!违反 Android 并发规则中的一个或两个将导致糟糕的用户体验。

没有什么可以阻止你在你的 Android 应用中生成优秀的 Java 线程,但是一个名为AsyncTask的类是专门为解决本节描述的问题而设计的,因此它是 Android 并发的首选实现。如果你正确实现了AsyncTask,遵循 Android 并发的两条规则就没有问题。

在本节中,我们将创建一个名为CurrencyConverterTask的内部类,它将用于获取openexchangerates.org. CurrencyConverterTask上引用的汇率。它是抽象类AsyncTask. AsyncTask的一个具体实现,有一个名为doInBackground()的抽象方法,所有具体类都需要覆盖它。此外,您还可以覆盖其他一些方法,包括onPreExecute()onProgressUpdate()onPostExecute()等等。AsyncTask的神奇之处在于doInBackground()方法是在后台线程上执行的,而AsyncTask的其余方法是在 UI 线程上执行的。如果我们不接触doInBackground()方法中的任何视图,AsyncTask使用起来是完全安全的。

CurrencyConverterTask定义为私有内部类,在MainActivity.java的末尾,但仍在MainActivity的括号内。除了扩展AsyncTask,还必须定义三个通用对象参数,如图 10-25 所示。解决任何导入。即使在您解析了导入之后,您的类定义仍将带有红色下划线,表明存在编译时错误。暂时忽略这个。

A978-1-4302-6602-0_10_Fig25_HTML.jpg

图 10-25。

Define CurrencyConverterTask

将光标放在CurrencyConverterTask类定义的花括号内,按 Alt+Insert | Cmd+N,并选择 Override Methods。选择doInBackground()onPreExecute()onPostExecute()方法,点击确定,如图 10-26 所示。注意,返回值以及参数doInBackground()onPostExecute()是根据通用参数<String, Void, JSONObject>定义的。第一个参数(String)用作doInBackground()方法的输入,第二个参数(Void)用于向onProgressUpdate()方法发送进度更新,第三个参数(JSONObject)是doInBackground()的返回值,也是onPostExecute()方法的输入参数。整个获取操作大约需要一秒钟,因此用户几乎察觉不到进度更新;这就是为什么我们省略了onProgressUpdate()方法并使用Void作为第二个参数。

A978-1-4302-6602-0_10_Fig26_HTML.jpg

图 10-26。

Select methods to override/implement

让我们重新排列我们的方法,使它们按照被触发的顺序出现。选择整个onPreExecute()块,包括@Override注释,按 Ctrl+Shift+Up | Cmd+Shift+Up 将onPreExecute()方法移动到doInBackground()方法之上。你的CurrencyConverterTask现在应该看起来如图 10-27 所示。

A978-1-4302-6602-0_10_Fig27_HTML.jpg

图 10-27。

Results after overriding methods in CurrencyConverterTask and moving onPreExecute() up

再次修改CurrencyConverterTask,使其看起来像清单 10-6 并解析任何导入。让我们依次讨论一下CurrencyConverterTask的三个被覆盖的方法。

Listing 10-6. Modify the CurrencyConverterTask

private class CurrencyConverterTask extends AsyncTask<String, Void, JSONObject> {

private ProgressDialog progressDialog;

@Override

protected void onPreExecute() {

progressDialog = new ProgressDialog(MainActivity.this);

progressDialog.setTitle("Calculating Result...");

progressDialog.setMessage("One moment please...");

progressDialog.setCancelable(true);

progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE,

"Cancel", new DialogInterface.OnClickListener() {

@Override

public void onClick(DialogInterface dialog, int which) {

CurrencyConverterTask.this.cancel(true);

progressDialog.dismiss();

}

});

progressDialog.show();

}

@Override

protected JSONObject doInBackground(String... params) {

return new JSONParser().getJSONFromUrl(params[0]);

}

@Override

protected void onPostExecute(JSONObject jsonObject) {

double dCalculated = 0.0;

String strForCode =

extractCodeFromCurrency(mCurrencies[mForSpinner.getSelectedItemPosition()]);

String strHomCode = extractCodeFromCurrency(mCurrencies[mHomSpinner.getSelectedItemPosition()]);

String strAmount = mAmountEditText.getText().toString();

try {

if (jsonObject == null){

throw  new JSONException("no data available.");

}

JSONObject jsonRates = jsonObject.getJSONObject(RATES);

if (strHomCode.equalsIgnoreCase("USD")){

dCalculated = Double.parseDouble(strAmount) / jsonRates.getDouble(strForCode);

} else if (strForCode.equalsIgnoreCase("USD")) {

dCalculated = Double.parseDouble(strAmount)  * jsonRates.getDouble(strHomCode) ;

}

else {

dCalculated = Double.parseDouble(strAmount) *  jsonRates.getDouble(strHomCode)

/ jsonRates.getDouble(strForCode)   ;

}

} catch (JSONException e) {

Toast.makeText(

MainActivity.this,

"There's been a JSON exception: " + e.getMessage(),

Toast.LENGTH_LONG

).show();

mConvertedTextView.setText("");

e.printStackTrace();

}

mConvertedTextView.setText(DECIMAL_FORMAT.format(dCalculated) + " " + strHomCode);

progressDialog.dismiss();

}

}

onPreExecute()

在触发doInBackground()方法之前,在 UI 线程上执行onPreExecute()方法。由于我们可能不会从后台线程接触 UI 中的任何视图,onPreExecute()方法代表了在doInBackground()被触发之前修改 UI 的机会。当onPreExecute()被调用时,会出现一个ProgressDialog,用户可以选择按下取消按钮并终止操作。

doInBackground()

doInBackground()方法是AsyncTaskexecute()方法的代理。例如,调用CurrencyConverterTask的最简单方法是实例化一个新的引用匿名对象,并像这样调用它的execute()方法:

new CurrencyConverterTask().execute("url_to_web_service");

您传递给execute()的参数将依次传递给doInBackground(),但不是在执行onPreExecute()之前。我们doInBackground()的全称是protected JSONObject doInBackground(String... params)doInBackground()的参数被定义为 varargs,因此我们可以向execute()中传递尽可能多的逗号分隔的类型为String的参数,尽管在这个简单的应用中我们只传递一个——URL 的字符串表示。在doInBackground()方法中,params被视为一个字符串数组。为了引用第一个(也是唯一的)元素,我们使用params[0]

doInBackground()的体内,我们称之为return new JSONParser().getJSONFromUrl(params[0]);getJSONFromUrl()方法从 web 服务中获取一个JSONObject。因为这个操作需要用户设备和远程服务器之间的通信——因此可能需要几毫秒以上的时间——我们将getJSONFromUrl()放在了doInBackground()方法中。getJSONFromUrl()方法返回一个JSONObject,它是为doInBackground()定义的返回值。如前所述,doInBackground()是 AsyncTask 唯一运行在后台线程上的方法,其他所有方法都运行在 UI 线程上。注意,我们在doInBackground()方法中没有触及任何视图。

onPostExecute()

onPreExecute()一样,onPostExecute()方法运行在 UI 线程上。doInBackground()的返回值被定义为JSONObject。这个相同的对象将作为参数传递给onPostExecute()方法,该方法的完整签名被定义为protected void onPostExecute(JSONObject jsonObject)。当我们进入onPostExecute()方法时,doInBackground()方法的后台线程已经终止,我们现在可以用从doInBackground()获取的JSONObject数据安全地更新 UI。最后,我们进行一些计算,并将格式化的结果分配给mConvertedTextView

在运行我们的应用之前,我们需要对代码进行最后一次修改,以便执行CurrencyConverterTask。根据图 10-28 修改mCalcButtononClick()方法。

A978-1-4302-6602-0_10_Fig28_HTML.jpg

图 10-28。

Fire the new CurrencyConverterTask in the mCalcButton onClick method

按 Ctrl+K | Cmd+K 并提交一条实现 CurrencyConverterTask 的消息。按 Shift+F10 | Ctrl+R 运行应用。在此处输入外币金额字段中输入金额,然后单击计算按钮。您应该从服务器返回一个结果,这个结果应该显示在以本币计算的结果字段中。如果您的应用未能返回结果,请验证您是否拥有来自openexchangerates.org的有效开发者密钥。

按钮选择器

当您运行您的货币应用时,您可能已经注意到在mConvertedTextView中显示的文本是黑色的,这不能提供足够的对比度。打开activity_main.xml文件,插入图 10-29 中高亮显示的行,修改txt_converted TextView的定义。

A978-1-4302-6602-0_10_Fig29_HTML.jpg

图 10-29。

Insert the textColor attribute of txt_converted and set to @color/white in activity_main.xml

右键单击(在 Mac 上按住 Ctrl 键单击)可绘制目录,并选择“新建➤可绘制资源文件”。将资源命名为 button_selector,如图 10-30 所示。修改 XML,使其看起来如图 10-31 所示。根据图 10-32 更改activity_main.xmlbtn_calc的定义。

A978-1-4302-6602-0_10_Fig32_HTML.jpg

图 10-32。

Modify the btn_calc in activity_main.xml

A978-1-4302-6602-0_10_Fig31_HTML.jpg

图 10-31。

Modify the button_selector resource file

A978-1-4302-6602-0_10_Fig30_HTML.jpg

图 10-30。

Create the button_selector resource file

按 Ctrl+K | Cmd+K 并提交一条创建按钮选择器的消息。

启动器图标

我们将定义自己的图标,而不是使用普通的 Android 图标作为启动图标。我冒昧地使用先进的谷歌图片搜索找到了一枚一欧元硬币的免版税图片,这是流通中最好的硬币之一。你可以在这里找到这个形象: http://pixabay.com/static/uploads/photo/2013/07/13/01/21/coin-155597_640.png

下载这张图片,并将其命名为 coin.png。将您的项目工具窗口切换到 Android 视图。右键单击(在 Mac 上按住 Ctrl 键单击)res/mipmap 目录,然后选择“新建➤图像素材”。在随后的对话框中,选择可绘制目录作为目标目录。使用图 10-33 中的设置为每个分辨率创建ic_launcher.png文件,然后点击下一步并完成。将以下代码行插入到MainActivityonCreate()方法中,在展开布局的代码行setContentView(R.layout.activity_main)之后;。这段代码在您的操作栏中显示一个自定义图标:

A978-1-4302-6602-0_10_Fig33_HTML.jpg

图 10-33。

Create ic_launcher icons

ActionBar actionBar = getSupportActionBar();

actionBar.setHomeButtonEnabled(true);

actionBar.setDisplayShowHomeEnabled(true);

actionBar.setIcon(R.mipmap.ic_launcher);

这个应用的图标现在将是一个一欧元硬币,而不是一个标准的 Android 图标。按 Ctrl+K | Cmd+K 并提交一条创建启动器图标的消息。

摘要

本章展示了 Android 如何扩大视图,以及R.java文件如何充当资源和 Java 源文件之间的桥梁。您学习了如何从包中解包一个值,并实现了菜单和编码它们的行为。您使用了一个ArrayAdapter将一个字符串数组绑定到微调器。您还了解了如何使用 Android Studio 将视图事件的处理委托给封闭活动。您学习了如何使用共享的偏好设置和资源。您学习了 Android 中的并发性——特别是关于AsyncTask的方法。您还实现了自己的CurrencyConverterTask,它从openexchangerates.org web 服务中获取货币汇率。最后,您使用 Android Studio 生成图像资源,并创建了一个按钮选择器。

我们已经完成了上一章开始的货币应用。通过按下 Shift+F10 | Ctrl+R 运行您的应用,并确保其正常运行。如果你是一个经验丰富的 Android 开发人员,或者只是一个特别好奇的 UI 测试人员,你可能会注意到有一个角落情况会导致应用崩溃。我们将把这个 bug 留在原处,并在第十一章中修复它,这一章专门用于分析和测试。

十一、测试和分析

测试是任何软件开发生命周期中的关键阶段。在一些商店中,质量保证团队负责编写和维护测试,而在其他商店中,开发团队必须执行这项任务。在这两种情况下,随着应用变得越来越复杂,测试的需求也变得越来越重要。测试允许团队成员识别应用的功能问题,这样他们就可以放心地继续工作,因为他们知道他们在源代码中所做的任何更改都不会导致运行时错误、错误的输出和意外的行为。当然,即使是最彻底的测试也不能消除所有的错误,但是测试是软件开发团队的第一道防线。

测试在软件开发人员中是一个有争议的问题。所有开发人员可能都会同意需要进行一些测试。然而,有些人认为测试是如此重要,以至于它应该在开发阶段之前(一种被称为测试驱动开发的方法),而在其他商店,特别是初创公司,有些人试图创建一个最小可行的产品,因此认为测试是一种潜在的浪费,只能有节制地进行。无论你对测试的看法如何,我们鼓励你熟悉本章所涉及的技术,包括android.test库中的类,以及 Android Studio 和 Android SDK 附带的工具。

我们选择了那些我们认为对 Android 开发者最有用的工具。在这一章中,我们介绍仪器测试;然后向您展示 Monkey,它是 Android SDK 附带的一个优秀工具,可以生成随机 UI 事件,用于对您的应用进行压力测试;最后,我们将向您展示 Android Studio 中的一些分析工具。

Tip

有一个很好的第三方测试框架叫做 Roboelectric。虽然 Roboelectric 并没有为我们在这里讨论的 Android SDK 测试框架提供任何明显的好处,但它仍然受到 Android 开发人员的欢迎。你可以在这里找到更多关于 Roboelectric 的信息:robo electric。org

创建新的检测测试

仪器测试允许您在设备上执行操作,就像人类用户在操作它一样。在本节中,您将通过扩展android.test.ActivityInstrumentationTestCase2类来创建一个插装测试。

从第十章中打开货币项目,将你的项目工具窗口切换到 Android 视图。在项目工具窗口中,右键单击(在 Mac 上按住 Ctrl 键并单击)该com.apress.gerber.currencies(androidTest)包,然后选择“新建➤ Java 类”。将您的类命名为 MainActivityTest,扩展ActivityInstrumentationTestCase2<MainActivity>.定义一个构造函数,如图 11-1 所示。您会注意到ActivityInstrumentationTestCase2<>的泛型参数是MainActivity,这是这里测试的活动。

A978-1-4302-6602-0_11_Fig1_HTML.jpg

图 11-1。

Define a class called MainActivityTest, which extends ActivityInstrumentationTestCase2

定义 SetUp()和 TearDown()方法

将光标放在MainActivityTest的类范围内,再次按 Alt+Insert | Cmd+N 调用生成上下文菜单,如图 11-2 所示。选择设置方法,然后按 Enter 键。对拆卸方法重复此过程。框架代码应该如图 11-3 所示。setUp()tearDown()方法是该仪器测试的生命周期方法。setUp()方法为您提供了一个机会,可以连接到任何需要的资源,通过一个包传递任何数据,或者在运行测试之前分配引用。在测试方法运行之后,tearDown()方法可以用来关闭任何连接和清理任何资源。

A978-1-4302-6602-0_11_Fig3_HTML.jpg

图 11-3。

SetUp and TearDown skeleton code

A978-1-4302-6602-0_11_Fig2_HTML.jpg

图 11-2。

Generate SetUp and TearDown methods

打开MainActivity.java文件,这是我们将要测试的活动,并检查onCreate()方法。在每一项活动中—MainActivity也不例外—onCreate()生命周期法是你获得膨胀视图参考的机会。例如,在MainActivity中,第mCalcButton = (Button) findViewById(R.id.btn_calc);行将找到在堆上实例化并由R.id.bnt_calc ID 标识的视图,将其转换为Button,并将该引用分配给mCalcButton

MainActivityTest中,我们将以几乎完全相同的方式引用MainActivity的视图。然而,由于findViewById()Activity的一个方法,而不是ActivityInstrumentationTestCase2,为了做到这一点,我们需要引用MainActivity。在MainActivityTest中定义一个名为MainActivity mActivity;的引用,以及其他引用,如图 11-4 所示。ActivityInstrumentationTestCase2<MainActivity>类有一个名为getActivity()的方法,该方法返回对MainActivity的引用。在将MainActivity引用传递给MainActivityTest的构造函数时,MainActivity中的视图已经被展开。一旦我们有了这个引用,我们就可以调用mActivity.findViewById()来获取我们的引用,如图 11-4 所示。

A978-1-4302-6602-0_11_Fig4_HTML.jpg

图 11-4。

Define the members and body of the setUp( ) method

按 Ctrl+K | Cmd+K 并提交,同时显示一条消息,提示获取对 MainActivity 中展开视图的引用。请记住,在正常情况下,MainActivity是从SplashActivity启动的,它获取有效货币代码并将其存储在一个ArrayList<String>中,然后将该ArrayList<String>打包成一个包,然后通过一个意向将该包穿梭到MainActivity。我们可以不借助SplashActivity来模拟这一切。重新创建代码,如图 11-5 所示。在第setActivityIntent(intent)行中,我们向MainActivity输入了测试数据——如果MainActivity在正常情况下被SplashActivity调用,它将会得到相同类型的数据。

A978-1-4302-6602-0_11_Fig5_HTML.jpg

图 11-5。

Simulate the work of SplashActivity by passing a loaded intent into MainActivity

在 MainActivity 中定义回调

在大多数情况下,您的插装测试将在 UI 线程上进行,而不需要修改测试中的活动。然而,在我们的例子中,我们希望在CurrencyConverterTask在后台线程上完成工作后测试应用的状态。为此,我们需要在MainActivity中定义一个回调。

打开MainActivity.java,定义实例、接口和设置器,如图 11-6 所示。同样,在CurrencyConverterTaskonPostExecute()方法的最后,根据图 11-7 添加代码。按 Ctrl+K | Cmd+K 并在 MainActivity 中提交定义回调的消息。

A978-1-4302-6602-0_11_Fig7_HTML.jpg

图 11-7。

Add an if block of code to the end of CurrencyConverterTask

A978-1-4302-6602-0_11_Fig6_HTML.jpg

图 11-6。

Define an interface in the MainActivity.java class

定义一些测试方法

返回到MainActivityTest.java。将光标放在类范围内。重新创建名为proxyCurrencyConverterTask()convertToDouble()的方法,如清单 11-1 所示。您需要解决一些导入问题。proxyCurrencyConverterTask()方法允许您用数据填充微调器,模拟单击 Calculate 按钮,并在测试从服务器返回的数据是否准确之前等待来自服务器的响应。

Listing 11-1. Create Method to Simulate CurrencyConverterTask and Wait for Termination

public void proxyCurrencyConverterTask (final String str) throws Throwable {

final CountDownLatch latch = new CountDownLatch(1);

mActivity.setCurrencyTaskCallback(new MainActivity.CurrencyTaskCallback() {

@Override

public void executionDone() {

latch.countDown();

assertEquals(convertToDouble(mConvertedTextView.getText().toString().substring(0, 5)),convertToDouble( str));

}

});

runTestOnUiThread(new Runnable() {

@Override

public void run() {

mAmountEditText.setText(str);

mForSpinner.setSelection(0);

mHomSpinner.setSelection(0);

mCalcButton.performClick();

}

});

latch.await(30, TimeUnit.SECONDS);

}

private double convertToDouble(String str) throws NumberFormatException{

double dReturn = 0;

try {

dReturn = Double.parseDouble(str);

} catch (NumberFormatException e) {

throw e;

}

return dReturn;

}

再次将光标放在类范围内的proxyCurrencyConverterTask()方法下面,然后按 Alt+Insert | Cmd+N 调用生成上下文菜单。选择测试方法并按 Enter 键。将您的方法命名为testInteger()并重新创建如图 11-8 所示的方法,包括将Exception替换为Throwable。对名为testFloat()的测试方法重复这些步骤。

A978-1-4302-6602-0_11_Fig8_HTML.jpg

图 11-8。

Create test methods. Pass a nonnumeric value such as “12..3” or “12,,3” into proxyCurrencyConverterTask( )

在这两种测试方法中,我们将大部分行为委托给了proxyCurrencyConverterTask()方法。请记住,为了让您的测试方法被ActivityInstrumentationTestCase2识别,它必须以小写的test开头。

testInteger()中,我们用整数 12 的字符串表示填充mAmountEditText,并用对应于EUR|Euro的货币数组索引设置mForSpinnermHomSpinner。然后我们通过调用performClick()方法来模拟点击mCalculateButton。我们使用一种叫做CountDownLatch的机制,它被设置为在我们从服务器获取汇率时暂停当前线程。一旦MainActivityCurrencyConverterTask的线程终止,CurrencyConverterTask将调用executionDone(),释放挂起的CountDownLatch,允许ActivityInstrumentationTestCase2继续调用assertEquals()。由于本币和外币都被设置为EUR,输出应该与输入相同。我们在这里创建的插装测试使用了 JUnit 框架;因此,如果assertEquals()方法评估为true,我们的测试将通过。

testFloat()方法中,我们模拟与前面描述的相同的过程,尽管我们用非数字数据填充mAmountEditText( 12..3).尽管我们通过将mAmountEditText的软键盘设置为仅允许数字输入来约束用户,但是我们的用户仍然有可能连续输入两个小数点,这就是我们在这里测试的场景。按 Ctrl+K | Cmd+K,并提交一条创建代理方法的消息。

Note

在某些语言中,逗号被用来代替小数点后面的句号。如果您的设备的默认语言设置为这种语言,您的软键盘将显示逗号而不是句号。您可以简单地测试(12,,3)而不是(12..3).

运行仪器测试

在项目工具窗口中右键单击(在 Mac 上按住 Ctrl 键单击)该MainActivityTest类,并从上下文菜单中选择 Run。您也可以从工具栏中 Run 按钮左侧的组合框中选择 MainActivityTest,然后按 Run 按钮。Android Studio 会显示运行工具窗口,控制台会显示你的进度。你的testFloat()方法应该会失败,你会看到一个红色的进度条,如图 11-9 所示。注意,抛出的异常被称为java.lang.NumberFormatException。将该值从 12..3 到 12.3(或者,如果您的语言使用逗号而不是句点来表示小数点,则从 12,,3 到 12,3),然后再次运行它。您的测试现在应该成功了,您应该会看到一个绿色的进度条,如图 11-10 所示。按 Ctrl+K | Cmd+K 并提交,同时显示一条创建检测测试的消息。

A978-1-4302-6602-0_11_Fig10_HTML.jpg

图 11-10。

All tests succeeded

A978-1-4302-6602-0_11_Fig9_HTML.jpg

图 11-9。

Failed testFloat( ) method

修复错误

您刚刚运行的失败测试突出了您的代码中的一个问题。即使键盘被设置为只接受数值,小数点也可能被输入多次,这将导致当 Android 试图将诸如"12..3"的字符串值转换为 double 时出现NumberFormatException。在调用CurrencyConverterTask之前,您需要验证用户输入的数据是数字。在MainActivity.java中,创建名为isNumeric()的方法,如清单 11-2 所示。

Listing 11-2. The isNumeric() Method to Be Used to Verify Input from the User

public static boolean isNumeric(String str)

{

try{

double dub = Double.parseDouble(str);

}

catch(NumberFormatException nfe) {

return false;

}

return true;

}

修改mCalcButtononClick()方法,使我们在执行CurrencyConverterTask之前验证输入数据是数值,如图 11-11 所示。

A978-1-4302-6602-0_11_Fig11_HTML.jpg

图 11-11。

Modify the onClick( ) method so that we verify the input value of mAmountEditText with isNumeric( )

祝贺您——您刚刚创建了一个插装测试,用它来识别一个 bug,然后在源代码中修复了这个 bug。按 Ctrl+K | Cmd+K,并通过验证输入是数字来提交一条修复 bug 的消息。

使用猴子

Android SDK 附带了一个非常好的工具,叫做 Monkey,也称为 UI/应用练习器 Monkey。这个工具允许你在你的应用上生成随机的 UI 事件,就像一只猴子在使用它一样。Monkey 对你的应用进行压力测试很有用。Monkey 的文档可以在developer.android.com/tools/help/monkey.html找到。

Note

除了 Monkey 之外,一个名为 MonkeyRunner 的工具允许您创建和运行 Python 脚本来自动化您的应用进行测试。MonkeyRunner 和猴子没有血缘关系。此外,MonkeyRunner 要求您知道如何使用 Python 编写脚本,这超出了本书的范围。如果您有兴趣了解更多关于 MonkeyRunner 的信息,请参阅位于developer.android.com/tools/help/monkeyrunner_concepts.html的文档。

首先在 Android Studio 中打开一个终端会话,方法是按下位于 IDE 底部边缘的终端窗口按钮。在工具栏的组合框中选择应用,然后单击绿色的运行按钮,启动货币应用。一旦应用运行并空闲,向终端会话发出以下命令,然后按 Enter 键,如图 11-12 所示:

A978-1-4302-6602-0_11_Fig12_HTML.jpg

图 11-12。

Open a terminal session, type the monkey command, and then press Enter

adb shell monkey -p com.apress.gerber.currencies -v 2000

从这个命令中您会注意到的第一件事是 Monkey 正在使用adb,即 Android Debug Bridge,它允许您与运行设备的操作系统外壳进行交互。如果您在发出此命令之前忘记启动您的应用,Monkey 将不会工作。-p开关告诉 Monkey 将其随机 UI 事件约束到com.apress.gerber.currencies包中。-v开关告诉 Monkey 以详细的方式报告事件和异常;如果 Monkey 抛出了一个异常,如果报告很详细,那么跟踪这个异常会更容易。最后一个参数(2000)是事件的数量。两千个随机的 UI 事件应该可以暴露出 UI 的任何问题,您可以根据需要随时运行这个命令。

Caution

当运行 Monkey 时,即使将 Monkey 的 UI 事件约束到一个特定的包中,您也有可能意外地更改设备的默认设置。例如,Monkey 翻转你的 Wi-Fi 或更改手机默认语言的情况并不少见。

使用分析工具

Android SDK 附带的分析工具叫做 Lint。不久前,开发人员还被要求从命令行调用这个工具。幸运的是,Lint 现在已经完全集成到 Android Studio 中。Lint 将分析您的源代码、XML 文件和其他素材,以寻找潜在的错误、未使用的资源、低效的布局、硬编码的文本和其他与 Android 相关的潜在问题。更重要的是,Android Studio 有自己的分析工具,可以对 Java 和 Android 语法执行类似的操作,甚至比 Lint 更强大。总之,这个完全集成的工具套件将保持您的代码整洁,希望没有错误。您可以从主菜单栏中的分析菜单访问 Android Studio 的分析工具。

检查代码

检查代码操作是最有用和最全面的分析操作。导航至分析➤检查代码以运行此操作。在弹出的对话框中,选择整个项目单选按钮,点击确定,如图 11-13 所示。等待几秒钟,让 Android Studio 分析你的整个项目,并在检查工具窗口中显示结果,如图 11-14 所示。您会注意到,首先列出了 Android Lint 检查的目录,然后进一步列出了 Android Studio 自己检查的几个目录。

A978-1-4302-6602-0_11_Fig14_HTML.jpg

图 11-14。

Inspection tool window showing results of the Inspect Code operation

A978-1-4302-6602-0_11_Fig13_HTML.jpg

图 11-13。

Select the Whole Project option from the Specify Inspection Scope dialog box

请记住,由 Inspect 代码操作识别的问题可能根本不是严重的问题。因此,不要觉得有义务去解决每一个问题。此外,在极少数情况下,建议的解决方案可能会破坏您的代码或违背您最初的良好意图。所以,你应该把 Lint 和 Android Studio 的分析工具识别出的问题当作建议。

切换打开检查工具窗口中的目录,直到您能够看到单个行项目。当您检查这些行项目时,请注意检查工具窗口右窗格中每个可能问题的摘要。详细信息包括名称、位置、问题概要、问题解决和抑制,如图 11-14 所示。修复一个潜在的问题就像直接点击问题解决标题下的蓝色超文本一样简单;Android Studio 会完成剩下的工作。避免试图修复由 Inspect 代码操作识别的每一个问题。如果你解决了这些问题中的一个,请谨慎操作并测试你的应用,以确保你没有引入新的错误。

分析依赖关系

“分析依赖项”操作同样位于主菜单栏的“分析”菜单中。分析依赖项将检查您的源代码,并自动为您识别任何依赖项。您可以通过检查项目中每个 Java 源文件的 import 语句来手动执行这个操作,但是这很繁琐。Analyze Dependencies 操作为您节省了这种繁琐的工作,并且还识别了每个依赖项的位置。

Android 中的依赖项可能来自各种来源,包括 Java JDK、Android SDK、第三方 JAR 库(如 Apache Commons)和库项目(如脸书)。如果协作开发人员无法编译和运行项目,主要怀疑是缺少依赖项,您可以使用分析依赖项操作来确定可能缺少哪些依赖项。在 Gradle 之前,管理依赖关系是一件大事。自从 Gradle 出现以来,大多数依赖项都是自动为您下载的,Gradle 使得管理依赖项变得容易和可移植。

从主菜单栏中选择分析➤分析相关性。等待 Android Studio 执行操作,在依赖查看器工具窗口查看结果,如图 11-15 所示。浏览左窗格和右窗格中的各个行项目,注意底部窗格突出显示了 Java 源文件中每个依赖项的位置。

A978-1-4302-6602-0_11_Fig15_HTML.jpg

图 11-15。

Analyze Dependencies tool window showing dependency on org.apache.http.HttpEntity.class

分析堆栈跟踪

假设您没有处于调试模式,并且抛出了一个异常,那么追踪它的最好方法是检查 logcat,它是 Android 的日志工具。Logcat 太好了,也太啰嗦了,很容易让你不知所措,这就是为什么你应该使用 Analyze Stacktrace。撤销我们之前做的错误修复。如果您熟悉 Git,您可以恢复最后一次提交。否则,注释掉修复这个错误的代码,如清单 11-3 所示。

Listing 11-3. Comment Out the Bug Fix

mCalcButton.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

// if (isNumeric(String.valueOf(mAmountEditText.getText()))){

new CurrencyConverterTask().execute(URL_BASE + mKey);

//  } else {

//  Toast.makeText(MainActivity.this, "Not a numeric value, try again.", //  Toast.LENGTH_LONG).show();

//  }

}

});

按下主工具栏中的绿色运行按钮,运行货币应用。一旦货币应用启动并准备就绪,输入 12..在mAmountEditText中输入 3(或者 12,3,如果您的语言使用逗号而不是句点),然后按下计算按钮。应用将崩溃,因为 12..3 不是数值。

按 Alt+6 | Cmd+6 激活 Android DDMS 工具窗口。单击 logcat 选项卡,这是 Android DDMS 工具窗口中最左侧的选项卡。按 Ctrl+A | Cmd+A 选择 logcat 窗口中的所有文本,然后按 Ctrl+C | Cmd+C 复制所有文本,如图 11-16 所示。

A978-1-4302-6602-0_11_Fig16_HTML.jpg

图 11-16。

Logcat window with verbose logs and stack trace

选择分析➤分析堆栈跟踪以调用分析堆栈跟踪操作。任何设置到剪贴板的文本现在都将出现在“分析堆栈跟踪”对话框中。点击正常化按钮,然后点击确定,如图 11-17 所示。运行工具窗口将被激活,堆栈跟踪将可见(不包括任何多余的日志)以及显示异常来源的超链接文本,如图 11-18 所示。Analyze Stacktrace 很好地解析和显示了相关的堆栈跟踪,现在可以轻松地对其进行分析了。

A978-1-4302-6602-0_11_Fig18_HTML.jpg

图 11-18。

The Stacktrace window showing only the relevant stack trace and hyperlinks to the exception’s source

A978-1-4302-6602-0_11_Fig17_HTML.jpg

图 11-17。

Analyze Stacktrace dialog box with contents of entire clipboard

您可以使用 Git 来恢复最后一次提交,或者取消对 bug 修复的注释,如清单 11-4 所示。

Listing 11-4. Uncomment the Bug Fix

mCalcButton.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

if (isNumeric(String.valueOf(mAmountEditText.getText()))){

new CurrencyConverterTask().execute(URL_BASE + mKey);

} else {

Toast.makeText(MainActivity.this, "Not a numeric value, try again.", Toast.LENGTH_LONG).show();

}

}

});

摘要

在这一章中,我们已经向你展示了如何使用 Android Studio 中的一些测试和分析工具。我们还向您展示了如何使用测试工具来识别 bug,然后我们继续修复 bug。最后,我们讨论了仪器测试、Monkey、Lint 和 Android Studio 自己的分析工具。

十二、排除故障

你的应用变得越复杂,就越有可能包含错误。没有什么比一个应用崩溃、在特定条件下无法运行或者妨碍了它本来要完成的任务更让用户沮丧的了。一种天真的开发方法是假设您的代码将总是沿着您定义的路径执行。这有时被称为快乐之路。

理解代码在哪里会偏离快乐的道路对于成为一名优秀的软件开发人员至关重要。因为您无法预测开发过程中所有潜在的不愉快路径,所以了解 Android 开发中涉及的各种诊断工具和技术会有所帮助。第十一章涵盖了任何工具;本章详细探讨了调试器,并回顾了其他一些分析工具,您不仅可以使用它们来修复错误,还可以在工作中洞察潜在的弱点。

记录

许多开发人员在 Android 中使用的第一个工具是 Android 登录系统。日志记录是将变量值或程序状态打印到系统控制台的一种方式,可以在程序运行时读取。如果你有编程的背景,你可能对这种技术很熟悉。然而,日志记录在 Android 中的形式与在其他平台上略有不同。第一个差异是您可能习惯于在普通 Java 平台上使用的函数或方法调用。Android 应用在一台机器上开发,但在另一台机器上执行,因此打印输出被藏在运行代码的设备上。

Android 上负责日志消息的框架叫做logger。它捕获各种事件的输出,不限于您的应用,并将该输出存储在一系列循环缓冲区中。循环缓冲区是一种类似链表的数据结构,但除了以串行方式链接其元素之外,它还将其最后一个元素链接到其第一个元素。这些缓冲区包括radio,它包含无线电和电话相关的消息;events,包含系统事件消息,如服务创建和销毁的通知;和main,包含主日志输出。SDK 提供了一组用于检查这些日志消息的编程和命令行工具。查看所有这些事件的日志就像切断消防水管喝一口水一样。因此,您可以使用各种操作和标志来减少输出。

使用 Logcat

从命令行,您可以使用 Logcat,它连接到一个附加设备,并将这些循环缓冲区的内容转发到您的开发控制台。它有多种选项,调用它的语法在表 12-1 中给出。

表 12-1。

Logcat Options and Filters

| `Log Options and Filters` | 描述 | | --- | --- | | `-c` | 清除或刷新日志。 | | `-d` | 将日志转储到控制台。 | | `-f ` | 将日志写入``。 | | `-g` | 显示给定日志缓冲区的大小。 | | `-n ` | 设置旋转日志的数量。默认值为 4。该选项需要`-r`选项。 | | `-r ` | 针对给定的每千字节数旋转日志文件。缺省值是 16,这个选项需要`-f`选项。 | | `-s` | 将默认过滤器设置为静音。 | | `-v ` | 将输出格式设置为以下格式之一:`brief`显示发出消息的进程的优先级、标签和 PID。`process`仅显示 PID。`tag`只显示优先级和标签。`raw`显示原始日志消息,没有任何其他字段。`time`显示发出消息的进程的日期、调用时间、优先级、标签和 PID。`threadtime`:显示每条消息线程的日期、调用时间、优先级、标签、PID 和线程 ID (TID)。`long`显示所有字段,并用空行分隔消息。 | | -b | 显示给定缓冲区的日志输出。缓冲区可以是下列之一:`radio`包含与无线电/电话相关的消息。`events`包含事件相关消息。`main`是主日志缓冲区(默认)。 |

adb logcat [option] ... [filter] ...

日志中的每条消息都有一个标签。标签是一个短字符串,通常代表发出消息的组件。该组件可以是一个View、一个CustomErrorDialog或应用中定义的任何小部件。每条消息还具有相关联的优先级,该优先级决定了该消息的重要性。优先事项如下:

  • V:详细(最低优先级)
  • D:调试
  • I:信息
  • W:警告
  • E:错误
  • F:致命
  • S:静默(最高优先级,日志中的所有内容都被忽略)

您可以通过使用过滤器表达式来控制 Logcat 的输出。使用正确的标志组合将有助于您关注与您的调查相关的输出。过滤表达式采用tag:priority的形式。例如,MyBroadcastReceiver:D将只包含来自MyBroadcastReceiver组件的日志消息,这些消息被标记为调试优先级。

Android Studio 包括一个内置的设备 Logcat 查看器,它通过使用图形控件来处理命令行的细节。插入您的设备或启动模拟器,然后单击 IDE 底部的数字 6 选项卡打开 DDMS 浏览器。如果尚未选择,选择Devices | Logcat选项卡。你的屏幕应该如图 12-1 所示。

A978-1-4302-6602-0_12_Fig1_HTML.jpg

图 12-1。

The Android DDMS tool window

在该视图的右上角,您将看到三个重要的过滤器控件。日志级别下拉列表按优先级控制过滤。在图 12-1 中,该选项设置为 Verbose,记录所有消息。将日志级别设置为 Debug 将包括 Debug 优先级或更高优先级的所有消息。该下拉列表旁边是一个手动文本输入控件,它将消息限制为只包含您在此处键入的文本。清除条目会清除过滤器。下一个下拉列表包括一组预设过滤器和一个编辑或更改这些预设的选项。单击编辑过滤器配置以打开创建新的 Logcat 过滤器对话框。该对话框如图 12-2 所示,包括修改任何预设过滤器的控件。

A978-1-4302-6602-0_12_Fig2_HTML.jpg

图 12-2。

The Create New Logcat Filter dialog box

您还可以添加、更改或删除任何自定义过滤器。这些预设可以通过标签、包名、进程 ID (PID)和/或日志级别进行过滤。

写入 Android 日志

当你的应用运行时,你可能想知道一个方法实际上正在执行,这个执行使得它通过了方法中的某个点,或者某些变量的值。SDK 在一个名为android.util.Log的类上定义了静态方法,您可以用它来写入日志。这些方法使用短名称— vdIwef—对应于详细、调试、信息、警告、错误和致命优先级。每个方法都有一个标签、一个消息字符串和一个可选的 throwable。您选择的方法决定了与您提供的消息相关联的优先级。例如,下面的片段是您可能在活动中找到的日志。它将记录带有调试优先级的文本onCreate(),同时使用类名作为标记:

protected void onCreate(Bundle savedInstanceState) {

Log.d(this.getClass().getSimpleName(), "onCreate()");

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

捕虫游戏。

大多数开发人员主要关注于编写有效的软件。本节向您介绍一个不工作的应用!它是故意带着问题写的,作为调试的练习。这个简单的数学测试应用有几个文本输入字段,用于输入任意数字。运算符下拉列表允许您选择加法、减法、乘法和除法。在底部的文本输入字段中,您可以尝试回答您构建的数学问题。复选按钮使您能够检查答案。通读清单 12-1 中的代码,看看它是如何工作的。

Listing 12-1. The DebugMe App

<FrameLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

xmlns:android="``http://schemas.android.com/apk/res/android

android:background="@android:color/black">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="Math Test"

android:id="@+id/txtTitle"

android:layout_gravity="center_horizontal|top"

android:layout_marginTop="10dp"

android:textColor="@android:color/white" />

<RelativeLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:layout_gravity="center">

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/editItem1"

android:text="25"

android:layout_above="@+id/editItem2"

android:layout_centerHorizontal="true"

android:layout_alignStart="@+id/editItem2"

android:textColor="@android:color/white" />

<Spinner

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/spinOperator"

android:layout_centerVertical="true"

android:layout_toLeftOf="@+id/editItem2"

android:layout_alignBottom="@+id/editItem2"

android:spinnerMode="dropdown" />

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/editItem2"

android:text="50"

android:layout_centerVertical="true"

android:layout_centerHorizontal="true"

android:layout_margin="25dp"

android:textColor="@android:color/white" />

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="\???"

android:id="@+id/editAnswer"

android:layout_below="@+id/editItem2"

android:layout_centerHorizontal="true"

android:layout_marginLeft="25dp"

android:textColor="@android:color/white" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="="

android:id="@+id/textView"

android:layout_below="@+id/editItem2"

android:layout_toLeftOf="@+id/editAnswer"

android:textColor="@android:color/white" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="25dp"

android:text="Check"

android:onClick="checkAnswer"

android:layout_toRightOf="@id/editAnswer"

android:layout_alignBottom="@id/editAnswer"

android:textColor="@android:color/white" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="The answer is:\nXXX"

android:id="@+id/txtAnswer"

android:layout_below="@+id/editAnswer"

android:layout_centerHorizontal="true"

android:textColor="@android:color/holo_red_light"

/>

</RelativeLayout>

</FrameLayout>

public class MainActivity extends Activity {

private static final int SECONDS = 1000;//millis

private Spinner operators;

private TextView answerMessage;

@Override

protected void onCreate(Bundle savedInstanceState) {

Log.d(this.getClass().getSimpleName(), "onCreate()");

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

answerMessage = (TextView) findViewById(R.id.txtAnswer);

answerMessage.setVisibility(View.INVISIBLE);

operators = (Spinner) findViewById(R.id.spinOperator);

final ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,

R.array.operators_array, android.R.layout.simple_spinner_item);

adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

operators.setAdapter(adapter);

}

public void checkanswer(View sender) {

InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);

imm.hideSoftInputFromWindow(findViewById(R.id.editAnswer).getWindowToken(), 0);

checkAnswer(sender);

}

public void checkAnswer(View sender) {

String givenAnswer = ((EditText) findViewById(R.id.editAnswer)).getText().toString();

int answer = calculateAnswer((EditText) findViewById(R.id.editItem1),

(EditText) findViewById(R.id.editItem2));

final String message = "The answer is:\n" + answer;

if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

eventuallyHideAnswer();

}

private int calculateAnswer(EditText item1, EditText item2) {

int number1 = Integer.parseInt(item1.getText().toString());

int number2 = Integer.parseInt(item2.getText().toString());

int answer = 0;

switch(((Spinner) findViewById(R.id.spinOperator)).getSelectedItemPosition()) {

case 0:

answer = number1 + number2;

break;

case 1:

answer = number1 - number2;

break;

case 2:

answer = number1 * number2;

break;

case 3:

answer = number1 / number2;

break;

}

return answer;

}

private void showAnswer(final boolean isCorrect, final String message) {

if (isCorrect) {

answerMessage.setText("Correct! " + message);

answerMessage.setTextColor(getResources().getColor(android.R.color.holo_green_light));

} else {

answerMessage.setText("Incorrect! " + message);

answerMessage.setTextColor(getResources().getColor(android.R.color.holo_red_light));

}

answerMessage.setVisibility(View.VISIBLE);

}

private void eventuallyHideAnswer() {

final Runnable hideAnswer = new Runnable() {

@Override

public void run() {

answerMessage.setVisibility(View.INVISIBLE);

}

};

answerMessage.postDelayed(hideAnswer,10 * SECONDS);

}

}

我们有一个活动,允许用户尝试解决一个简单的数学问题。onCreate()方法将所有视图组件保存在实例变量中,并将基本操作符(加、减、乘、除)插入到ArrayAdapter中。在调用被覆盖的checkAnswer()方法之前,checkanswer()方法隐藏了键盘,该方法执行检查我们答案的实际工作。这个被覆盖的checkAnswer()方法调用一个calculateAnswer()方法来寻找实际的答案。然后,checkAnswer()方法将答案与给定的答案进行比较,并构建一个答案消息。如果答案与给定的答案匹配,则调用showAnswer(),其中true值指示成功;否则,用false调用showAnswer()。最后,checkAnswer()方法通过调用eventuallyHideAnswer()方法最终隐藏了应答消息,这将发布一个Runnable代码块在 10 秒钟后执行。

当你开始使用这个应用时,你可能不会注意到这些错误,但它们很快就会出现。如果您通读了示例代码,并在运行它之前自己输入了它,您可能会对它明显的弱点很敏感。保留默认答案或尝试回答并点击“检查”按钮。应用会立即崩溃。试着再运行一次。这一次,输入数学题的错误答案,然后点击“检查”按钮。没有可见的反馈告诉你答案是否正确!您可能认为您知道崩溃的根源在哪里,但是我们不会猜测假设的问题,而是尝试使用调试器来正确地隔离 bug。

使用交互式调试器

Android Studio 包括一个交互式调试器,允许您设置断点。您可以通过单击编辑器左侧空白处您想要检查的行来设置断点。请记住,断点必须设置在包含可执行语句的行上;例如,您不能在包含注释的行上设置断点。设置断点时,Android Studio 会在装订线中添加一个粉色圆圈图标,并以粉色突出显示整行。当在调试模式下运行应用,并且程序执行到达断点时,槽中的圆圈变为红色,该行突出显示,执行暂停并进入交互式调试模式。在交互调试模式下,许多应用状态都显示在调试工具窗口中,包括变量和线程。程序的状态可以被详细检查或者甚至被改变。

要开始调试,可以通过单击顶部工具栏中的 bug 图标以调试模式启动程序,也可以单击 bug 图标右侧的图标。这将在程序运行时将调试器附加到程序上(见图 12-3 )。您选择的方法取决于您试图捕捉的问题。您的 bug 可能会在真实世界的条件下出现,因此您需要将设备带到特定的位置或以特定的方式使用它。在这种情况下,将设备连接到电脑可能会不方便。在这些情况下,让您的设备进入 bug 开始显现的状态,然后将设备连接到您的计算机以启动调试器是有意义的。然而,如果 bug 发生在应用启动的早期,以调试模式启动可能是有意义的,这样应用启动时可以立即暂停执行。在第三种方法中,你可以从 Android 设备设置中将一个应用设置为可调试的,并让它等待调试器连接。当您试图发现应用启动时出现的问题,但不想上传和替换设备上已安装的实际应用时,这很有帮助。

A978-1-4302-6602-0_12_Fig3_HTML.jpg

图 12-3。

Attach the debugger while running

我们将从在MainActivity中每个方法的第一行添加断点开始。当您不确定问题的确切位置并且没有太多方法时,这种方法非常有效。但是,它不会随着应用复杂性的增加而扩展。单击左边距中的装订线,在每个方法的第一行添加断点。你应该会看到类似于图 12-4 的东西。

A978-1-4302-6602-0_12_Fig4_HTML.jpg

图 12-4。

Add breakpoints to each method in the MainActivity class

单击运行➤调试应用,等待 Android Studio 在设备上构建并启动您的应用。当应用启动时,您会看到一个简短的对话框,指示 adb (Android Debug Bridge)正在等待调试器连接,然后 IDE 才会建立连接。然后 Android Studio 最终会高亮显示(蓝色)在onCreate()方法行的第一个断点,如图 12-5 所示。调试工具窗口将会打开,如果您在等待断点时碰巧正在运行另一个程序,IDE 甚至会请求焦点并跳到屏幕的前面。这可能很方便,但如果你碰巧使用社交网络或聊天应用,会造成干扰,因为你的击键可能会进入编辑器并破坏你的代码,所以要小心!

A978-1-4302-6602-0_12_Fig5_HTML.jpg

图 12-5。

Execution stops at a breakpoint and highlights it blue

当第一个断点显示为蓝色时,“调试工具”窗口从底部窗格打开,此时您可以开始检查程序的状态。“调试工具”窗口具有一些功能,您可以使用这些功能深入到执行和控件的不同区域,您可以使用这些功能单步执行、单步退出和单步跳过方法。当前行恰好是对Log.d()方法的调用,该方法向 Logcat 发送一行文本。单击 Logcat 选项卡显示日志,然后单击 Step Over 按钮A978-1-4302-6602-0_12_Figf_HTML.jpg执行 log 语句。Logcat 显示日志信息,执行移动到下一行,如图 12-6 所示

A978-1-4302-6602-0_12_Fig6_HTML.jpg

图 12-6。

The Logcat view shows the log message after stepping over

单击调试器选项卡以显示变量视图。在这个视图下,您应该看到三个变量:thissavedInstanceanswerMessage。点击this变量旁边的三角形,展开与this对象相关的所有变量。this对象总是表示正在执行的当前类,所以当前文件中的所有实例变量在您深入查看时都是可见的。您还会看到许多其他实例变量,每个都是从父类派生的。筛选如此多的变量可能有些乏味,但它有助于理解您当前正在调试的类的结构。折叠this变量,再点击两次单步执行,将执行点移动到answerMessage的赋值处。请注意,在 variables 视图中突然出现了操作符的实例变量。当执行点接近实例变量的赋值时,它们开始显示在 variables 视图中。

评估表达式

在运行将设置answerMessage变量的赋值语句之前,您可以分解这一行,看看在它发生之前会有什么赋值。单击并拖动选择到findViewById(R.id.txtAnswer)表达式上,然后按 Alt+F8。你会看到一个类似于图 12-7 的对话框。

A978-1-4302-6602-0_12_Fig7_HTML.jpg

图 12-7。

Using the Evaluate Expression dialog box

该表达式将被复制到“表达式求值”对话框中,并且可以独立于该行的其余部分执行。该对话框接受任何 Java 代码片段,并显示其评估结果。单击“求值”(或按 Enter 键,因为默认情况下选择了“求值”)来求值并执行表达式。对话框最终会被表达式的结果填充,你可以看到代表TextView的对象,它保存了答案文本。当你检查答案时,你最终会看到同样的TextView。结果是一个以扩展形式显示的对象,它提供了关于TextView状态的大量信息。您可以检查内部的mText属性、文本颜色、布局参数等等。在表达式后面追加一个getVisibility()方法调用,如图 12-8 所示。

A978-1-4302-6602-0_12_Fig8_HTML.jpg

图 12-8。

Examine the answer EditText’s visibility in the Evaluate Expression dialog box

findViewById(R.id.txtAnswer).getVisibility()表达式的结果为 0,等于View.VISIBLE常数。记住常量的值可能很困难,但您可以在表达式计算器中使用任何表达式。这意味着通过使用如下表达式,您可以直接询问 Android Studio,“我的视图可见吗?”

findViewById(R.id.txtAnswer) == View.Visible

前面一行代码的结果将是true;但是,尝试跳过接下来的两行,执行将视图设置为不可见的那一行。按 Alt+F8 再次调出“表达式计算器”对话框,并使用向下箭头键在您计算过的早期表达式中循环,找到“我的视图可见吗?”表情。此时,结果应该是false,在意料之中。这个想法是隐藏答案,直到点击检查按钮。逐行单步执行语句可以让您了解实际发生了什么,而使用表达式计算器可以让您在程序运行时确认变量或表达式的值。

单击调试器左侧控制面板中的运行按钮,恢复正常执行。app 会继续完成onCreate()方法,以正常速度运行,直到到达另一个断点。在onCreate()完成之后,用户界面应该在您的设备或模拟器上呈现。至此,我们可以开始解决实际问题了。当你试图检查数学问题的给定答案时,第一个问题出现了。键盘永远不会隐藏,答案永远不会透露。点击问号激活答案字段TextEdit控件,清除它,并键入任何数字。接下来点击检查按钮。即使在第一个checkanswer()方法的开头有一个断点,执行也会在第一行的checkAnswer()方法处暂停。这里的意图是第一个checkanswer()方法应该在有逻辑隐藏键盘的地方被调用。这个方法然后调用第二个checkAnswer()方法来完成验证输入的实际工作。因为没有调用第一个方法,所以键盘保持可见!

现在您已经知道了这个问题的原因,让我们检查一下代码的其他部分,看看为什么这个方法没有被调用。我们的例子使用activity_main布局文件中的onClick属性将按钮连接到方法。打开activity_main布局文件,你会找到根本原因。Check 按钮的onClick属性被设置为checkAnswer(使用大小写混合版本),而你确实希望onClick属性调用checkanswer(全小写版本)。忽略使用仅大小写不同的两个方法名的明显不良模式,修复在android:onClick属性中的调用,将其设置为checkanswer。现在,单击左侧控制窗格中的调试器停止按钮。这将分离调试器,并允许程序恢复正常执行。构建并再次运行应用以查看结果。您应该会看到类似于图 12-9 的内容。

A978-1-4302-6602-0_12_Fig9_HTML.jpg

图 12-9。

The keypad is dismissed, and the answer’s TextView is visible

使用堆栈跟踪

您通过使用交互式调试器发现并修复了两个错误。然而,更多的问题存在。如果您再次启动应用并立即点击检查按钮,应用将会崩溃。您可以使用交互式调试器来查找根本原因,也可以跟踪堆栈跟踪。堆栈跟踪是崩溃时堆栈上每个方法的转储,包括行号。堆栈引用一系列方法,每个方法都由它之前的方法调用。Java 将程序错误表示为ExceptionThrowable对象。这些特殊的对象携带关于错误原因的元数据以及错误发生时的程序状态。异常沿着程序堆栈向上传播到调用方法及其父调用方,直到它们被捕获和处理。如果它们没有被发现和处理,它们会一直传播到操作系统,使你的应用崩溃。要想清楚,最好看一个例子。触发崩溃,然后立即在 Android DDMS 工具窗口下的 logcat 窗口中查找堆栈跟踪。

Listing 12-2. The Stack Trace Produced When Check Is Tapped

03-08 20:10:56.660    9602-9602/com.apress.gerber.debugme E/AndroidRuntime: FATAL EXCEPTION: main

Process: com.apress.gerber.debugme, PID: 9602

java.lang.IllegalStateException: Could not execute method of the activity

at android.view.View$1.onClick(View.java:3841)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.reflect.InvocationTargetException

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at android.view.View$1.onClick(View.java:3836)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.NumberFormatException: Invalid int: "???"

at java.lang.Integer.invalidInt(Integer.java:137)

at java.lang.Integer.parse(Integer.java:374)

at java.lang.Integer.parseInt(Integer.java:365)

at java.lang.Integer.parseInt(Integer.java:331)

at com.apress.gerber.debugme.MainActivity.checkAnswer(MainActivity.java:46)

at com.apress.gerber.debugme.MainActivity.checkanswer(MainActivity.java:39)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at android.view.View$1.onClick(View.java:3836)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

堆栈跟踪可能会很长,这取决于你的应用的复杂性,但是学习导航它们是一项有价值的技能,可以添加到你的武器库中。在前面的堆栈跟踪中,您将看到各种方法名称和行号一起列出。第一个列出的方法View$1.onClick被认为是堆栈的顶部,是最近调用的方法。方法名旁边是一个行号,指向发生异常的源代码的实际行。因为这个类不是我们作为示例的一部分编写的代码,所以您必须深入查看堆栈。当您向下查看堆栈时,您会看到以Caused By开头的条目。阅读的方式如下:你有一个异常,这个异常是由一个异常引起的,以此类推。如果你读了最后一个原因,你会发现实际的问题,Invalid int: "???"。系统抱怨您向Integer.java中的InvalidInt方法传递了一个无效的整数值,一系列问号。这是 Android 运行时的一部分,不受你的控制。但是,如果您继续阅读,您会看到invalidInt被更多的 Java 运行时方法调用,这些方法实际上是由checkAnswer调用的,这些方法在MainActivity.java中。您可以单击 Logcat 视图中的行号,这将直接跳转到下面代码片段中指示的位置:

if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

eventuallyHideAnswer();

此时,在点击 Check 之后,我们将把givenAnswer变量传递给Integer.parseInt方法。在同一方法的前几行,您将看到以下初始化 givenAswer 变量的代码:

String givenAnswer = ((EditText) findViewById(R.id.editAnswer)).getText().toString();

来自EditText控件的文本值存储在givenAnswer字符串变量中。在将值转换为数字之前,您应该检查它是否确实是一个数字,以防止系统崩溃。将调用Integer.parseIntif块改为使用以下if / else if逻辑:

if(! isNumeric(givenAnswer)) {

showAnswer(false, "Please enter only numbers!");

} else if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

接下来定义isNumeric方法如下:

private boolean isNumeric(String givenAnswer) {

String numbers = "1234567890";

for(int i =0; i < givenAnswer.length(); i++){

if(!numbers.contains(givenAnswer.substring(i,i+1))){

return false;

}

}

return true;

}

isNumeric()方法根据所有数字的列表测试每个字符。如果该方法返回false,那么修改后的if块将调用showAnswer(),并提示用户只输入数字。准备就绪后,再次尝试运行该应用。轻按“检查”按钮,不更改带问号的默认答案。崩溃行为应该被重点关注。代码中还有一个故意放置的错误,这可能会导致崩溃。我们稍后解释。使用该应用来解决一些数学问题与一些其他运营商除了揭露崩溃。花一点时间用你所学的来看看你是否能找到它。

本节介绍了调试的基础知识。现在,您将深入探索交互式调试器,并访问它的更多特性。在第十一章中,我们讨论了分析堆栈跟踪工具,它可以帮助你解析长堆栈跟踪。

探索交互式调试器的工具窗口

调试器工具窗口包括跟踪执行时逐句进入代码行的控件。默认情况下,“框架”选项卡显示调用堆栈。调用堆栈是被调用以到达当前断点的方法调用的堆栈。在这些堆栈中,最后调用的方法在顶部,而调用它的方法就在下面。属于 Android 运行时的方法被涂成黄色,以区别于项目中定义的方法,后者被涂成白色。

图 12-10 描述了一个调用栈,并关注两个项目方法。在这个例子中,checkAnswer()方法调用了calculateAnswer()方法,所以calculateAnswer()方法在栈顶。

A978-1-4302-6602-0_12_Fig10_HTML.jpg

图 12-10。

Use the frames view to examine the call stack

跳过按钮A978-1-4302-6602-0_12_Figa_HTML.jpg跳过当前行,到下一行。当前行上的所有指令,包括任何方法调用,都被立即执行。当它到达下一行时,应用将暂停。

单步执行按钮A978-1-4302-6602-0_12_Figb_HTML.jpg执行当前行上的所有指令,直到该行上的第一个方法调用。执行在第一个方法调用的第一行暂停。如果一行中出现多个方法调用,则遵循 Java 定义的正常操作顺序:从左向右执行,嵌套方法首先执行。不考虑在项目外部的类中定义的方法(比如第三方 JAR 文件,以及内置的 Java 和 Android API 方法)。这些方法的执行步骤。

Force Step Into 按钮A978-1-4302-6602-0_12_Figc_HTML.jpg的行为类似于 Step Into 按钮,除了外部定义的方法,如 Android SDK 中定义的方法,也被单步执行。

步出按钮A978-1-4302-6602-0_12_Figd_HTML.jpg完成当前方法中所有指令的执行,并步出该方法,到调用堆栈中的前一个调用方法。执行在调用方法后的下一行代码处暂停。

Show Execution Point A978-1-4302-6602-0_12_Fige_HTML.jpg按钮将您导航到执行当前暂停的位置。有时,您可能会在调试时远离断点并深入代码。通过使用导航中介绍的一些高级功能,您可以进入各种方法调用或探索类的调用者。这种探索可能会导致您丢失最初跟踪的方法的上下文。此选项允许您快速重新校准并从开始的地方开始。

使用断点浏览器

点击运行查看断点打开断点对话框,如图 12-11 所示。此对话框概述了您在应用中创建的所有断点。如果双击列表中的任何断点,IDE 将跳转到源代码的该行。选择任何断点都会在右侧视图中显示其详细信息。详细视图使您能够禁用断点,并控制当执行到达断点时应用暂停的方式和时间。这个视图充满了强大的选项,允许您微调断点的行为。你有能力运行任意程序语句,有条件地在感兴趣的点暂停应用,甚至控制其他断点的执行。

A978-1-4302-6602-0_12_Fig11_HTML.jpg

图 12-11。

Set breakpoint properties with the Breakpoints dialog box

视图中的第一个复选框启用和禁用断点。“挂起”复选框控制到达断点时的执行行为。如果未选中此复选框,断点将被完全禁用,并且在运行时对应用没有影响。当与其他一些选项(如 Log Evaluated Expression 选项)结合使用时,此功能特别有用。Suspend 选项旁边的单选按钮将导致断点分别挂起整个应用或当前线程。这是一项高级功能,有助于调试行为难以遵循的多线程应用。

Condition 选项允许您指定断点处于活动状态的条件。该下拉列表接受任何计算结果为布尔值的有效 Java Android 代码表达式。表达式中使用的代码在定义断点的方法的上下文中执行。因此,代码可以访问在定义它的方法中可见的任何变量。它遵循 Java 语法规则来确定作用域,您可以参考这些规则来获得关于变量可见性的更多细节。当条件为false时,断点被忽略。当条件为true时,执行将在到达断点时暂停。

每次到达断点时,“将消息记录到控制台”选项都会向调试控制台发出一条通用日志消息。这个通用消息包括方法的完全限定名和行号的可点击引用。若要查看实际效果,请检查当前在“断点”对话框中设置的每个断点。取消选中“暂停”复选框,并为每个选项选中“将消息记录到控制台”复选框。在应用运行的情况下,点击 Check 按钮触发对checkanswer()的呼叫。激活调试器工具窗口中的“控制台”选项卡,以查找来自调试器的日志消息。

Log Evaluated Expression 选项包括一个文本输入字段,它接受任何有效的 Java 代码语句。每当到达断点时,就会执行下拉列表中的代码,并将代码评估结果写入调试控制台。与 Condition 选项非常相似,这段代码在定义它的方法的上下文中运行。代码遵循与 Condition 选项相同的变量可见性规则。通常,您会指定一个计算结果为字符串的 Java 表达式,但是要知道任何 Java 语句都可以被计算,甚至是 Java 赋值语句。这使您能够在应用运行时插入代码,甚至改变行为!

Remove Once Hit 选项允许您定义自毁断点。当在一个紧密的循环中使用时,这是很有用的,在这种循环中,多次点击会使你试图看到的东西变得模糊。

“命中选定断点前禁用”选项允许您将一个断点连接到另一个断点。此选项使当前断点保持禁用状态,直到执行到达此处指定的断点。假设您有一个方法foo,它重复调用另一个方法bar,您正试图调试这个方法。当foo调用bar时,您正试图跟踪它的行为。让事情变得复杂的是,假设其他几种方法也调用bar。您可以在foobar中放置一个断点,然后选择bar的断点并配置此选项以禁用bar,直到到达foo中的断点。

早些时候,我们认为应用中还有一个错误会导致崩溃。这场崩溃可能很明显,也可能不明显。如果输入类似图 12-12 的表达式,就会触发 bug。您可以使用本章介绍的任何功能来调试崩溃。查看堆栈跟踪可以直接找到问题的根源。

A978-1-4302-6602-0_12_Fig12_HTML.jpg

图 12-12。

Try a division problem to find a crash!

switch / case块中的算术表达式需要防止被零除。使用以下代码片段解决崩溃问题:

switch(((Spinner) findViewById(R.id.spinOperator)).getSelectedItemPosition()) {

case 0:

answer = number1 + number2;

break;

case 1:

answer = number1 - number2;

break;

case 2:

answer = number1 * number2;

break;

case 3:

if(number2 != 0) {

answer = number1 / number2;

}

break;

}

条件断点

调试中比较乏味的练习之一是跟踪重复方法调用和循环之间的错误行为。根据逻辑的复杂程度,您可能会花费宝贵的时间逐句通过代码行,等待逻辑出现异常的特定情况。为了节省时间,Android Studio 支持条件断点。这些断点仅在给定的条件下是活动的。为了演示,假设您想要支持数学测试的指数特性。给arrays.xml中的operators_array添加一个指数运算符,如下所示:

<resources>

<string-array name="operators_array">

<item>+</item>

<item>-</item>

<item>x</item>

<item>/</item>

<item>exp</item>

</string-array>

</resources>

因为您已经在数组的索引 4 处添加了 exp,所以您必须向calculateAnswer()方法添加另一个 case 块,如下所示:

case 4:

if (number2!=0) {

answer = 1;

for(int i=0; i <=number2; i++) {

answer = answer * number1;

}

}

break;

您添加的是一个简单的循环,使用第二个数字作为循环计数器,将第一个数字乘以自身。在这一点上,故意的错误对您来说可能是显而易见的,也可能是不明显的。构建并运行应用,尝试解决一个 2 的 8 次方的数学问题。图 12-13 展示了这些变化会给你带来什么。

A978-1-4302-6602-0_12_Fig13_HTML.jpg

图 12-13。

The exponent answer is correct, but the app gives an error

应用错误地将答案计算为 512。您将使用交互式调试器来查找问题。首先,清除所有断点以避免任何不必要的暂停。单击“附加调试器”图标进入交互式调试模式并附加您的调试器。现在,您可以在刚刚添加的 for 循环中间放置一个断点,遍历 8 个循环,看看为什么会得到错误的结果。或者,您可以使用条件断点来查看最后一次迭代中发生了什么。单击装订线以在该行添加断点:

answer = answer * number1;

接下来右击断点,在条件字段中输入表达式 i==8(如图 12-14 所示)。

A978-1-4302-6602-0_12_Fig14_HTML.jpg

图 12-14。

Set a condition for the breakpoint

单击完成关闭弹出窗口,然后点击设备或模拟器上的检查按钮。执行将在断点处暂停,但只有在i计数器增加到 8 之后。在调试工具窗口的变量视图中查看所有变量的状态。number1变量设为 2,number2变量设为 8,答案是 256。但是,此时单击“单步执行”会导致发生额外的乘法运算,从而改变值。预期的行为是循环在第 8 个周期后终止,但事实并非如此。如果您仔细观察 for 循环中的条件,您会看到i如何初始化为 0,以及对i<=number2的检查。你需要检查i<number2,因为i是从 0 开始的。进行更改,然后构建并正常运行应用来测试它。图 12-15 显示了更改后运行的应用。

A978-1-4302-6602-0_12_Fig15_HTML.jpg

图 12-15。

Figure 12-15.

摘要

在本节中,您学习了如何使用 Android Studio 中的各种工具和功能进行调试。您了解了如何在各种级别使用日志记录,以及如何直接在 IDE 中检查 Android logcat。您探索了交互式调试器并研究了它的高级特性。您还在一个损坏的应用中进行了代码潜水,并使用调试工具来查找和修复崩溃。通过代码示例,您已经熟悉了从 stacktraces 导航和设置常规断点和条件断点。本章只讲述了在 Android Studio 中调试的基础知识。您可以创造性地组合调试器中的许多功能,以定制您的体验。您还可以在调试会话中结合 Android Logcat,以便更深入地了解您的应用。