Android Studio 学习手册(四)
十三、级别
当 Android 最初发布时,Google 开发了一个基于 Apache Ant 的构建系统,作为 SDK 的一部分。Ant 是一项成熟的技术,经过多年的改进,拥有一个庞大的贡献者社区。多年来,出现了其他构建系统,其中一些在蓬勃发展的社区中变得流行起来。在这些构建系统中,Gradle 成为 Java 开发的下一个发展阶段。本章探讨了 Gradle,并举例说明了如何最好地使用它来开发和维护您的 Android 应用。
在阐述 Gradle 之前,本章解释了什么是构建系统,以及为什么你可能需要对现有的构建系统进行改进。历史上,创建应用或任何软件的过程都涉及用特定的编程语言编写代码,然后将代码编译成可执行的形式。
Note
本章后面会有一个实验,解释如何在多模块项目中使用 Gradle。我们邀请您使用 Git 克隆这个项目的 Lab,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果您的计算机上没有安装 Git,请参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到 C:\androidBook\reference(如果没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clonehttps://bitbucket.org/csgerber/gradleweather.gitgradle weather。
现代软件开发不仅包括链接和编译,还包括测试、打包和最终产品的发布。构建系统通过提供完成这些任务的必要工具来满足这些紧急需求。考虑许多开发人员今天面临的紧急需求列表:支持最终产品的变体(调试版本、发布版本、付费版本和免费版本),管理作为产品一部分的第三方软件库和组件,以及基于外部因素向整个过程添加条件。
Android 构建系统最初是用 Ant 编写的。它基于一个相当扁平的项目结构,没有为诸如构建变化、依赖性管理和将项目的输出发布到中央存储库之类的事情提供太多的支持。Ant 有一个简单且可扩展的基于 XML 标记的编程模型,尽管许多开发人员觉得它很麻烦。此外,Ant 使用声明性模型。尽管 Ant 遵循函数式编程的一些原则,但许多开发人员对大多数现代编程语言中常见的命令式模型并不陌生。简而言之,不直接支持诸如循环结构、条件分支和可重分配属性(变量的 Ant 等价物)之类的东西。
Gradle build 是用 Groovy 编程语言编写的,它构建在 Java 的核心运行时和 API 之上。Groovy 松散地遵循 Java 的语法,当与 Java 的语法结合时,降低了学习曲线。这增加了 Groovy 的吸引力,因为它非常接近 Java 语言,您可以将大部分 Java 代码移植到 Groovy,只需做很少的更改。这也增加了 Gradle 的优势,因为您可以在 Gradle 构建的任何时候添加 Groovy 代码。由于 Groovy 语法与 Java 如此接近,您实际上可以在 Gradle 构建脚本的中间添加 Java 语法,以达到您想要的效果。Groovy 还在 Java 的语法中加入了闭包。闭包是用花括号括起来的代码块,可以赋给变量或传递给方法。闭包是 Gradle 构建系统的核心部分,稍后您将了解更多。
gradle 语法
Gradle 构建脚本实际上是遵循某些约定的 Groovy 脚本文件。因此,您可以在构建中包含任何有效的 Groovy 语句。然而,大多数都是由遵循基于块的简单语法的语句组成的。Gradle 构建脚本的基本结构包括配置和任务块。任务块定义了在构建过程中不同时间点执行的代码。配置块是特殊的 Groovy 闭包,它在运行时向底层对象添加属性和方法。您可以在 Gradle 构建脚本中包含其他类型的块,但这超出了本书的范围。您将主要使用配置块,因为 Gradle Android 构建中涉及的任务已经定义好了。配置块采用以下形式:
label {
//Configuration code...
}
其中label是特定对象的名称,花括号定义了该对象的配置块。配置块中的代码采用以下形式:
{
stringProperty "value"
numericProperty 123.456
buildTimeProperty anObject.someMethod()
objectProperty {
//nested configuration block
}
}
该块可以访问对象的各个属性,并为它们赋值。这些属性可以是字符串、数字或对象本身。字符串属性可以接受文字值或从 Groovy 方法调用返回的值。文字值遵循类似于 Java 的规则。但是,字符串可以用双引号、单引号或 Groovy 用来表示字符串的任何其他方式来表示。对象特性使用嵌套块来设置它们各自的特性。
Gradle 构建脚本遵循一定的标准。在这个标准下,构建脚本的顶部是您声明 Gradle 插件的地方。这些是用 Groovy/Gradle 编写的组件,它们增加或扩展了 Gradle 的特性。插件声明遵循apply plugin: 'plugin.id'的形式,其中plugin.id是您希望使用的 Gradle 插件的标识符。
Gradle 任务和配置块以任何顺序遵循插件定义。习惯上声明 Android 插件,它是通过android属性在构建脚本中可用的对象。项目的依赖项通常遵循 Android 配置。依赖项列出了支持项目使用的任何外部 API、声明的插件或组件的所有库。下面是一个 Gradle 构建脚本的例子。稍后你会了解到更多的细节。
Listing 13-1. A Gradle Build Script Example
apply plugin: 'com.android.application'
android {
compileSdkVersion 20
buildToolsVersion '20.0.0'
defaultConfig {
applicationId "com.company.package.name"
minSdkVersion 14
targetSdkVersion 20
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:20.+'
}
IntelliJ 核心构建系统
Android Studio 基于 IntelliJ IDEA 平台构建,继承了 IntelliJ 核心的大部分功能。它以插件的方式向内核添加了更多 Android 特有的功能。插件是一种软件组件,可以从 IntelliJ 插件库中下载,并以可插拔的方式安装或删除,就像乐高积木一样。这些插件用于增强 IntelliJ 的功能,每个插件都可以通过使用设置窗口来启用或禁用。IntelliJ Gradle 插件将 IntelliJ 的核心构建系统融合到 Gradle 构建系统中。当输出通过 IntelliJ 核心反馈并以 IntelliJ 熟悉的方式格式化时,通常触发应用构建的动作改为调用 Gradle。
Gradle 构建概念
Gradle build 系统是一个通用工具,用于从源文件集合中构建软件包。它定义了一些构建软件的高级概念,这些概念对于大多数项目都是一致的。最常见的概念包括项目、源集、构建工件、依赖工件和存储库。项目是硬盘上的一个位置,包含所有项目源代码的集合。一个 Gradle build 将有一组源文件,它们被表示为源集。它将有一个可选的依赖列表。这些依赖项是软件工件,可以包括从 JAR 或 ZIP 存档到文本文件,再到预编译的二进制文件的任何东西。工件从存储库中取出。存储库是以特殊方式组织的工件的集合,以允许构建系统找到给定的工件。它可以是硬盘上的一个位置,也可以是一个按照标准惯例组织工件的特殊网站。每个工件可以有选择地包括它自己的依赖集,这些依赖集可以包含在构建中。构建将源集与依赖工件结合起来,生成构建工件。构建者可以有选择地将这些工件发布到存储库中,以便其他开发人员或团队可以使用它们。
Gradle Android 结构
Gradle Android 项目有一个分层结构,将子项目或模块嵌套在项目根下的各个文件夹中。这类似于 Android Studio 的 IntelliJ 基础传统上管理项目的方式。使用 Gradle 和 IntelliJ 环境,一个简单的项目可以包含一个名为app的模块,以及一些其他的文件夹和文件,或者它可以包含多个不同名称的模块。相似之处到此为止,因为 Gradle 允许模块的无限嵌套。换句话说,一个项目可以包含一个模块,该模块也包含嵌套模块。因此,Android Studio 构建系统在幕后运行 Gradle。下面的列表简要描述了一个典型的 Gradle Android 项目中包含的各个文件和文件夹。此列表主要关注您可能会考虑更改的文件:
- 临时的 Gradle 输出、缓存和其他支持元数据都存储在这个文件夹下。
app:单个模块按照名字嵌套在根目录下的文件夹中。每个模块文件夹包含一个 Gradle 项目文件,该文件生成主项目使用的输出。最简单的 Android 项目将包括一个生成 APK 工件的 Gradle 项目。- 这个文件夹包含了 Gradle 包装器。Gradle 包装器是一个 JAR 文件,包含与当前项目兼容的 Gradle 运行时版本。
build.gradle:整个项目构建逻辑存在于这个文件中。它负责包含任何必需的子项目,并触发每个子项目的构建。gradle.properties: Gradle 和 JVM 属性存储在这个文件中。您可以使用它来配置 Gradle 守护进程,并管理 Gradle 在构建期间如何生成 JVM 进程。你也可以使用这个文件来帮助 Gradle 在网络上通过 web 代理进行通信。- 这些文件是操作系统特有的文件,用于通过包装器执行 Gradle。如果您的系统上没有安装 Gradle,或者您没有与您的构建兼容的版本,那么建议使用这些文件中的一个来调用 Gradle。
local.properties:这个文件用来定义本地机器特有的属性,比如 Android SDK 或者 NDK 的位置。settings.gradle:多项目构建或任何定义子项目的项目都需要这个文件。它定义了整体构建中包含哪些子项目。Project.iml、.idea、.gitignore:在 Android Studio 中创建新项目时,您可能会注意到根目录下的这些文件。虽然这些文件(除了在第七章中讨论的.gitignore)不是 Gradle 构建系统的一部分,但是它们会随着你对 Gradle 文件的修改而不断更新。它们会影响 Android Studio“看待”您的项目的方式。- 所有的 Gradle 构建输出都在这个文件夹下。这包括生成的源。Gradle 有组织有意识地将所有输出保存在一个文件夹中。这简化了项目,因为要从版本控制中排除的内容列表不那么令人生畏,而清理只是删除一个文件夹。
项目相关性
Gradle 简化了依赖项管理,使得跨多个项目使用和重用代码变得容易,与平台无关。当一个项目变得越来越复杂时,将它分成单独的部分是有意义的,这在 Android 中被称为库。这些库可以在单独的 Gradle 项目中独立开发,也可以在 Android Studio 的多模块项目中共同开发。因为 Android Studio 将模块作为 Gradle 项目来处理,所以界限可能会变得模糊,这为代码共享带来了强大的可能性。调用全球另一个团队开发的代码中的对象与调用本地单独模块中的对象几乎是一样的!当您的项目中的代码需要调用另一个 Gradle 项目或另一个 Android Studio 模块中的代码时,您只需在主项目中声明一个依赖项,就可以将代码绑定在一起。最终的结果是将独立的部分无缝地缝合在一起,形成一个有凝聚力的应用。
考虑一个简单的例子,您的应用需要调用外部类Foo中的方法bar。使用传统的 Android 工具,你必须找到定义类Foo的项目。这可能包括从网上下载,或者如果你不太确定项目的位置或主页,甚至是费力的网上搜索。然后,您必须执行以下操作:
- 将下载的项目保存到开发计算机上
- 可能从源代码构建它
- 找到它的输出 JAR 文件,并将其复制或移动到项目的
libs文件夹中 - 可能将其签入源代码管理
- 如果您的 IDE 或工具集不能自动完成这一工作,请将它添加到您的库构建路径中
- 编写调用该方法的代码
所有这些步骤都容易出错,如果项目使用其他项目的 jar 或代码,许多步骤都需要重复。此外,项目的不同版本有时可能位于不同的位置,或者与您已经包含在应用中的其他项目不兼容。如果项目由您公司的另一个团队维护,您可能会遇到缺少预构建 JAR 的问题,这意味着您需要将另一个团队的构建文件与您的构建文件相结合,这可能会大大增加构建应用的时间和复杂性!
有了 Android Studio 和 Gradle,你可以跳过所有的混乱。您只需要在构建文件中将项目声明为依赖项,然后编写代码来调用该方法。为了理解依赖项是如何声明的,回想一下本章前面介绍的 Gradle 构建文件示例,它包括以下块:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:20.+'
}
第一个compile行指示 Gradle 获取libs文件夹下的所有 JAR 文件,作为编译步骤的一部分。这类似于经典的 Ant 构建脚本处理依赖项的方式,主要是为了与旧项目兼容。第二个compile行告诉 Gradle 从资源库中找到由com.android.support组组织的support-v4库的版本 20 或更高版本,并使其在项目中可用。请记住,存储库是一个抽象的位置,包含一组预构建的工件。Gradle 将根据需要从互联网上下载依赖工件,并使它们在编译器的类路径中可用,并将它们与您生成的应用打包在一起。
案例研究:Gradle 气象项目
在这一节中,您将研究一个项目 Gradle Weather,它将逐步公开各种类型的 Gradle 构建。这个项目显示天气预报。虽然一些实现使用了中等程度的高级功能,但我们将主要关注将应用缝合在一起并截断许多源列表的构建文件。演练的每一步都有分支。这个项目的 Git 存储库标记了这个研究中各个步骤的分支。您可以通过逐个检查这些步骤或者通过查看 Git 日志中与它们相关联的变更列表来参考本章中的这些步骤。请随意深入探究其来源。
我们从一个呈现假天气预报的极简实现开始 Gradle Weather。打开 Git 日志,找到名为 Step1 的分支。右键单击该分支,从上下文菜单中选择新建分支,创建一个新分支,如图 13-4 所示。将这个分支命名为 mylocal。随着您的继续,您将对这个分支进行提交。Gradle Weather 建立在全屏活动模板的基础上,使用作为该模板的一部分生成的SystemUiHider逻辑。它启动时有一个闪屏,运行在一个 5 秒钟的计时器上,并通过从一个名为TemperatureData的硬编码的普通 Java 对象中提取数据来模拟天气预报的加载。这个TemperatureData对象被赋予一个Adapter类来填充一个充满预测的列表视图。(ListView组件将在第八章的中深入讨论。)TemperatureData使用一个TemperatureItem类来描述给定一天的天气预报。该项目的构建脚本代码遵循之前定义的相同的标准 Gradle Android 项目结构。首先,您将检查负责 Gradle 构建的根文件夹中的文件。图 13-1 和清单 13-2 到 13-5 详细说明了控制构建的核心文件背后的代码。
图 13-1。
Create a new branch from the Step1 branch Listing 13-2. Settings.gradle
include ':app'
Listing 13-3. Root build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
Listing 13-4. local.properties
sdk.dir=C\:\\Android\\android-studio\\sdk
Listing 13-5. app\build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 20
buildToolsVersion '20.0.0'
defaultConfig {
applicationId "com.apress.gerber.gradleweather"
minSdkVersion 14
targetSdkVersion 20
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:20.+'
}
settings.gradle文件只定义了app子项目的路径。接下来是build.gradle,包括一个buildscript { ... }区块。buildscript块配置当前的构建文件。它包括应用中唯一的子项目app。接下来,build.gradle文件定义了适用于所有子项目的所有构建设置。它定义了一个包含 JCenter 存储库的buildscript块。这个可通过互联网访问的 Maven 资源库包含许多 Android 依赖项和开源项目。然后,该文件设置对 Gradle 0.12 或更高版本的依赖。最后,它将所有子项目设置为使用同一个 JCenter 存储库。
local.properties文件只包含 Android SDK 位置的设置。最后我们有app\build.gradle。这包括我们应用的所有构建配置和逻辑。第一行使用 Android Gradle 插件,用于当前构建。然后,它在android { ... }块中应用 Android 特有的配置。在这个块中,我们设置 SDK 版本和构建工具版本。SDK 指的是您希望编译的 Android SDK APIs 的版本,而构建工具版本指的是用于 Dalvik 可执行文件转换(dx步骤)、ZIP 对齐等等的构建工具的版本。defaultConfig { ... }块定义了应用 ID(当您提交到谷歌 Play 商店时使用)、您的应用兼容的最低 SDK 版本、您的目标 SDK、应用版本和版本名称。
buildTypes { ... }块控制构建的输出。它允许您重写控制生成输出的不同配置。使用此块,您可以定义发布到谷歌 Play 商店的特定配置。
dependencies { ... }块定义了应用的所有依赖关系。第一个依赖项是一个本地依赖项,它使用一个特殊的fileTree方法调用,该方法调用包含了libs子文件夹中的所有 JAR 文件。第二行声明了一个外部依赖项,它将从远程存储库中获取。使用一种特殊的语法,通过给定的字符串来声明外部依赖关系。该字符串由冒号分隔成几个部分。第一部分是组 ID,它标识了创建工件的公司或组织。第二部分是工件名称。最后一部分是您的模块所依赖的工件的特定版本。
Gradle Weather 定义了一个MainActivity类和另外三个负责建模和显示天气数据的类。清单 13-6 显示了这个活动的代码。这些级别包括TemperatureAdapter、TemperatureData和TemperatureItem。在这个应用的最初版本中,天气仅仅是一个虚构的数据集,硬编码在TemperatureData类中。
Listing 13-6. MainActivity.java
public class MainActivity extends ListActivity implements Runnable{
private Handler handler;
private TemperatureAdapter temperatureAdapter;
private TemperatureData temperatureData;
private Dialog splashDialog;
String [] weekdays = { "Sunday","Monday","Tuesday",
"Wednesday","Thursday","Friday","Saturday" };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
temperatureAdapter = new TemperatureAdapter(this);
setListAdapter(temperatureAdapter);
showSplashScreen();
handler = new Handler();
AsyncTask.execute(this);
}
private void showSplashScreen() {
splashDialog = new Dialog(this, R.style.splash_screen);
splashDialog.setContentView(R.layout.activity_splash);
splashDialog.setCancelable(false);
splashDialog.show();
}
private void onDataLoaded() {
((TextView) findViewById(R.id.currentDayOfWeek)).setText(
weekdays[Calendar.getInstance().get(Calendar.DAY_OF_WEEK)-1]);
((TextView) findViewById(R.id.currentTemperature)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.CURRENT));
((TextView) findViewById(R.id.currentDewPoint)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.DEW_POINT));
((TextView) findViewById(R.id.currentHigh)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.HIGH));
((TextView) findViewById(R.id.currentLow)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.LOW));
if (splashDialog!=null) {
splashDialog.dismiss();
splashDialog = null;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public void run() {
temperatureData = new TemperatureData(this);
temperatureAdapter.setTemperatureData(temperatureData);
// Set Runnable to remove splash screen just in case
handler.postDelayed(new Runnable() {
@Override
public void run() {
onDataLoaded();
}
}, 5000);
}
}
MainActivity.java假装加载天气数据时,暂时显示闪屏。(这样做是为了计划项目的后续修订,这将引入实际的数据负载。)然后,它通过使用TemperatureData类将数据加载到屏幕上的各个视图中。TemperatureData类包含一组假想的预测数据,如下面的部分代码片段所示:
protected List<TemperatureItem> getTemperatureItems() {
List<TemperatureItem>items = new ArrayList<TemperatureItem>();
items.add(new TemperatureItem(drawable(R.drawable.early_sunny),
"Today", "Sunny",
"Sunny, with a high near 81\. North northwest wind 3 to 8 mph."));
items.add(new TemperatureItem(drawable(R.drawable.night_clear),
"Tonight", "Clear",
"Clear, with a low around 59\. North wind 5 to 10 mph becoming
light northeast in the evening."));
items.add(new TemperatureItem(drawable(R.drawable.sunny_icon),
"Wednesday", "Sunny",
"Sunny, with a high near 82\. North wind 3 to 8 mph."));
//example truncated for brevity...
return items;
}
public Map<String, String> getCurrentConditions() {
Map<String, String> currentConditions = new HashMap<String, String>();
currentConditions.put(CURRENT,"63");
currentConditions.put(LOW,"59");
currentConditions.put(HIGH,"81");
currentConditions.put(DEW_POINT,"56");
return currentConditions;
}
主活动的布局包括一个由清单 13-7 中所示的TemperatureAdapter类填充的ListView。这个类接受一个TemperatureData对象,用它来拉一个TemperatureItems列表。它使用图 13-2 所示的temperature_summary布局为每个TemperatureItem创建一个视图。清单 13-8 中详述的每个TemperatureItem仅仅是一个数据容器对象,带有重要数据字段的 getters。这些总结包含在活动的主布局中,如图 13-3 所示。
图 13-3。
The activity_main layout
图 13-2。
The temperature_summary layout Listing 13-7. TemperatureAdapter.java
public class TemperatureAdapter extends BaseAdapter {
private final Context context;
List<TemperatureItem>items;
//This example is truncated for brevity...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView != null ? convertView : createView(parent);
TemperatureItem temperatureItem = items.get(position);
((ImageView) view.findViewById(R.id.imageIcon)).setImageDrawable(temperatureItem.
getImageDrawable());
((TextView) view.findViewById(R.id.dayTextView)).setText(
temperatureItem.getDay());
((TextView) view.findViewById(R.id.briefForecast)).setText(
temperatureItem.getForecast());
((TextView) view.findViewById(R.id.description)).setText(
temperatureItem.getDescription());
return view;
}
private View createView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflater.inflate(R.layout.temperature_summary, parent, false);
}
public void setTemperatureData(TemperatureData temperatureData) {
items = temperatureData.getTemperatureItems();
notifyDataSetChanged();
}
}
Listing 13-8. TemperatureItem.java
class TemperatureItem {
private final Drawable image;
private final String day;
private final String forecast;
private final String description;
public TemperatureItem(Drawable image, String day, String forecast,
String description) {
this.image = image;
this.day = day;
this.forecast = forecast;
this.description = description;
}
public String getDay() {
return day;
}
public String getForecast() {
return forecast;
}
public String getDescription() {
return description;
}
public Drawable getImageDrawable() {
return image;
}
}
Android 库依赖项
虽然一个微不足道的 Android 应用可能包含由单个团队开发的代码,但随着时间的推移,该应用最终会成熟,以包含由其他开发人员或团队实现的功能。这些可以在 Android 库中对外提供。Android 库是一种特殊类型的 Android 项目,您可以在其中开发一个软件组件或一系列组件,为您的应用提供一些行为——无论是像两个数字相乘这样简单的事情,还是像提供一个列出朋友和活动的社交网络门户这样复杂的事情。Android 库以一种允许你即插即用而没有太多麻烦的方式将特性具体化。Gradle 强大的存储库系统允许您轻松定位和使用来自其他公司、开源库或您自己组织中其他人的库的代码。在本节中,您将使用一个 Android 库依赖项来改进我们的应用,该依赖项通过网络请求天气数据。这种改变对于里程碑版本来说是不够的,因为它不会以有意义的方式呈现网络数据。然而,它足以演示如何在现有的 Android 应用中使用库项目中的代码。您将做进一步的修改来呈现数据。
添加 Android 库的流程类似于从头开始创建 Android 应用。选择文件➤新建模块打开新建模块向导,如图 13-4 所示。然后在第一个对话框中选择 Android 库。在第二个对话框中,输入WeatherRequest作为模块名称,并选择符合您的 app 要求的最低 SDK 设置,如图 13-5 所示。
图 13-5。
Set the library module’s name and SDK levels
图 13-4。
Add a library module
在向导的下一页选择不添加活动,如图 13-6 所示。单击“完成”按钮,将库模块添加到项目中。
图 13-6。
Choose the Add No Activity option
克隆的存储库中的步骤 2 有新的模块,您可以将其用作参考。您的新模块将包含以下build.gradle文件:
apply plugin: 'com.android.library'
android {
compileSdkVersion 20
buildToolsVersion "20.0.0"
defaultConfig {
minSdkVersion 14
targetSdkVersion 14
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
这个版本和应用主版本的主要区别是使用了 Android 库插件。这个插件从模块源生成一个特殊的 Android 归档文件格式,AAR。AAR 格式是 Android 新增的增强功能之一,它允许代码以库的形式在项目间共享。这些库可以通过使用新的 Gradle 构建系统发布到工件存储库中。您还可以声明对任何拥有已发布的 AAR 工件的项目的依赖,并在您的项目中使用它。典型的 AAR 文件仅仅是一个扩展名为.aar的 ZIP 文件。它具有以下结构:
/AndroidManifest.xml(必需)/classes.jar(必需)/res/(必需)/R.txt(必需)/assets/(可选)/libs/*.jar(可选)/jni/<abi>/*.so(可选)/proguard.txt(可选)/lint.jar(可选)
AndroidManifest.xml描述了归档文件的内容,而classes.jar包含了编译后的 Java 代码。资源可以在res目录下找到。R.txt文件包含aapt工具的文本输出。
Android AAR 文件允许您随意捆绑素材、本地库和/或 JAR 依赖项,这在 SDK 的早期版本中是不可能的。
在我们例子中的存储库的步骤 3 分支中,我们已经向项目添加了一个WeatherRequest模块,并更改了主应用模块,以将该模块作为依赖项包含进来。这个新模块包含一个类NationalWeatherRequest,它代表主应用与国家气象局建立网络连接。这是一个返回任何地点的天气信息的服务。位置以经度和纬度的形式给出,响应是 XML 格式的。研究清单 13-9 中的代码会有更好的理解。
Listing 13-9. NationalWeatherRequest.java
public class NationalWeatherRequest {
public static final String NATIONAL_WEATHER_SERVICE =
"``http://forecast.weather.gov/MapClick.php?lat=%f&lon=%f&FcstType=dwml
public NationalWeatherRequest(Location location) {
URL url;
try {
url = new URL(String.format(NATIONAL_WEATHER_SERVICE,
location.getLatitude(), location.getLongitude()));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(
"Invalid URL for National Weather Service: " +
NATIONAL_WEATHER_SERVICE);
}
InputStream inputStream;
try {
inputStream = url.openStream();
} catch (IOException e) {
log("Exception opening Nat'l weather URL " + e);
e.printStackTrace();
return;
}
log("Dumping weather data...");
BufferedReader weatherReader = new BufferedReader(
new InputStreamReader(inputStream));
try {
for(String eachLine = weatherReader.readLine(); eachLine!=null;
eachLine = weatherReader.readLine()) {
log(eachLine);
}
} catch (IOException e) {
log("Exception reading data from Nat'l weather site " + e);
e.printStackTrace();
}
}
private int log(String eachLine) {
return Log.d(getClass().getName(), eachLine);
}
}
这个新类检索天气数据并将其转储到 Android 日志中,作为使用 Android 库的一个基本示例。要在我们的项目中包含新模块,必须编辑 app 模块中的build.gradle文件。找到 dependencies 块并对其进行更改,如下所示:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:20.+'
compile project(':WeatherRequest')
}
compile project()行引入了一个项目依赖。项目位置是作为参数给定给project()方法的相对路径,这个位置使用冒号作为路径分隔符。前面的例子是在主项目文件夹GradleWeather中名为WeatherRequest的文件夹中定位一个项目。Gradle 将项目依赖视为主构建中的附加工作。在构建应用模块之前,Gradle 将运行WeatherRequest依赖项目,然后查看这个项目,在build/outputs文件夹下找到它的输出。WeatherRequest项目输出一个 AAR 文件作为它的主要输出,由 app 模块中的构建使用。AAR ZIP 文件在 app 模块的build/intermediates文件夹下展开,其内容包含在其编译输出中。您通常不需要了解哪个项目文件包含在哪里的细节。只是在你的主模块的dependencies块中引用另一个模块是告诉 Gradle 把它作为你的应用的一部分的高级方式。对您的本地分支进行相同的更改,并提交给 get。
Java 库依赖性
我们项目的下一个版本,包括在第 4 步中,包含了一个纯 Java 依赖。这展示了 Android 和 Gradle build 系统的灵活性,因为它为包含大量预先存在的代码打开了大门。选择文件➤新建模块打开新建模块向导,如图 13-7 所示。然后在第一个对话框中选择 Java 库。在第二个对话框中,输入 WeatherParse 作为库名,点击完成,如图 13-8 所示。
图 13-8。
Name the new JAR library
图 13-7。
Add a new JAR library
如你所见,添加 Java 库模块类似于添加 Android 模块。主要区别在第二个对话框中很明显,它的选项较少。这是因为 Java 模块通常只包含编译过的 Java 类文件,其输出是一个 jar 文件。与输出 aar 文件的 Android 库模块相比,AAR 文件可以包括布局、原生 C/C++ 代码、素材、布局文件等等。
这就引出了一个问题,为什么会有人想要使用 Java 模块而不是 Android 库呢?一开始优势并不明显,但是有了 Java 模块,你就有机会在 Android 平台之外重用你的 Java 代码。这在很多情况下都会让你受益。考虑一个服务器端 web 解决方案,它定义了一个复杂的图像处理算法来匹配相似的人脸。这种算法可以单独定义为一个 Gradle 项目,并直接在您的 Android 应用中使用,以添加相同的功能。Java 模块也可以与普通的 JUnit 测试用例集成。虽然 Android 包括 JUnit 框架的衍生产品,但这些测试用例必须在设备或仿真器上部署和执行,这在几个周期后很快成为一个繁琐的过程。使用 pure JUnit 来测试 Java 代码,这样只需点击一下按钮,测试就可以直接在 IDE 中运行。这些测试的运行速度通常比 Android JUnit 的同类产品快一个数量级。
我们的示例项目将包含一些复杂的 XML 解析逻辑,以理解来自国家气象局的 XML 响应。我们的WeatherParse使用开源的 kXML 库来解析响应。这是与 Android 运行时捆绑在一起的同一个库。挑战在于在 kXML 所在的 Android 运行时之外编译我们的解析器。虽然我们可以为 kXML 设置一个依赖项,但是我们还需要在设备上分发和使用我们的 Java 库,而不包括多余的 kXML API 副本。我们稍后将解决这个问题。现在,让我们看看添加的 Java 依赖关系的build.gradle文件:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'kxml:kxml:2.2.2'
testCompile 'junit:junit:4.11'
}
processTestResources << {
ant.copy(todir:sourceSets['test'].output.classesDir) {
fileset(dir:sourceSets['test'].output.resourcesDir)
}
}
除了 Java 插件的声明之外,这里没有太多内容。Java 插件配置 Gradle 生成一个 JAR 文件作为输出,同时设置编译、测试和打包类文件所需的构建步骤。dependencies { ... }块为 kXML 解析器和 JUnit 定义了编译时依赖关系。Gradle 将生成一个 Java JAR 文件,其中只包含项目中已编译的类。该项目还包括两个 Java 类文件(一个调用解析器,一个处理解析器事件)以及一个单元测试 Java 类。该测试将来自服务的典型天气 XML 响应的副本提供给解析器,并验证解析器可以提取天气信息。响应的副本保存在 resources 文件夹下。参见清单 13-10 中简短的单元测试代码片段。
Listing 13-10. WeatherParseTest.java
public class WeatherParseTest extends TestCase {
private WeatherParser weather;
private String asString(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream));
StringBuilder builder = new StringBuilder();
for(String eachLine = reader.readLine(); eachLine != null;
eachLine = reader.readLine()) {
builder.append(eachLine);
}
return builder.toString();
}
public void setUp() throws IOException, XmlPullParserException {
URL weatherXml = getClass().getResource("/weather.xml");
assertNotNull("Test requires weather.xml as a resource at the CP root.",
weatherXml);
String givenXml = asString(weatherXml.openStream());
this.weather = new WeatherParser();
weather.parse(new StringReader(givenXml.replaceAll("<br>", "<br/>")));
}
public void testCanSeeCurrentTemp() {
assertEquals(weather.getCurrent("apparent"), "63");
assertEquals(weather.getCurrent("minimum"), "59");
assertEquals(weather.getCurrent("maximum"), "81");
assertEquals(weather.getCurrent("dew point"), "56");
}
public void testCanSeeCurrentLocation() {
assertEquals("Should see the location in XML", weather.getLocation(),
"Sunnyvale, CA");
}
}
任何单元测试都可以通过右击测试方法名并单击上下文菜单中的 run 选项来运行。反馈是即时的,因为测试直接在 IDE 中运行,没有启动或选择设备、上传 APK 文件和启动的开销。当您从 Android Studio 中的 Java 库运行单元测试时,Gradle 会在后台被调用,并将资源从 resources 文件夹复制到测试要定位的输出文件夹中。测试用例中的setUp方法利用复制的weather.xml文件,并使用定制的asString方法将它作为字符串读入。(另外,XML 包含 HTML <br>标签,需要使用 Java 的String replaceAll()方法正确终止这些标签,以防止 XML 解析异常。)方法setUp()继续创建一个WeatherParser对象,同时要求它解析 XML。前面代码中包含的两个测试方法演示了如何使用天气解析器从响应中找到当前温度和当前位置。
有了天气解析 Java 库,你可以自由地改变我们的天气请求 Android 库来使用它。要做到这一点,你需要做两件事。首先,您要确保 Java 库包含在GradleWeather项目根目录下的顶层settings.gradle文件中。接下来,您在WeatherRequest gradle build 中设置一个依赖项来获取WeatherParse项目输出。同样,WeatherParse项目是一个输出单个 JAR 文件的 Java 库,但是有一个微妙的细节需要注意。我们的 Java 库包括对 kXML 的依赖,它被认为是可传递的。我们可以在WeatherRequest模块中声明依赖关系如下:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':WeatherParse')
}
但是,这将导致以下编译器错误:
Output:
UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexException:
Multiple dex files define Lorg/xmlpull/v1/XmlPullParser;
许多开发人员感到沮丧的一个常见原因是,你的 APK 中包含了不止一个相同的文件。在这种情况下,例外来自于 Android,它已经将 kXML API 中定义的XmlPullParser作为 SDK 的一部分。Android SDK 使这些和其他 API 在任何 Android 应用或库项目的编译过程中可用。我们在构建WeatherParse模块时不会出错的原因是它被定义为一个 Java 库。Java 库项目是用 Java SDK 编译的,编译过程中不包含任何 Android APIs。为了解决这个错误,我们需要从WeatherRequest模块中考虑的依赖项列表中排除这个可传递的依赖项。我们将图 13-9 所示的代码添加到WeatherRequest模块的 Gradle 构建文件中,以消除错误。
图 13-9。
Exclude the kXML dependency
该项目现在被更新为解析 XML 天气响应,并通过使用来自 XML 的链接下载图像。NationalWeatherRequest对象将 URL 对象缓存为成员变量,并添加一个getWeatherXml方法来使用 URL,如清单 13-11 所示。
Listing 13-11. NationalWeatherRequest.java
public class NationalWeatherRequest {
public static final String NATIONAL_WEATHER_SERVICE =
"``http://forecast.weather.gov/MapClick.php?lat=%f&lon=%f&FcstType=dwml
private final URL url;
//...
public String getWeatherXml() {
InputStream inputStream = getInputStream(url);
return readWeatherXml(inputStream);
}
private String readWeatherXml(InputStream inputStream) {
StringBuilder builder = new StringBuilder();
if (inputStream!=null) {
BufferedReader weatherReader = new BufferedReader(
new InputStreamReader(inputStream));
try {
for(String eachLine = weatherReader.readLine(); eachLine!=null;
eachLine = weatherReader.readLine()) {
builder.append(eachLine);
}
} catch (IOException e) {
log("Exception reading data from Nat'l weather site " + e);
e.printStackTrace();
}
}
String weatherXml = builder.toString();
log("Weather data " + weatherXml);
return weatherXml;
}
private InputStream getInputStream(URL url) {
InputStream inputStream = null;
try {
inputStream = url.openStream();
} catch (IOException e) {
log("Exception opening Nat'l weather URL " + e);
e.printStackTrace();
}
return inputStream;
}
清单 13-12 详述了如何更新NationalWeatherRequestData对象以使用新的getWeatherXML方法,并将其结果提供给新的WeatherParse Java 组件。
Listing 13-12. NationalWeatherRequestData.java
public NationalWeatherRequestData(Context context) {
this.context = context;
Location location = getLocation(context);
weatherParser = new WeatherParser();
String weatherXml = new NationalWeatherRequest(location).getWeatherXml();
//National weather service returns XML data with embedded HTML <br> tags
//These will choke the XML parser as they don't have closing syntax.
String validXml = asValidXml(weatherXml);
try {
weatherParser.parse(new StringReader(validXml));
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public String asValidXml(String weatherXml) {
return weatherXml.replaceAll("<br>","<br/>");
}
@Override
public List<TemperatureItem> getTemperatureItems() {
ArrayList<TemperatureItem> temperatureItems =
new ArrayList<TemperatureItem>();
List<Map<String, String>> forecast = weatherParser.getLastForecast();
if (forecast!=null) {
for(Map<String,String> eachEntry : forecast) {
temperatureItems.add(new TemperatureItem(
context.getResources().getDrawable(R.drawable.progress),
eachEntry.get("iconLink"),
eachEntry.get("day"),
eachEntry.get("shortDescription"),
eachEntry.get("description")
));
}
}
return temperatureItems;
}
TemperatureAdapter级经历了一次大修,变得相当复杂。它使用来自WeatherRequest的图像链接在后台下载图像。参见清单 13-13 中的定义。
Listing 13-13. TemperatureAdapter.java
public class TemperatureAdapter extends BaseAdapter {
private final Context context;
List<TemperatureItem>items;
public TemperatureAdapter(Context context) {
this.context = context;
this.items = new ArrayList<TemperatureItem>();
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView != null ? convertView : createView(parent);
TemperatureItem temperatureItem = items.get(position);
ImageView imageView = (ImageView) view.findViewById(R.id.imageIcon);
imageView.setImageDrawable(temperatureItem.getImageDrawable());
if(temperatureItem.getIconLink()!=null){
Animation animation = AnimationUtils.loadAnimation(
context, R.anim.progress_animation);
animation.setInterpolator(new LinearInterpolator());
imageView.startAnimation(animation);
((ViewHolder) view.getTag()).setIconLink(temperatureItem.getIconLink());
}
((TextView) view.findViewById(R.id.dayTextView)).setText(
temperatureItem.getDay());
((TextView) view.findViewById(R.id.briefForecast)).setText(
temperatureItem.getForecast());
((TextView) view.findViewById(R.id.description)).setText(
temperatureItem.getDescription());
return view;
}
class ViewHolder {
private final View view;
private String iconLink;
private AsyncTask<String, Integer, Bitmap> asyncTask;
public ViewHolder(View view) {
this.view = view;
}
public void setIconLink(String iconLink) {
if(this.iconLink != null && this.iconLink.equals(iconLink)) return;
else this.iconLink = iconLink;
if(asyncTask != null) {
asyncTask.cancel(true);
}
asyncTask = new AsyncTask<String,Integer,Bitmap>() {
@Override
protected Bitmap doInBackground(String... url) {
InputStream imageStream;
try {
imageStream = new URL(url[0]).openStream();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return BitmapFactory.decodeStream(imageStream);
}
@Override
protected void onPostExecute(final Bitmap bitmap) {
if (bitmap == null) {
return;
}
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ImageView imageView = (ImageView) view
.findViewById(R.id.imageIcon);
imageView.clearAnimation();
imageView.setImageBitmap(bitmap);
}
});
asyncTask = null;
}
};
asyncTask.execute(iconLink);
}
}
private View createView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View inflatedView = inflater.inflate(R.layout.temperature_summary,
parent, false);
inflatedView.setTag(new ViewHolder(inflatedView));
return inflatedView;
}
public void setTemperatureData(TemperatureData temperatureData) {
items = temperatureData.getTemperatureItems();
notifyDataSetChanged();
}
}
每个ImageViews都与一个ViewHolder相关联,并用一个微调图标和一个模拟无限进度微调的旋转动画来初始化。大部分工作都是用ViewHolder的setIconLink方法完成的。该方法触发后台天气图标的下载。下载完成后,ImageView会根据下载的图像进行更新。并且旋转动画被取消。同样,这个类文件非常复杂,只是为了处理图像的加载。简化不是更好吗?
第三方库
有时你没有实现一个复杂逻辑的能力或专业知识。第三方库经常被用来解决 Android 开发中的这些和其他棘手的问题。如前所述,调用由地球上其他地方的另一个开发人员或团队开发的代码与调用项目中另一个模块的代码几乎是一样的。我们继续 Step5 分支,在这里我们将演示如何将开源组件用于 Gradle Weather 项目。我们的应用下载了一系列图片,每张图片代表某一天的情况。我们从图 13-10 所示的 app 模块下的 Gradle build 的极简添加开始。
图 13-10。
Add the universal image loader
就这样!您将立即看到一个黄色提示,指示 Gradle 文件已更改,以及一个超链接文本按钮,使项目同步开始。点击图 13-11 所示的链接,允许 Android Studio 将底层 IntelliJ 项目文件与依赖项同步。格雷尔会在后台下载它们。
图 13-11。
Gradle files need to be synced
项目同步和下载完成后,可以更改代码来调用 API。回顾之前的内容,我们可以体会到在后台下载天气图标是多么容易:
private final ImageLoader imageLoader;
List<TemperatureItem>items;
public TemperatureAdapter(Context context, ImageLoader imageLoader) {
this.context = context;
this.imageLoader = imageLoader;
this.items = new ArrayList<TemperatureItem>();
}
public void setIconLink(String iconLink) {
final ImageView imageView = (ImageView) view.findViewById(
R.id.imageIcon);
imageLoader.displayImage(iconLink, imageView,
new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
imageView.clearAnimation();
super.onLoadingComplete(imageUri, view, loadedImage);
}
});
}
构造函数被更新以获取一个ImageLoader对象并将其存储在一个实例变量中。setIconLink方法只是将iconLink交给了ImageLoader,后者完成了所有繁重的工作。
打开旧项目
Android Studio 现在包括强大的导入工具,用于将旧项目迁移到新的 Gradle build 系统中。这种支持几乎是透明的,并且会在您打开旧项目时自动发生。在 Android Studio 的早期测试阶段,许多人在打开这些旧项目时会感到恼火。令人沮丧的部分是 Gradle 的快速更新周期,这可能导致旧版本有时无法工作。当你在旧版本上使用新版本的 Gradle 时会发生这种情况。在导入旧项目时使用 Gradle 包装器应该可以在某种程度上减轻这种痛苦,但有时这并不可行或有效。当您在更新版本的 Android Studio 中打开一个旧项目时,例如,从 0.8x 版本迁移到 1.x 版本,您可能会看到如图 13-12 所示的不支持的 Android Gradle 插件错误。
图 13-12。
Unsupported version error
你可以点击修复插件版本并重新导入项目链接,但是你会看到图 13-13 中的错误,它抱怨一个丢失的 DSL 方法runProGuard()。有了 Gradle 的新知识,您可以推测 DSL 方法是什么,并且您现在知道打开应用的build.gradle文件来找到这个错误的方法调用。1.x 版本不赞成这个调用,而支持minifyEnabled。
图 13-13。
DSL method not found error
摘要
您已经探索了 Gradle 构建系统的基础。我们展示了一个具有不同依赖类型的多模块 Android 项目。您还看到了如何在 Android 项目中将常规 Java 代码与 JUnit 测试结合起来。最后,您学习了如何使用 Android Studio 内置的导入功能来打开旧项目。您演练了如何修复这些旧项目导入的一些常见问题。Gradle 还包括一个强大的依赖管理系统,允许您轻松地在项目间重用代码。这一章仅仅触及了 Android Studio 中 Gradle 的一些皮毛。请自行探索,并进一步增强示例项目。
十四、更多 SDK 工具
Android Studio 是 IntelliJ IDEA 的一个特殊构建,它包含了面向 Android 开发的工具。本章探讨了您可以使用的各种工具。其中许多被嵌入到各种工具窗口中,其他的只需轻轻一击。
Android 设备监视器
Android 设备监视器(ADM)是 SDK 中最强大的工具之一。它允许您从多个角度监控您的设备,并检查诸如内存、CPU、网络利用率等内容。要开始使用 ADM,请从 Android Studio 菜单中选择工具➤ Android ➤ Android 设备监视器。打开的窗口左侧有一个设备视图。
在这个视图中,您应该看到连接到开发计算机的所有设备,以及在每个设备上运行的进程列表。如果您的应用没有运行,请启动它,然后在进程列表中找到它。该名称应该遵循通常的包命名约定。如果读进程名有困难,您可以在设备视图中调整各个列的大小。点击你的应用将其选中,它将成为 ADM 中各种工具的焦点,在这些示例中,你将分析 Gradle Weather 应用。图 14-1 显示了选择 Gradle Weather 应用的 ADM 窗口。
图 14-1。
The Android Device Monitor screen
线程监视器
选中您的应用后,您可以通过单击启用 ADM 中的特性来开始探索其执行的各种特征。线程活动是比较容易监控的事情之一。单击 Update Threads 按钮,用活动线程列表以及 id、状态和名称填充右侧视图。当您执行更新时,单击右视图中的任何线程将显示其活动的更多细节。其他详细信息将作为堆栈跟踪显示在“线程”选项卡下的窗格中。例如,单击名为 main 的线程。您可能会看到类似如下的堆栈跟踪:
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:138)
at android.os.Looper.loop(Looper.java:123)
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)
主线程通常迭代android.os.MessageQueue,寻找用户交互。当在屏幕上做手势、敲键盘或发生其他交互时,系统会将活动记录为消息并填充MessageQueue。系统调用nativePollOnce()来检索这些消息,然后将它们作为事件发送到你的应用。这个调用是从由主循环调用的MessageQueue.next()方法调用的,主循环由ActivityThread.main()方法调用。沿着堆栈往下看,您可以看到主线程是由Zygote.Init()启动的,它是您启动设备时首先启动的进程之一。您可以单击堆栈跟踪上方的刷新按钮来更新它。
探索应用中其他线程的堆栈跟踪,了解它们在做什么。在图 14-2 中,我们探索了 Gradle Weather 项目中众多通用图像加载器线程之一,同时更新了堆栈跟踪。堆栈跟踪揭示了从网络流中读取图像并将其解码为位图所涉及的工作。
图 14-2。
The thread monitor
堆监视器
堆监视器允许您在应用运行时检查在堆上分配的对象。单击 ADM 窗口右侧 Threads 选项卡旁边的 Heap 选项卡,将 heap monitor 置于前台。保持您的应用在设备窗格中处于选中状态,单击更新堆按钮以启用堆更新,如图 14-3 所示。每次垃圾收集器在设备上运行时,都会发生堆更新;每次执行时,描述堆的新数据都被发送到 ADM 用户界面。在偶然的用例下与你的应用的交互可能最终触发垃圾收集器的执行。您还可以通过单击看起来像垃圾桶的垃圾收集图标,随时强制执行。
图 14-3。
The heap monitor
Heap 选项卡中充满了标识堆上各个对象的类型和数量以及每种类型的最小和最大大小的详细信息。选择单个类型允许您深入查看该特定类型的分配计数。在我们的示例中,我们深入研究了 11,212 个 2 字节数组对象,它们占据了堆中最大的总空间。堆详细信息下面的图表显示,有超过 2,500 个 2 字节数组正好是 32 字节长。这些数组可能是用于图标的分配,因为 32 字节是管理图像数据的最佳大小。
分配跟踪器
分配跟踪器也可以用来跟踪你的应用中内存的使用情况。您可以通过“分配跟踪器”选项卡访问分配跟踪器,该选项卡位于“堆”选项卡旁边,有两个按钮:开始跟踪和获取分配。单击开始跟踪按钮开始跟踪分配。单击获取分配按钮,在用户分配视图中加载捕获的数据。跟踪器运行时,“开始”按钮变成“停止”按钮。您可以随时单击停止跟踪来终止跟踪器。
在捕获时,视图将显示顺序、大小、类、线程 ID 以及每个分配的类和方法。该列表最初按大小降序排序,但您可以单击任何列标题来更改排序顺序。重复单击列标题可在升序和降序之间切换排序顺序。单击视图中的任何条目都会加载发生分配的堆栈跟踪。同样,这个例子使用了 Gradle Weather 应用,您可以滚动列表。该应用将加载不同日子的图标,同时跟踪分配。图 14-4 说明了结果。
图 14-4。
The allocations tracker results from Gradle Weather
作为从网络下载图标数据的一部分,您可以看到几个 32KB byte数组的分配。如果您的应用遇到内存不足的问题,这可能是优化的目标。除非遇到内存不足的情况,否则不应该优化代码,理解这一点很重要。过早地优化代码会导致不必要的复杂性,并且可能与您的性能优化目标背道而驰。
网络统计
网络统计选项卡能够监控网络流量。这个工具和其他工具一样容易使用。图 14-5 显示了开始网络统计数据捕获之前的选项卡。单击网络统计选项卡上的开始按钮,开始捕获网络流量。“开始”按钮变成了“停止”按钮,单击它可以停止采集。
图 14-5。
Tracking network statistics
该视图将显示一个图表,绘制应用运行时的传入和传出流量。图表顶部的 RX 部分代表响应数据,而 TX 部分代表传输数据。在我们的示例中,我们捕获了 1MB 的响应数据,这些数据是在 Gradle Weather 应用中滚动列表视图以下载图像数据时发生的。设备已经发送了总计 52KB 的请求数据。
层次结构查看器
通常,您可能很难正确渲染布局。您的活动中可能有逻辑,根据用户交互有条件地定位视图或设置可见性。当事情变得复杂时,转储 ADM 中的视图层次结构会有所帮助。单击屏幕截图中的元素会在屏幕截图右侧的窗格中显示细分。细分以树形结构给出,节点代表ViewGroup对象。图 14-6 显示了 Gradle Weather 应用的层级转储。您可以浏览这些节点,查看它们各自的布局属性。您还可以钻取到任何节点以浏览其子对象。
图 14-6。
Exploring the Gradle Weather UI by using a hierarchy ump
单击右窗格中的单个节点将在屏幕截图中找到相应的视图对象,同时在其周围绘制一个红色矩形高亮显示。任何选定节点的属性都显示在树视图窗格下的窗格中。这些属性表明视图是否可见、聚焦、可点击、被选择等等。您还可以检查视图边界、资源 ID 和内容描述。如果您遇到一个应该可见但没有的视图,您可以选择包含的ViewGroup布局并钻取以找到该视图。
对视图的一个常见误解是View.INVISIBLE和View.GONE常量属性值之间的差异。标记为View.GONE的视图不会出现在层次结构中。标记为View.INVISIBLE的视图将出现在层次结构中,但不会被绘制到屏幕上。另一个常见的问题是理解如何在ViewGroup布局或容器上使用wrap_content属性为视图保留空间,即使它们是不可见的。如果视图被标记为View.GONE,容器将不会保留空间,并将缩小尺寸以容纳任何剩余的内容。
Note
Android 设备监视器基于 Eclipse 工具,让您能够通过切换视角来调整用户界面。如果您不熟悉 Eclipse,请理解透视图代表了一个特定的工作流,并且选项卡和视图以一种最适合该工作流的方式定位。Eclipse 工具通常有几个预配置的透视图,同时允许您创建自己的透视图。因为 ADM 中的许多工具都被嵌入到 Android Studio IDE 中,所以本节只讨论 ADM 专用工具的一个子集。
点击窗口➤打开透视图,查看监视器可用的工作流程,如图 14-7 所示。
图 14-7。
Switching the perspective in ADM
单击层次视图选项打开层次视图透视图。层次查看器与层次转储工具的不同之处在于,它仅适用于模拟器或根设备。要使用级别查看器,请启动模拟器并在模拟器中启动您的应用。单击刷新按钮,然后在 Windows 选项卡的设备列表中找到您的模拟器。您的屏幕应该类似于图 14-8 。在设备列表中找到代表您的应用的进程,然后单击加载按钮,从您的应用加载当前屏幕的视图层次。层次视图提供了当前在屏幕上呈现的布局的大而深的树形视图。
图 14-8。
Exploring the Gradle Weather UI using the Hierarchy viewer
ADM 窗口左侧的“视图属性”选项卡包含一个全面的属性列表,而中间的窗格则显示层次结构的放大视图。您可以在窗口的右下角找到布局视图选项卡,它显示了当前屏幕的类似线框的摘要。单击这些选项卡中的元素会选择其他选项卡中的等效元素,因为它们都保持同步。
Android 显示器集成
Android Studio 在 IDE 底部的 Android DDMS 视图中捆绑了来自 ADM 的一些更常用的工具。这些工具允许您生成系统信息转储、执行垃圾收集、终止应用、分析堆以及执行方法跟踪。随着你的应用越来越复杂,这些工具将会成为你宝库中的无价之宝。在 Android DDMS 视图中,在进程列表中选择您的应用。可以在 Android 视图中的设备➤日志目录选项卡下找到进程列表。如果它还不在最前面,请单击此选项卡使其成为焦点。选择运行应用的流程后,将会启用附加工具按钮。
内存监视器
内存监视器显示当前调试的应用消耗的内存的图形图表。它可用于轻松识别一般内存趋势。单击屏幕右下角的内存监视器按钮,该按钮位于事件日志和 Gradle 控制台按钮旁边。这将打开监视器工具窗口。试验您的应用,并在监视器运行时观察图表。在图 14-9 中,我们运行 Gradle Weather 应用,同时滚动预测列表以查看内存影响。您还可以使用 Initiate GC 按钮在任何时间点触发垃圾收集,并查看回收了多少内存。如果图形中使用的内存在启动垃圾收集器后没有恢复到合理的水平,您的应用可能会泄漏内存。
图 14-9。
Memory consumption of Gradle Weather while scrolling the list
方法跟踪工具
方法跟踪工具可以帮助您找到需要大量 CPU 周期来执行的方法。CPU 周期是一种宝贵的资源,方法应该这样对待它们。当一个或多个方法在 CPU 上运行得太舒服时,应用就会变慢。如果您的应用速度变慢,或者您只想更好地了解典型用例中 CPU 的使用情况,您可以使用方法跟踪工具来记录在任何给定场景下使用应用时的活动。
方法跟踪工具使用起来很简单。准备好你的应用,或者让它进入你想要检查的状态。从流程列表中选择您的应用后,单击启动方法跟踪图标开始跟踪。使用您的应用练习您感兴趣的任何方法,然后再次单击按钮以完成方法跟踪。在图 14-10 中,我们在滚动列表时从 Gradle Weather 捕捉到了活动。
图 14-10。
The Method Trace tool
在本例中,我们运行了 Gradle Weather 应用,并在录制时滚动了天气条目列表。当您最初完成方法跟踪时,视图将默认为主线程。每个方法调用都以可视化方式表示,为调用绘制条形。这些条根据它们的独占时间来着色,独占时间是仅在该方法中花费的时间,不包括在它调用的方法中花费的时间。“线程”下拉列表可用于切换其他线程的视图,以便您可以看到它们遇到的活动。该图探索了发生在后台线程(不是主线程)中的图像加载和解码。虽然在主线程上做大量的工作是 UI 迟缓的常见原因,但是您永远不能排除在其他线程上做的工作。只要注意到有多少额外的线程正在运行以及它们正在执行什么工作,许多问题就会浮出水面。
使用鼠标上的滚轮可以放大和缩小跟踪视图。围绕鼠标光标在屏幕上的位置进行缩放。需要一段时间来习惯探索跟踪视图,因为您可能已经习惯了典型的左/右滚动行为,这在查看器中是不存在的。要找到一个位图加载方法调用的细节,您可以在查看器中找到它,然后用鼠标指向它。然后向下滚动,放大你需要的视觉细节。缩放时,查看器会包含更多细节,堆栈中较低的方法调用会被显示和标记。稍后,要查看之前发生的方法调用,您可以向上滚动鼠标滚轮以缩小并查看更多的跟踪。然后,您将指向先前的方法调用,并重复该过程。
可视化查看器下面的表格显示了所有方法调用的细目分类。该细分包括名称、调用计数以及包含和排除时间。所有这些计时都与记录跟踪所花费的时间相关。如果你花 4 秒钟记录一个轨迹,50%的读数相当于 2 秒钟。您可以将鼠标悬停在查看器中的任何方法调用上,等待 2 秒钟,工具提示就会出现,并以毫秒为单位给出准确的时间。
分配跟踪器
分配跟踪器现在内置在 Android Studio 中。它的工作原理与 ADM 类似。单击 Android DDMS 工具窗口下左侧工具栏中的内存跟踪器,开始跟踪分配。在应用运行时与其互动,然后再次点按该按钮以停止跟踪分配。编辑器中将打开一个新的选项卡,显示跟踪的结果。如图 14-11 所示。
图 14-11。
The built-in allocation tracker
屏幕捕获
Android DDMS 窗口包含几个选项,允许你在使用应用时捕捉屏幕。屏幕捕获按钮立即捕获设备的当前屏幕,并在预览对话框中加载图像,您可以选择将其保存到磁盘。图 14-12 显示了该对话框。“屏幕截图”对话框还允许您通过使用手机或平板电脑设计的框架来给图像加框。有放大和缩小屏幕的缩放控件。您可以启用投影、屏幕眩光,甚至在保存之前旋转图像。单击“重新加载”按钮,用当前屏幕渲染的图像刷新对话框。
图 14-12。
Using the Screen Capture tool
屏幕录制按钮允许您在与应用交互时录制屏幕视频。点击该按钮,会出现如图 14-13 所示的对话框,提示您选择录制比特率和分辨率。单击开始录制按钮开始录制并使用您的应用。完成后,单击“停止录制”以生成包含录制的交互的视频文件。另一个对话框将提示您保存记录。使用任何文件名,并将其保存到系统中容易找到的位置。Windows 用户可能需要安装替代编解码器或软件,因为文件以 MP4 格式保存。图 14-14 展示了使用 Windows 上流行的 VLC 播放器回放与 Gradle Weather 应用的交互。
图 14-14。
Playback of a screen recording
图 14-13。
Starting the Screen Recorder tool
导航编辑器
导航编辑器是 Android Studio 中的一个全新功能。虽然在撰写本文时它已经可以使用了,但它仍在大量开发中。该编辑器允许您在进出特定活动和片段的编辑模式时,快速构建应用的高级流程原型。如果你对一个应用有一个粗略的想法,并想想象用户如何在屏幕之间移动,导航编辑器是理想的工具。它还可以发现现有应用中屏幕之间的现有流程和连接。随着时间的推移,看到这个工具成熟将是令人兴奋的。
让自己熟悉它的最好方法就是一个全新的项目。想象一下,你想设计一个新的购物应用,允许用户通过他们喜欢的社交网络凭证快速注册,并随意浏览商品列表。找到一件商品后,用户可以在决定购买前点击它以获得更多细节。要设计这样的流程,您可以使用手绘草图、白板或其他工具,这些工具提供了与您的 IDE 的有限集成。将您的粗略想法转化为功能性应用的过程可能是一个艰巨的过程,外部工具会在您工作时管理多个设计师程序时增加额外的麻烦。在使用 IDE 时,人们经常使用线框或图表工具,如 OmniGraffle、Lucidchart 等。在这些程序之间转换以实现一个工作应用的过程并不总是简单明了的。导航编辑器为您提供了一种方法,可以在您的 IDE 中轻松地构建原型和绘制流程。在本节中,您将使用该工具探索我们的购物应用。
设计用户界面
使用新建项目向导和空白活动模板,创建一个名为 Navigate 的项目。项目加载后,您应该在设计模式下开始编辑activity_main xml 布局。移除 Hello World 标签,拖出一个下面有三个按钮的大文本标签。将标签的文本更改为 Mini-Shopper,并更改按钮上的文本以反映三个虚构的社交网络服务。图 14-15 中的例子使用了 FaceBox、Twiggler 和 G++,但是你可以随心所欲地发挥创造力。
图 14-15。
Designing the FaceBox UI
导航编辑器的第一步
接下来,从主菜单中单击工具 Android ➤导航编辑器。您的屏幕将类似于图 14-16 。
图 14-16。
Opening the Navigation Editor
Android Studio 将创建一个main.nvg.xml文件,并将其呈现在导航编辑器中。它会直观地显示你的活动及其相关的 Android 上下文菜单。(空白活动模板会自动创建此上下文菜单。)该编辑器允许您快速创建新的活动,并将这些活动与现有活动上的控件相关联,以创建转换。它还允许你连接到 Android 系统上下文菜单中的项目。您可以在编辑器中单击并拖动项目,如上下文菜单。
右键单击编辑器中的任意位置,打开带有单个新活动选项的编辑器上下文菜单,如图 14-17 所示。单击此选项打开新建活动向导。选择空白活动模板,并将新活动命名为 FaceBoxLoginActivity。您将返回到导航视图,该视图现在显示这两个活动。
图 14-17。
Create a new activity with the Navigation Editor
连接活动
重新定位新活动,使其与原始活动相邻。你需要在它们之间建立联系。在编辑器中工作时,可以随意重新定位上下文菜单。按住 Shift 键,同时单击 FaceBox 按钮并拖动到新的FaceBoxLoginActivity。编辑器将在它们之间绘制一条连接线,一个粉红色的点代表位于线中间的过渡。单击此点查看过渡的定义。过渡通过按压手势将源MainActivity连接到目的FaceBoxLoginActivity,如图 14-18 所示。
图 14-18。
Connecting activities with the Navigation Editor
现在打开MainActivity.java源文件。您应该看到一个点击监听器连接到启动FaceBoxLoginActivity的按钮:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MainActivity.this.startActivity(new Intent(MainActivity.this,
FaceBoxLoginActivity.class));
}
});
}
这段代码是通过在编辑器中简单的点击和拖动操作生成的。返回导航编辑器,双击FaceBoxLoginActivity。您将被设置到这个活动的图形编辑视图,在这里您可以拖放以用更多的控件和选项来装饰它。创建一个极简的登录界面,有两个TextView标签,两个用于用户名和密码的EditText输入框,最后是一个登录按钮。图 14-19 显示了假装 FaceBox 登录屏幕。
图 14-19。
Designing the FaceBox login screen
编辑菜单
返回导航编辑器,这将反映出FaceBoxLogin布局的变化。您可以运行应用来测试过渡和新的FaceBoxLogin布局更改。在导航编辑器中,双击与登录活动关联的上下文菜单。menu_facebox_login.xml文件将被打开,并在右侧显示一个即时预览窗口。更改菜单中的单个项目,将其 ID 设为@+id/action_back,标题设为@string/action_back。按 Alt+Enter 弹出意图对话框,提示创建新的字符串值资源的动作,如图 14-20 所示。按回车键执行此操作。
图 14-20。
Editing the FaceBox menu
在资源对话框中键入 back 作为新字符串的值,然后按 Enter 键继续。返回导航编辑器。现在,您将从新菜单项建立到MainActivity的连接。像以前一样,按住 Shift 键,从后退菜单项单击并拖动到MainActivity。当您建立新的连接时,编辑器将在MainActivity中生成代码。打开MainActivity.java文件,查看下面生成的代码:
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean result = super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.action_back).setOnMenuItemClickListener(new
MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
FaceBoxLoginActivity.this.startActivity(new Intent(FaceBoxLoginActivity.this,
MainActivity.class));
return true;
}
});
return result;
}
在建立这些连接时,构建并运行应用,以测试过渡是如何进行的。此时,您应该能够从主活动转换到FaceBoxLogin活动,然后通过使用新的上下文菜单项返回到主活动。
现在您已经对导航编辑器的基本用法有了一些了解,试着为应用再创建两个活动,一个用于显示项目列表,一个用于查看项目细节。
末端的
也许您的工具箱中需要的最实用的插件是终端。单击 IDE 底部的“终端”选项卡,打开一个终端窗口,您可以在其中输入操作系统命令。您可以单击绿色加号按钮在单独的选项卡中开始新的会话。当您找不到或记不起等效的 IDE 时,命令窗口可以帮助您完成任务。也许你需要了解的终端中最重要的工具是 ADB,Android 调试桥。此工具使您可以直接控制连接的设备或仿真器。该命令采用adb {device-options} sub-command {sub-command-options}的形式。设备选项如下:-d指向唯一连接的设备,-e指向唯一连接的仿真器,或者-s deviceID指向具有给定 ID 的特定设备。
打开您的终端,研究本节其余部分描述的命令。
查询设备
adb devices
devices子命令列出每个连接设备的名称和设备 id。模拟器将列出一个设备 ID,格式为emulator-<port>。
安装 APK
adb install /path/to/app.apk
install命令会将一个 Android APK 推送到设备上并安装它。只需提供开发机器上 APK 文件的路径。
下载文件
adb pull /path/to/device/file.ext /path/to/local/destination/
pull命令将任意文件从设备下载到您的开发机器上。
上传文件
adb push /path/to/local/file.ext /path/to/device/destination/
push命令将任意文件从您的开发机器上传到设备。
左舷向前
adb forward local-port remote-port
forward命令将把开发机器上的网络连接重定向到设备。这是一种在高级场景中使用的技术,例如调试 Chrome web 浏览器中运行的代码或连接到设备上运行的网络服务器。
谷歌云工具
之前,您探索了一个使用网络服务收集天气预报的 Android 应用。在本节中,您将探索如何使用 Google Cloud tools 开发和部署您自己的后端。首先,您将设计前端,它将与任意 bean 通信以构建问候。稍后,您将构建后端并在本地运行它。最后,您将发布到 Google 的云服务,并对项目进行端到端的测试。首先,您需要使用您的 Google 帐户登录 Google,如图 14-21 所示。
图 14-21。
Sign into Google
创建 HelloCloud 前端
使用空白活动模板创建一个新的 Android 项目,并将其命名为 HelloCloud。将空白活动命名为 MainActivity,然后单击 Finish 开始您的项目。对您的MainActivity使用清单 14-1 中的代码,对您的activity_main.xml布局使用清单 14-2 中的 XML。
Listing 14-1. The MainActivity for the HelloCloud Front End
public class MainActivity extends Activity {
private SimpleCloudBean cloudBean;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setCloudBean(new SimpleCloudBean());
setContentView(R.layout.activity_main);
}
public void onGoClick(View sender) {
final TextView txtResponse = (TextView) findViewById(R.id.txtResponse);
txtResponse.setText(getCloudBean().getResponse());
txtResponse.setVisibility(View.VISIBLE);
}
public SimpleCloudBean getCloudBean() {
return cloudBean;
}
public void setCloudBean(SimpleCloudBean cloudBean) {
this.cloudBean = cloudBean;
}
public class SimpleCloudBean {
public CharSequence getResponse() {
return "This response is from " + getClass().getSimpleName();
}
}
}
Listing 14-2. The activity_main.xml for the HelloCloud Front End
<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android
xmlns:tools="``http://schemas.android.com/tools
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<TextView
android:text="@string/greeting_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/txtGreeting" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="go!"
android:id="@+id/button"
android:layout_below="@+id/txtGreeting"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="42dp"
android:layout_marginTop="72dp"
android:onClick="onGoClick" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Response Shows Here"
android:id="@+id/txtResponse"
android:layout_below="@+id/txtGreeting"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="34dp"
android:visibility="invisible" />
</RelativeLayout>
这段代码调用一个简单的本地 bean,该 bean 返回对活动的响应。响应在一个隐藏的TextView组件中更新,然后被设置为View.Visible。
创建 Java 端点后端模块
现在,您可以向项目中添加一个新的后端模块。这个后端模块将包含在 web 服务器上运行的代码。点击文件➤新建模块,选择谷歌云模块,如图 14-22 所示。
图 14-22。
Create an App Engine module
命名你的模块后端,保留其他选项的默认设置,如图 14-23 所示。点击 Finish,Android Studio 将生成一个基本的 Java servlet 项目,并带有一个随时可用的 Google Cloud 端点。Gradle 将开始用新模块同步您的项目。
图 14-23。
Select App Engine Java Endpoints Module
同步完成后,右键单击项目窗口中的后端模块,然后选择“使模块成为后端”选项。接下来,在运行配置列表中找到后端选项,并单击 run 按钮启动它。Android Studio 将把 servlet 代码包装在本地运行的 Jetty web servlet 引擎的一个实例中,供您探索。控制台会提示如何使用 web 浏览器与终端进行交互。启动您的浏览器并将其指向http://localhost:8080/以查看运行中的端点。您将看到如图 14-24 所示的页面。
图 14-24。
Running your Google Cloud Endpoint
连接零件
在验证端点正在运行之后,您可以让 Android Studio 生成并安装可以在您的 Android 应用中使用的客户端库。在右侧的 Gradle 构建工具窗口中找到后端模块的构建,并运行appengineEndpointsInstallClientLibs任务。如图 14-25 所示。
图 14-25。
Install the client libs for your endpoint
Android Studio 早期版本的菜单中有一个选项,最近从版本 1.0.1 中删除了。在 0.8.x 版本中,您可以单击工具➤谷歌云工具➤安装客户端库。图 14-26 显示了之前的菜单。
图 14-26。
Earlier versions of Android Studio had the task baked into the menu
Android Studio 触发了一个特殊的 Gradle build,它将生成一个可安装的客户端库,作为 Android 客户端和后端 web 服务器之间的代理。Gradle 构建完成后,您可以在后端模块的构建文件夹中的client-libs文件夹下找到作为 ZIP 文件的客户端库。ZIP 文件包含一个readme.html文件,其中包含所有关于如何使用它的说明。寻找编译时依赖项,这些依赖项需要复制到使用端点的模块中。您可以忽略解释如何安装客户端库的额外说明,因为 IDE 会在生成过程中执行此步骤。
在应用模块的build.gradle文件中添加编译时依赖项后,您的dependencies块应该如下所示:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile ([group: 'com.apress.gerber.cloud.backend', name: 'myApi',
version: 'v1-1.19.0-SNAPSHOT'])
compile([group: 'com.google.api-client', name: 'google-api-client-android',
version: '1.19.0'])
// compile project(path: ':backend', <- remove this line
// configuration: 'android-endpoints') <- remove this line
}
当我们向项目中添加新模块时,我们示例中被注释掉的依赖项是自动添加的。应该删除它,因为您不希望应用直接链接到 servlet 代码;相反,它使用客户端库来代理请求。您还必须确保已经将本地 Maven 存储库添加到项目中。打开顶层build.gradle文件并将其添加到allprojects部分:
allprojects {
repositories {
jcenter()
mavenLocal()
}
}
在添加了依赖项和mavenLocal存储库之后,您应该将您的项目与 Gradle build 同步,以使 API 可用。在您的应用模块中添加一个新类来使用它。调用这个类 RemoteCloudBeanAsyncTask 并使其扩展AsyncTask。声明一个MyApi类型的静态变量。应该提示您导入该类,它现在应该在类路径中可用。如果您没有导入它的选项,请仔细检查您的依赖项并重新构建模块,以确保您已经正确地包含了生成的客户端库。清单 14-3 定义了这个新类。
Listing 14-3. The RemoteCloudBeanAsyncTask Class Definition
class RemoteCloudBeanAsyncTask extends AsyncTask<String, Void, String> {
public static final String RESULT = "result";
private static MyApi apiService = null;
private final Handler handler;
public RemoteCloudBeanAsyncTask(Handler handler) {
this.handler = handler;
}
@Override
protected String doInBackground(String... params) {
String name = params[0];
try {
return getMyApi().sayHi(name).execute().getData();
} catch (IOException e) {
return e.getMessage();
}
}
private MyApi getMyApi() {
//Lazily initialize the API service
if(apiService == null) {
MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(),
new AndroidJsonFactory(), null)
// The special 10.0.2.2 IP points to the local machine's IP address
// in the emulator
.setRootUrl("``http://10.0.2.2:8080/_ah/api/
.setGoogleClientRequestInitializer(new
GoogleClientRequestInitializer() {
@Override
public void initialize(AbstractGoogleClientRequest<?>
abstractGoogleClientRequest) throws IOException {
abstractGoogleClientRequest.setDisableGZipContent(true);
}
});
apiService = builder.build();
}
return apiService;
}
@Override
protected void onPostExecute(String result) {
final Message message = new Message();
final Bundle data = new Bundle();
data.putString(RESULT, result);
message.setData(data);
handler.sendMessage(message);
}
}
记住使用AsyncTask很重要,因为初始化服务和进行网络调用需要一些时间。这个对象是用 Android 处理程序实例化的,稍后将在逻辑中使用。我们在doInBackground方法中检索对 API 的引用。返回引用的方法创建并延迟初始化它。获得 API 引用后,调用 web 端点,并返回调用结果。然后,一条消息被发送到onPostExecute方法中的处理程序。
通过修改onGoClick方法将此AsyncTask插入MainActivity;
public void onGoClick(View sender) {
final RemoteCloudBeanAsyncTask remoteCloudBeanAsyncTask =
new RemoteCloudBeanAsyncTask(new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
final String result = msg.getData().getString(RemoteCloudBeanAsyncTask.RESULT);
final TextView txtResponse = (TextView) findViewById(R.id.txtResponse);
txtResponse.setText(result);
txtResponse.setVisibility(View.VISIBLE);
}
});
remoteCloudBeanAsyncTask.execute("Developers");
}
这里我们创建了RemoteCloudBeanAsyncTask并给它一个处理程序,将消息传递给隐藏的文本视图并使其可见。当后端服务器仍然在您的开发机器上运行时,在模拟器上构建并运行这个示例。点击 Go 按钮,您应该会看到来自 Google Cloud 端点的返回消息,如图 14-27 所示。如果您收到指示超时的消息,请仔细检查您的服务器是否仍在运行,并且可以使用 web 浏览器访问。确保您已经在清单中声明了 Internet 权限。您可能还需要更改或禁用已启用的任何主动防火墙设置。
图 14-27。
Running the app on the emulator against the endpoint
部署到应用引擎
既然这项服务已经在本地运行并产生了结果,你可以部署到谷歌的云服务器上。部署到云很简单。如果后端正在本地运行,请停止它。使用您的 Google 帐户在 https://console.developers.google.com/project 登录开发者控制台。单击创建项目按钮,在 Google 的云服务中创建新的端点。给项目起一个名字,比如 MyBackend,然后将生成的项目 ID 复制并保存到一个可以访问的地方。示例见图 14-28 。点击创建,您将看到如图 14-29 所示的进度指示器。给它一点时间让谷歌服务完成这个过程。返回 Android Studio,找到appengine-web.xml文件,并将保存的项目 id 复制到application标签中。这个文件在src ➤ main ➤ webapp ➤ WEB-INF下。
图 14-29。
Google Developers Console will spin momentarily while it works
图 14-28。
Creating a new Java Endpoints project with Google Developers Console
从顶部菜单中,选择构建➤将模块部署到应用引擎。单击“部署到”下拉列表,并选择您的项目 ID。首次部署时,您需要登录 Google 进行选择。图 14-30 显示了点击部署到下拉菜单后的登录屏幕。
图 14-30。
Sign into Google Developers Console
登录提示打开浏览器窗口,如图 14-31 所示。单击“接受”以授予必要的权限。
图 14-31。
Google Developers Console permission prompt
后端发布后,切换到之前创建的AsyncTask并更新加载 API 的方法:
private MyApi getMyApiRemote() {
//Lazily initialize the API service
if(apiService == null) {
MyApi.Builder builder = new MyApi.Builder(
AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null)
.setRootUrl("https://{your-project-id}.appspot.com/_ah/api/");
apiService = builder.build();
}
return apiService;
}
用您在线创建的项目的项目 ID 替换{your-project-id}。在您的设备或模拟器上构建并运行该应用,您应该会得到相同的结果。
摘要
本章探讨了用于分析和设计应用的各种工具。它着眼于从不同方面探索应用性能的许多可用选项。您学会了使用新的导航编辑器快速构建想法的原型,这些想法可以在以后构建到成熟的应用中。最后,您深入研究了 Google 的云服务,了解了如何使用 Google 提供的强大计算引擎来构建、测试和部署客户机服务器应用。这些工具都为您提供了强大的控制和洞察力,可用于构建健壮的应用。