安卓异步编程第二版-全-

安卓异步编程第二版(全)

原文:zh.annas-archive.org/md5/c0428736fdd1c42d1336ce7235255473

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

无论你是 Android 初学者开发者还是经验丰富的 Android 程序员,这本书都将探讨如何实现高效且可靠的多线程 Android 应用程序。

我们将探讨 Android 开发者社区常用的最佳异步结构和技巧,以在主线程之外执行计算密集型或阻塞任务,保持 UI 响应,告诉用户事情进展情况,确保我们完成开始的工作,利用那些强大的多核处理器,并且所有这些都不浪费电池。

通过使用正确的异步结构,许多复杂性被从开发者中抽象出来,使得应用程序源代码更易于阅读和维护,并且更不容易出错。

通过使用逐步指南和代码示例,你将学习如何管理多个线程之间的交互,并避免在两个或更多线程访问共享资源以完成后台工作、更新 UI 或检索最新应用程序数据时可能出现的并发和同步问题。

在这段旅程的终点,你将了解如何构建行为良好的应用程序,拥有平滑、响应式的用户界面,使用户通过快速的结果和始终新鲜的数据感到愉悦。

本书涵盖的内容

第一章,Android 中的异步编程,概述了 Android 进程和线程模型,并描述了一些关于并发的一般挑战和好处,然后讨论了特定于 Android 的问题。

第二章,使用 Looper、Handler 和 HandlerThread 执行工作详细介绍了HandlerHandlerThreadLooper的基本和相关主题,并说明了如何使用它们在主线程上调度任务,以及如何在协作的后台线程之间协调和通信工作。

第三章,探索 AsyncTask,涵盖了 Android 编程中最常见的并发结构。我们学习了AsyncTask的工作原理,如何正确使用它,以及如何避免即使是经验丰富的开发者也容易陷入的常见陷阱。

第四章,探索加载器,介绍了Loader框架,并处理了异步加载数据以保持用户界面响应和避免故障的重要任务。

第五章,与服务交互,我们探讨了非常强大的 Android 组件Service,将其用于执行带有或没有可配置并发级别的长时间运行的后台任务。这个组件为我们提供了在单个Activity生命周期之外执行后台操作的手段,并确保即使在用户离开应用程序的情况下,我们的工作也能完成。

第六章,使用 AlarmManager 安排工作,向我们介绍了一个系统 API,该 API 可用于延迟工作或创建周期性任务。安排的任务可以唤醒设备以完成工作或提醒用户有新内容。

第七章,探索 JobScheduler API,介绍了 Android Lollipop 中引入的一个作业调度系统 API,该 API 允许我们在满足一系列设备条件(如能源或网络)时启动后台工作。

第八章,与网络交互,我们详细介绍了HttpUrlConnection Android HTTP 客户端。使用HttpUrlConnection HTTP 客户端,我们将创建一个异步工具包,能够从远程服务器获取 JSON 文档、XML 或文本。

第九章,在本地层进行异步工作,介绍了 JNI 接口,这是一个 Java 标准接口,将允许我们在本地代码(C/C++)上执行并发任务,从本地层与 Java 代码交互或从本地代码更新 UI。

第十章,使用 GCM 进行网络交互,我们将学习如何使用 Google GCM 高效地从您的服务器推送和拉取实时消息,以及如何使用 Google Play 服务框架安排工作。

第十一章,探索基于总线通信,我们将向读者介绍发布/订阅消息模式和 Event Bus 库,这是一个发布/订阅实现,允许我们在 Android 应用程序组件之间传递异步消息。

第十二章,使用 RxJava 进行异步编程,我们将介绍 RxJava,这是一个库,通过使用可观察数据流,可以轻松地在 Java 中组合异步和基于事件的任务。

您需要为此本书准备的东西

要跟随示例进行实验,您需要一个装有 Java 7(或 8)SE 开发套件和 Android 软件开发套件版本 9 或更高版本的开发计算机(您至少需要版本 21 才能尝试所有示例)。

您还需要 Android Studio IDE。示例已使用 Google 的新 Android Studio IDE 开发,并使用其集成的构建系统 Gradle。

虽然您可以使用 Android SDK 提供的模拟器运行示例,但这只是真实设备的糟糕替代品。物理 Android 设备是开发并测试 Android 应用程序更快、更愉快的方式!

许多示例可以在运行任何版本 Android 2.3(姜饼)或更高版本的设备上运行。一些示例演示了较新的 API,因此需要更近期的 Android 版本——最高到 Android 5,Lollipop。

这本书面向的对象

本书是为希望学习如何使用高级异步技术和概念构建多线程和可靠 Android 应用程序的 Android 开发者而编写的。

他们想学习这项技术,因为他们想了解如何使用 Android 标准构造和 API 构建能够有序与内部/外部服务和框架交互的高效应用程序。

不需要具备并发和异步编程的先验知识。本书也针对新接触 Android 的 Java 专家。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称按照以下方式显示:“我们可以通过使用include指令包含其他上下文。”

代码块按照以下方式设置:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都按照以下方式编写:

# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
 /etc/asterisk/cdr_mysql.conf

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“点击下一步按钮将您带到下一个屏幕。”

注意

警告或重要提示会出现在这样的框中。

提示

技巧和窍门看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载

您也可以通过点击 Packt Publishing 网站上书籍网页上的代码文件按钮下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录您的 Packt 账户。

文件下载完成后,请确保使用最新版本的软件解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Asynchronous-Android-Programming。我们还有其他丰富的图书和视频代码包,可在github.com/PacktPublishing/找到。请查看它们!

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中找到错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,在搜索字段中输入书籍名称。所需信息将在错误清单部分显示。

盗版

互联网上对版权材料的盗版是一个持续存在的问题,在所有媒体中都是如此。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过发送电子邮件至<copyright@packtpub.com>并附上疑似盗版材料的链接与我们联系。

我们感谢您的帮助,保护我们的作者和提供有价值内容的能力。

询问

如果您在这本书的任何方面遇到问题,您可以通过发送电子邮件至<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章. Android 中的异步编程

在过去几年中,异步编程已经成为一个重要的讨论话题,尤其是在使用最新移动硬件上可用的并发处理能力时。

近年来,CPU 上可用的独立处理单元(核心)数量有所增加,为了利用这种新的处理能力,出现了一种新的编程模型,称为异步编程,以协调设备上几个独立硬件处理单元之间的工作。异步编程应运而生,以解决可能由此新处理范式引发的问题。

Android 应用程序,由于它们主要运行在拥有多个处理单元的设备上,应该利用异步编程来在阻塞操作和需要 CPU 密集型任务时进行扩展和提升应用性能。

Android 是一个基于 Linux 内核的开源操作系统(OS),由 Andy Rubin、Nick Sears、Chris White 和 Rick Miner 于 2003 年设计,并于 2005 年 7 月被谷歌收购。

Android 操作系统,实际上由谷歌和开放手机联盟维护,是为了为计算、内存和能源资源有限的设备提供一个开放的移动设备平台而创建的。

该平台已经纳入了高级移动设备标准,如 NFC 和蓝牙低功耗(LE),其范围已从纯智能手机平台扩展到更广泛的软件平台,包括智能手表、电视、平板电脑和游戏机。

自从首次发布以来,维护者一直在定期更新平台,带来了许多新特性和对次要和主要版本的改进。

下图显示了 Android 版本随时间的变化:

Android 中的异步编程

Android 软件栈

Android 软件栈(C 库和 Java 框架),由 Android 运行时(Dalvik VM,最近还有 ART)编排,围绕 Linux 内核创建,旨在在经过充分验证的一组技术之上提供高度交互的用户体验。

在每个新的操作系统版本中,都会为开发者提供一个明确的应用程序接口(API),以便围绕发布时引入的新特性和标准创建应用程序。

Android 应用程序的编译代码(字节码),通常是 Java 编译代码,在基于 Dalvik 或 ART 的虚拟机上运行。

Dalvik 运行时

丹·博斯坦创建的Dalvik 虚拟机DVM)运行时是平台上的第一个运行时,它是一个基于寄存器的虚拟机,旨在在有限的运行时、有限的电源处理、RAM 和电力下有效地运行 Java 代码。

Dalvik 的创造者声称,DVM 平均比标准 Java VM(Oracle)高效约 30%。根据 Bornstein 的说法,它需要 30% fewer instructions 和 35% fewer coding units。

显然,谷歌已经竭尽全力从每一款移动设备中榨取性能,以帮助开发者构建响应式应用程序。

虚拟机在 Linux 进程中运行,具有自己的内存空间和文件描述符,并管理自己的线程组。它还管理自己的线程组。

在更高级的架构中,Android 应用程序可能在单独的进程中运行服务并通过 IPC 机制进行通信,但大多数时候,它在一个自包含的进程中运行。

dex 文件和应用程序资源由 AAPT 打包成Android 应用程序包APK),最终在终端用户的设备上通过 Google Play 安装。

注意

自 2007 年苹果 iPhone 发布以来,应用程序商店的分布模式在移动平台上变得极为流行。

自 Android 2.2 以来,DVM 自带基于跟踪的即时编译JIT)功能,该功能在应用程序运行频繁使用的字节码段(称为跟踪)时进行主动优化。

生成的机器代码在应用程序执行和某些密集型 CPU 任务上提供了显著的性能改进,并因此降低了电池消耗。

ART 运行时

ART 运行时是 DVM 的新版本,旨在提高运行时性能和内存消耗。新的运行时在 Android 4.4 KitKat 中作为实验性运行时引入,自 Android 5.0 Lollipop 以来,它已成为主要的 Android 运行时。

这种新的运行时利用预编译AOT)编译,在启动时间和应用程序执行上带来了新的性能优化。与 DVM JIT(即时)相反,AOT 在安装时使用设备上的 dex2oat 工具编译 dex 文件。由 dex2oat 工具生成的编译代码为目标设备生成系统依赖代码,并消除了每次应用程序执行时 JIT 编译引入的延迟。

AOT 编译器还减少了应用程序使用的处理器周期数,因为它消除了 JIT 编译器将代码转换为机器代码所花费的时间,并且使用更少的电池电量来运行应用程序。

AOT 编译的一个缺点是与 DVM 使用的 JIT 相比,内存占用更大。

新的运行时还在内存分配和垃圾回收GC)方面引入了一些改进,从而实现了更响应的 UI 和更好的应用程序体验。

内存共享和 Zygote

基本上,该平台为每个应用程序运行一个 DVM/ART 实例,但平台的大规模优化是通过创建和管理新的 DVM 实例的方式实现的。

当安卓设备首次启动时,会启动一个称为 Zygote 的特殊进程(动物繁殖中的第一个生命细胞)——所有安卓应用程序都是基于此进程的。

Zygote 启动一个虚拟机,预加载核心库,并初始化各种共享结构。然后它通过监听套接字等待指令。

当一个新的安卓应用程序启动时,Zygote 接收一个命令来创建一个虚拟机以运行该应用程序。它是通过复制其预热好的 VM 进程并创建一个新的子进程来实现的,该子进程与父进程共享一些内存部分,使用的技术称为写时复制COW)。

可在大多数 Unix 系统上使用的 COW 技术仅在进程尝试更改从父进程克隆的内存时才在子进程中分配新内存。

这种技术有一些非常显著的好处,如下所示:

  • 首先,虚拟机和核心库已经加载到内存中。无需从文件系统读取这块重要数据来初始化虚拟机,这极大地减少了启动开销。

  • 其次,这些核心库和常见结构所在的内存由 Zygote 与其他所有应用程序共享,当用户运行多个应用程序时,这可以节省大量内存。

安卓进程模型

安卓是一个多用户、多任务系统,可以并行运行多个应用程序,其中所有应用程序都试图获取 CPU 时间来执行其任务。

每个应用程序都在一个独立的 Linux 进程中独立运行,该进程是从 Zygote 进程克隆出来的,并且默认情况下,所有安卓组件都在与应用程序包中指定的相同名称的进程中运行,该名称在安卓应用程序清单AAM)中指定。

Linux 内核将为应用程序执行分配少量 CPU 时间,称为 CPU 时间片。这种时间片方法意味着即使单处理器设备也能同时看起来在多个应用程序中积极工作,而实际上,每个应用程序都在 CPU 上轮流执行非常短暂的时间。

进程优先级

安卓操作系统试图尽可能长时间地保持应用程序运行,但当可用内存低时,它将通过首先终止重要性较低的过程来尝试通过释放系统资源。

这就是进程优先级发挥作用的时候;安卓进程按照以下五个类别从高优先级到低优先级进行排序:

  • 前台进程:这是一个托管用户当前与之交互的活动或服务的进程:在前台启动的服务或正在运行其生命周期回调的服务

  • 可见进程:这是一个托管暂停活动或与可见活动绑定的服务的进程

  • 服务进程:这是一个托管未绑定到可见活动的服务的进程

  • 后台进程:这是一个承载非可见活动的进程;所有后台进程都按最近最少使用LRU)列表排序,因此,最近使用的进程是当它们具有相同排名时最后被杀死的进程。

  • 空进程:这是一个用于缓存不活跃的 Android 组件并提高任何组件启动时间的进程。

当系统达到需要释放资源的状态时,可被杀死的进程将按进程排名、最后使用进程和运行的组件等因素进行排序。

进程沙箱化

Android 应用程序始终在安装应用程序期间分配给应用程序的唯一Linux 用户 IDUID)下运行,以便进程在沙箱环境中运行,默认情况下,将隔离您的数据和代码执行与其他应用程序。

在某些情况下,可能需要用户明确地与其他应用程序共享 UID 以访问其数据:

USER     PID   PPID  VSIZE  RSS  PC  NAME
root            319   1     1537236 31324 S zygote
….
u0_a221   5993  319   1731636 41504 S com.whatsapp
u0_a96    3018  319   1640252 29540 S com.dropbox.android
u0_a255   4892  319   1583828 34552 S com.accuweather.android…

在 Android SDK Table 计算机上运行adb shell ps命令生成的先前表格是 Android 运行进程的列表。

第一列显示了在安装时分配的用户标识符UID),第二列是进程 IDPID),第三列显示了父进程 IDPPID),对于 Android 应用来说,这是 Zygote 进程,最后一列显示了应用程序包。

从这个列表中,我们可以确认 WhatsApp 应用程序正在用户 ID u0_a221下运行,进程 ID 为5993,父进程是 Zygote 进程,PID 为319

Android 线程模型

在 Android 进程中,可能有多个执行线程。每个线程都是整体程序中的一个独立的顺序控制流——它按顺序执行其指令,一个接一个,它们还共享由操作系统任务调度器管理的分配的 CPU 时间片。

当系统启动应用程序进程并阻止其直接干扰其他进程的内存地址空间中的数据时,线程可能由应用程序代码启动,并且可以与同一进程内的其他线程进行通信和共享数据。除了在同一进程中共享的所有线程共享的数据外,一个线程可以使用其自己的内存缓存来存储其数据在自己的内存空间中。

主线程

当应用程序进程启动时,除了 DVM 维护线程外,系统还会创建一个名为main的执行线程。这个线程,正如其名所解释的,在应用程序生命周期中扮演着至关重要的角色,因为它是与 Android UI 组件交互、更新设备屏幕上状态和外观的线程。

此外,默认情况下,所有 Android 应用程序组件(ActivityServiceContentProviderBroadcastsReceiver)也是通过主线程执行线执行的。以下图像显示了应用程序进程内运行的线程列表,其中主线程位于列表顶部,并分配了一个系统指定的唯一线程 IDTID):

主线程

主线程,也称为 UI 线程,是处理 UI 事件发生的线程,因此为了尽可能保持应用程序的响应性,你应该:

  • 避免任何可能长时间阻塞处理的长执行任务,例如可能无限期阻塞处理的输入/输出I/O)任务

  • 避免可能导致此线程长时间占用的 CPU 密集型任务

以下图表显示了Looper执行线程中的主要交互和组件:

主线程

UI/Main线程,它附加了一个Looper设施,持有要按顺序执行的一些工作单元的消息队列(MessageQueue)。

当队列中有消息准备好被处理时,Looper 线程从队列中弹出消息并将其同步地转发到消息上指定的目标处理器。

当目标Handler完成当前消息的处理后,Looper线程将准备好处理队列中可用的下一个消息。因此,如果Handler花费了显著的时间处理消息,它将阻止Looper处理其他挂起的消息。

例如,当我们在一个Activity类的onCreate()方法中编写代码时,它将在主线程上执行。同样,当我们向用户界面组件附加监听器以处理点击和其他用户输入手势时,监听器回调将在主线程上执行。

对于执行少量 I/O 或处理的应用程序,例如不进行复杂数学计算的应用程序,不使用网络实现功能,也不使用文件系统资源的应用程序,这种单线程模型是可行的。然而,如果我们需要执行 CPU 密集型计算,从持久存储中读取或写入文件,或与网络服务通信,那么在我们完成这项工作期间到达的任何进一步的事件都将被阻塞。

注意

自 Android 5.0(Lollipop)以来,引入了一个名为RenderThread的新重要线程,以保持 UI 动画的平滑,即使主线程正忙于执行任务。

应用程序无响应(ANR)对话框

如你所想,如果主线程正忙于进行繁重的计算或从网络套接字读取数据,它无法立即响应用户输入,如点击或滑动。

一个对用户交互反应不快的应用程序会感觉不响应——超过几百毫秒的延迟都是可以察觉的。这是一个非常有害的问题,Android 平台通过保护用户免受在主线程上执行过多操作的应用程序的侵害来解决这个问题。

注意

如果一个应用程序在五秒内没有响应用户输入,用户将看到应用程序无响应ANR)对话框,并可以选择退出应用程序。

以下截图显示了典型的 Android ANR 对话框:

应用程序无响应(ANR)对话框

Android 努力同步用户界面重绘与硬件刷新率。这意味着它旨在以每秒 60 帧的速率重绘——即每帧 16.67 毫秒。如果我们主线程上的工作需要接近 16 毫秒,我们就有可能影响帧率,导致卡顿——动画卡顿、滚动不流畅等等。

当然,理想情况下,我们不想丢掉任何一帧。卡顿、不响应,尤其是 ANR,提供了一个非常糟糕的用户体验,这转化为差评和不受欢迎的应用程序。在构建 Android 应用程序时遵循的一个规则是:不要阻塞主线程!

注意

Android 在每个设备上的开发者选项中提供了一个有用的严格模式设置,当应用程序在主线程上执行长时间运行的操作时,它会在屏幕上闪烁。

在 Honeycomb(API 级别 11)中,平台增加了额外的保护措施,引入了一个新的Exception类,NetworkOnMainThreadException,它是RuntimeException的子类,当系统检测到在主线程上发起的网络活动时会被抛出。

维护响应性

那么,理想情况下,我们可能希望将任何长时间运行的操作从主线程卸载,以便它们可以在后台由另一个线程处理,而主线程可以继续平滑地处理用户界面更新,并及时响应用户交互。

应该在后台线程中处理的典型耗时任务包括以下内容:

  • 网络通信

  • 本地文件系统上的输入和输出文件操作

  • 图像和视频处理

  • 复杂的数学计算

  • 文本处理

  • 数据编码和解码

为了使其有用,我们必须能够协调工作并安全地在协作线程之间传递数据——特别是在后台线程和主线程之间,这正是异步编程被用来解决这个问题的原因。

让我们从同步与异步的对比图开始:

维护响应性

以下示例直观地显示了两种处理模型之间的主要区别。在左侧,数据下载任务在主线程上执行,直到下载数据完成,使线程保持忙碌。因此,如果用户与 UI 交互并生成一个事件,例如触摸事件,如果下载任务需要大量时间才能完成,应用程序将出现延迟或无响应。

在右侧,异步模型会将下载数据任务交给另一个后台线程,保持主线程可用以处理来自 UI 交互的任何事件。当下载的数据可用时,后台任务可以将结果发布到主线程,如果数据处理需要更新任何 UI 状态。

当我们使用异步模型来编程我们的应用程序时,Android 操作系统也会利用最新设备中可用的额外 CPU 核心同时执行多个后台线程,从而提高应用程序的电源效率。

注意

这种可能相互交互的独立代码路径的并行执行称为并发

将工作子单元并行执行以完成一个工作单元的操作称为并行性

Android 中的并发

如前所述,为了在多核设备环境中实现可扩展的应用程序,Android 开发者应该能够创建并发执行行,这些执行行从多个资源中组合和汇总数据。

Android SDK,因为它基于 Java SDK 的一个子集,源自 Apache Harmony 项目,提供了对低级并发构造的访问,例如java.lang.Threadjava.lang.Runnable以及synchronizedvolatile关键字。

这些构造是实现并发和并行性的最基本构建块,所有高级异步构造都是围绕这些构建块创建的。

最基本的一个是java.lang.Thread,这是最常使用的类,也是创建 Java 程序中新的独立执行线的构造。

public class MyThread extends Thread {
    public void run() {
        Log.d("Generic", "My Android Thread is running ...");
    }
}

在前面的代码中,我们继承了java.lang.Thread以创建我们自己的独立执行线。当Thread启动时,将自动调用 run 方法,并在 Android 日志上打印消息。

MyThread myThread = new MyThread();
myTread.start();

在此时,我们将创建我们的MyThread实例,并在第二行启动它时,系统将在进程内部创建一个线程并执行run()方法。

其他有用的线程相关方法包括以下内容:

  • Thread.currentThread():这检索当前运行的线程实例。

  • Thread.sleep(time):这将在给定时间段内暂停当前线程的执行。

  • Thread.getName()Thread.getId():这些分别获取名称和 TID,以便它们在调试目的上是有用的。

  • Thread.isAlive(): 这检查线程是否目前正在运行,或者它是否已经完成了其工作。

  • Thread.join(): 这将阻塞当前线程,直到被访问的线程完成其执行或死亡。

Runnable 接口,这是来自 Java API 的另一个构建块,是一个定义了用于指定和封装旨在由 Java 线程实例或任何其他处理此 Runnable 的类执行的代码的接口:

package java.lang;

public interface Runnable {   
    public abstract void run();
}

在以下代码中,我们基本上创建了 Runnable 子类,以便它实现 run() 方法,可以被线程传递并执行:

public class MyRunnable implements Runnable {

    public void run(){
        Log.d("Generic","Running in the Thread " +
                        Thread.currentThread().getId());
	// Do your work here
	...
    }
}

现在,我们的 Runnable 子类可以被传递给 Thread,并在并发执行行中独立执行:

Thread thread = new Thread(new MyRunnable());
thread.start();

虽然启动新线程很容易,但并发实际上是一个非常困难的事情。并发软件面临许多问题,可以分为两大类:正确性(产生一致和正确的结果)和活性(向完成迈进)。Thread 创建也可能导致一些性能开销,过多的线程可能会降低性能,因为操作系统将在这些执行行之间切换。

并发程序中的正确性问题

正确性问题的一个常见例子是当两个线程需要根据其当前值修改相同变量的值时。让我们考虑我们有一个 myInt 整数变量,其当前值为 2。

为了增加 myInt 的值,我们首先需要读取它的当前值,然后将其加 1。在单线程世界中,这两个增加操作将按严格的顺序发生——我们将读取初始值 2,将其加 1,然后将新值设置回变量,并重复此过程。在两次增加之后,myInt 保持值为 4。

在多线程环境中,我们将遇到潜在的时间问题。可能两个尝试增加变量的线程都会读取相同的初始值 2,将其加 1,并将结果(在两种情况下都是 3)设置回变量:

int myInt = 2;
...
public class MyThread extends Thread {

    public void run() {
         super.run();
         myInt++;
   }
}
...
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();

两个线程在其局部世界的视角中表现正确,但在整体程序方面,我们显然会存在一个正确性问题;2 + 2 不应该等于 3!这种时间问题被称为竞态条件。

解决如竞态条件等正确性问题的常见方法是互斥——防止多个线程同时访问某些资源。通常,这是通过确保线程在读取或更新共享数据之前获取独占锁来实现的。

为了实现这种正确性,我们可以使用 synchronized 构造来在以下代码片段上解决正确性问题:

Object lock = new Object();
public class MyThread extends Thread {
    public void run() {
        super.run();
        synchronized(lock) {
            myInt++;
        }
    }
}

在前面的代码中,我们使用了每个 Java 对象中可用的内建锁来创建一个互斥代码区域,这将确保增量语句能够正确工作,并且不会像之前解释的那样出现正确性问题。当一个线程获得对受保护区域的访问时,我们说该线程获得了锁,当线程离开受保护区域后,它释放了可以被另一个线程获得的锁。

创建互斥作用域的另一种方法是创建一个带有同步方法的函数:

int myInt = 2;
synchronized void increment(){
    myInt++;
}
...
public class IncrementThread extends Thread {
    public void run() {
        super.run();
        increment();
    }
}

同步方法将使用对象内建的锁,其中myInt被定义为创建一个互斥区域,这样IncrementThread通过increment()方法增加myInt将防止任何线程干扰和内存一致性错误。

并发程序中的活性问题

活性可以理解为应用程序执行有用工作并朝着目标前进的能力。活性问题往往是解决正确性问题时的不幸副作用。

在适当的并发程序中,应同时实现这两个属性,尽管正确性关注的是防止程序在进展中发生死锁、活锁或饥饿,而正确性关注的是产生一致和正确的结果。

注意

死锁是一种情况,其中两个或更多线程无法继续前进,因为每个线程都在等待其他线程做某事。活锁是一种情况,其中两个或更多线程在响应其他线程状态的变化时不断改变自己的状态,但没有做任何有用的工作。

通过锁定对数据或系统资源的访问,可能会创建瓶颈,其中许多线程都在争夺访问单个锁,导致潜在的显著延迟。

更糟糕的是,当使用多个锁时,可能会出现一种情况,即没有任何线程可以继续前进,因为每个线程都需要对另一个线程当前拥有的锁进行独占访问——这种情况被称为死锁。

线程协调

线程协调是并发编程中的一个重要主题,尤其是在我们想要执行以下任务时:

  • 同步线程对共享资源或共享内存的访问:

    • 共享数据库、文件、系统服务、实例/类变量或队列
  • 在一组线程内协调工作和执行:

    • 并行执行、流水线执行、相互依赖的任务等

当我们想要协调线程的努力以实现目标时,我们应该尽量避免等待或轮询机制,这些机制在等待另一个线程中的事件时会让 CPU 保持忙碌。

以下示例展示了一个小循环,我们将在此循环中持续占用 CPU,同时等待某个状态变化发生:

while(!readyToProcess) {
  // do nothing .. busy waiting wastes processor time.
}

为了克服协调问题并实现我们自己的结构,我们应该使用一些低级信号或消息机制在线程之间进行通信并协调交互。

在 Java 中,每个对象都有wait()notify()notifyAll()方法,这些方法提供了低级机制在多个线程之间发送信号并将线程置于等待状态,直到满足条件。

这种机制,也称为监控器守卫,是在其他语言中常用的一种设计模式,它确保在任何给定时间只有一个线程可以进入代码的特定部分,并且能够等待直到条件发生。

与我们之前的示例相比,这种设计模式在等待另一个线程发生特定情况时,提供了更好的和高效的 CPU 周期管理,并且通常用于需要在不同执行线路之间协调工作的情况。

在下面的代码示例中,我们将解释如何使用这个结构创建一个基本的具有 10 个线程的多线程Logger,这些线程将在监控区域等待,直到其他线程在应用程序中推送(条件)消息。

负责记录输出的Logger有一个最多 20 个位置的队列来存储新的日志文本消息:

public class Logger {
    LinkedList<String> queue = new LinkedList<String>();
    private final int MAX_QUEUE_SIZE = 20;
    private final int MAX_THREAD_COUNT = 10;

在接下来的代码中,我们将创建一个无限运行的Runnable工作单元,从队列中检索消息并在 Android 日志上打印消息。

之后,我们将创建并启动 10 个线程,这些线程将执行Runnable工作单元task

public void start() {
    // Creates the Loop as a Runnable
    Runnable task = new Runnable() {
        @Override
        public void run() {
            while(true) {
                String message = pullMessage();
                Log.d(Thread.currentThread().
                         getName(),message);
		     // Do another processing
             }
         }
     };
    // Create a Group of Threads for processing
    for(int i=0; i< MAX_THREAD_COUNT; i++){
         new Thread(task).start();
    }
 }

pullMessage(),这是一个synchorized方法,当它达到wait()方法时运行互斥并将线程置于等待状态。所有创建的线程都将保持这种状态,直到另一个线程调用notifyAll()

// Pulls a message from the queue
// Only returns when a new message is retrieves
// from the queue.
private synchronized String pullMessage(){
    while (queue.isEmpty()) {
        try {
            wait();
        } catch (InterruptedException e) { ... }
    }
    return queue.pop();
}
// Push a new message to the tail of the queue if
// the queue has available positions
public synchronized void pushMessage(String logMsg) {
    if ( queue.size()< MAX_QUEUE_SIZE ) {
        queue.push(logMsg);      
        notifyAll();
    }
}

当任何线程处于等待状态时,它会暂时释放锁,给其他线程一个进入互斥区域以推送新消息或进入等待状态的机会。

在下面的代码片段中,我们首先创建Logger实例,然后调用 start 方法来启动工作线程,并将 10 条消息推入待处理的工作队列。

当调用pushMessage()方法时,一个新的日志消息会被插入到队列的末尾,并且调用notifyAll()来通知所有可用的线程。

由于pullMessage()方法在互斥(同步)区域运行,只有一个线程会被唤醒并从pull方法返回。一旦pullMessage()返回,日志消息就会被打印:

Logger logger =new Logger();
logger.start();
for ( int i=0; i< 10 ; i++) {
    ...
    logger.pushMessage(date+" : "+"Log Message #"+i);
}

在下面的控制台输出中,我们有一个这个代码将生成的输出示例,并且日志消息是由任何可用的线程以有序的方式处理的:

D/Thread-108(23915): <Date>: Log Message #0
D/Thread-109(23915): ...: Log Message #1
D/Thread-110(23915): ...: Log Message #2
D/Thread-111(23915): ...: Log Message #3

这种低级结构也可以用来控制共享资源(轮询)以管理后台执行(并行性)和控制线程池。

并发包结构

java.util.concurrent提供的其他 Java 并发结构,在 Android SDK 中也可用,如下所示:

  • 锁对象java.util.concurrent):它们通过更高级别的惯用语实现锁定行为。

  • Executors:这些是用于启动和管理一组线程执行的高级 API(ThreadPool等)。

  • 并发集合:这些是更改集合的方法受到同步问题保护的集合。

  • 同步器:这些是高级结构,用于协调和控制线程执行(信号量、循环屏障等)。

  • 原子变量java.util.concurrent.atomic):这些是提供对单个变量线程安全操作的类。一个例子是AtomicInteger,可以在我们的示例中用来解决正确性问题。

一些 Android 特定的结构将这些类用作基本构建块来实现它们的并发行为,尽管开发者也可以使用这些类来构建自定义并发结构以解决特定用例。

Executor 框架

Executor框架是java.util.concurrent上可用的另一个框架,它提供了一个提交Runnable任务的接口,将任务提交与任务运行方式解耦:

public interface Executor {
  void execute(Runnable command);
}

每个Executor,它实现了我们之前定义的接口,可以通过多种方式管理异步资源,如线程创建、销毁和缓存,以及任务排队,以实现针对特定用例的完美行为。

java.util.concurrent提供了一组开箱即用的实现,涵盖了大多数通用用例,如下所示:

  • Executors.newCachedThreadPool(): 这是一个可以增长和重用先前创建的线程的线程池

  • Executors.newFixedThreadPoolnThreads):这是一个具有固定线程数和用于存储工作的消息队列的线程池

  • Executors.newSingleThreadPool(): 这与newFixedThreadPool类似,但只有一个工作线程

要在Executor上运行任务,开发者必须通过传递Runnable作为参数来调用execute()

public class MyRunnable implements Runnable {
    public void run() {
        Log.d("Generic", "Running From Thread " +
              Thread.currentThread().getId());   
	 // Your Long Running Computation Task
    }
}
public void startWorking(){
    Executor executor = Executors.newFixedThreadPool(5);
    for ( int i=0; i < 20; i++ ) {
        executor.execute(new MyRunnable());
    }
}

在前面的代码中,我们使用固定数量的五个线程的工厂方法创建了ThreadPool,以便处理工作。

在创建ExecutorService实例后,新的Runnable任务被提交以进行异步处理。

当提交新的工作单元时,选择一个空闲的线程来处理任务;但当所有线程都忙碌时,Runnable将在本地队列中等待,直到有线程准备好工作。

Android 主要构建块

一个典型的 Android 应用程序由以下四个主要构建块组成:

  • android.app.Activity

  • android.app.Service

  • android.content.BroadcastReceiver

  • android.content.ContentProvider

活动、服务和BroadcastReceiver可以通过异步消息Intent显式或隐式地激活。

这些构建块各自都有自己的生命周期,因此如果使用异步架构从主线程卸载工作,它们可能会遇到不同的并发问题。

活动并发问题

Activity 构建块与表示层有紧密的联系,因为它管理着在定义的片段和视图树上的 UI 视图,这些视图显示信息并响应用户交互。

Android 应用程序通常由一个或多个android.app.Activity的子类组成。Activity 实例有一个非常明确的生命周期,系统通过执行生命周期方法回调来管理它,所有这些回调都是在主线程上执行的。

为了保持应用程序的响应性和反应性,以及活动转换的平滑,开发者应该了解每个 Activity 生命周期回调的本质。

Activity 生命周期中最重要的回调如下:

  • onCreate(): 在此状态下,Activity 不可见,但所有私有 Activity 资源(视图和数据)都是在这里创建的。为了减少用户在 Activity 转换期间得不到视觉反馈的时间,应异步执行长时间和密集的计算。

  • onStart(): 当 UI 可见但无法在屏幕上交互时,会调用此回调。任何在此处产生的触摸事件都会被系统错过,这可能会让用户感到愤怒。

  • onResume(): 当 Activity 即将进入前台并处于可交互状态时,会调用此回调。

  • onPause(): 当 Activity 即将进入后台且不可见时,会调用此回调。计算应该迅速结束,因为下一个 Activity 不会在当前方法结束之前恢复。

  • onStop(): 当 Activity 不再可见但可以重新启动时,会调用此回调。

  • onDestroy(): 当 Activity 实例即将在后台被销毁时,会调用此回调。属于此实例的所有资源和引用都必须被释放。

完成的 Activity 实例应该有资格进行垃圾回收,但引用 Activity 或其视图层次结构一部分的后台线程可以防止垃圾回收并造成内存泄漏。

同样,如果结果永远不会显示,因为 Activity 已完成,继续进行后台工作很容易浪费 CPU 周期(以及电池寿命)。

最后,Android 平台可以在任何时间自由地杀死不是用户当前焦点的进程。这意味着如果我们有长时间运行的操作需要完成,我们需要某种方式让系统知道不要杀死我们的进程。

所有这些都使得“不要阻塞主线程”规则变得更加复杂,因为我们需要及时取消后台工作或在适当的时候将其与 Activity 生命周期解耦。

操作用户界面

另一个 Android 特有的问题不在于您可以使用 UI 线程做什么,而在于您不能做什么。

注意

您不能从除主线程以外的任何线程操作用户界面。

这是因为用户界面工具包不是线程安全的,也就是说,从多个线程访问它可能会导致正确性问题。实际上,用户界面工具包通过积极拒绝从创建这些组件以外的线程访问用户界面组件来保护自己免受潜在问题的侵害。

如果系统检测到这一点,它将通过抛出CalledFromWrongThreadException立即通知应用程序。

最终的挑战在于安全地同步后台线程与主线程,以便主线程可以使用后台工作的结果更新用户界面。

如果开发者可以访问Activity实例,可以使用runOnUiThread实例方法从后台线程更新 UI。

该方法接受一个Runnable对象,就像用于创建线程执行任务的执行任务一样:

public final void runOnUiThread (Runnable)

在以下示例中,我们将使用这个设施来发布由后台线程处理的同义词搜索的结果。

为了在OnCreate活动回调期间实现目标,我们将设置onClickListener在创建的线程上运行searchTask

// Get the Views references
Button search = (Button) findViewById(R.id.searchBut);
final EditText word = (EditText) findViewById(R.id.wordEt);

// When the User clicks on the search button 
// it searches for a synonym
search.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Runnable that Searchs for the synonym and
        // and updates the UI.
        Runnable searchTask = new Runnable() {
            @Override
            public void run() {
                // Retrieves the synonym for the word
                String result = searchSynomim(
                   word.getText().toString());
                // Runs the Runnable SetSynonymResult
                // to publish the result on the UI Thread
                runOnUiThread(new SetSynonymResult(result));
            }
        };
        // Executes the search synonym an independent thread
        Thread thread = new Thread(searchTask);
        Thread.start();
    }
});

当用户点击搜索按钮时,我们将创建一个用于搜索在R.id.wordEt EditText中输入的单词的Runnable匿名类,并启动线程来执行Runnable

当搜索完成后,我们将创建一个Runnable实例SetSynonymResult,在 UI 线程上发布结果到同义词TextView

class SetSynonymResult implements Runnable {
    final String synonym;

    SetSynonymResult(String synonym){
      this.synonym = synonym;
    }
    public void run() {
      TextView tv = (TextView)findViewById(R.id.synonymTv);
      tv.setText(this.synonym);
    }
};

这种技术有时并不是最方便的,尤其是在我们没有访问 Activity 实例的情况下;因此,在接下来的章节中,我们将讨论更简单、更干净的从后台计算任务更新 UI 的技术。

服务并发问题

这些是在后台运行的 Android 实体,通常在不需要任何用户交互的name应用程序中执行任务。

默认情况下,Service在应用程序进程的主线程中运行。它不会创建自己的线程,因此如果您的Service要进行任何阻塞操作,例如下载图片、播放视频或访问网络 API,用户应该设计一种策略将工作的时间从主线程卸载到另一个线程。

由于Service可以有自己的并发策略,它还应该考虑到,就像 Activity 一样,它应该通过主线程更新 UI,因此从后台将结果回传到主循环的策略是必不可少的。

在 Android 服务领域,启动服务的方式将Service的性质分为以下两组:

  • 已启动服务:这是通过startService()启动的服务,即使启动它的组件被销毁,它也可以持续运行。已启动的服务不会直接与启动它的组件交互。

  • 绑定服务:此服务在至少有一个 Android 组件通过调用bindService()绑定到它时存在。它为组件之间的通信提供了一个双向(客户端-服务器)通信通道。

已启动服务问题

当我们实现已启动服务时,任何应用程序组件都可以在调用startService(Intent)方法时启动它。一旦系统收到startService(Intent)并且服务尚未启动,系统会调用onCreate()然后调用onStartCommand(),并将封装在 Intent 对象上的参数传递给它们。如果Service已经存在,则只调用onStartCommand()

已启动服务使用的回调如下:

// Called every time a component starts the Service
// The service arguments are passed over the intent
int onStartCommand(Intent intent, int flags, int startId)

// Used to initialize your Service resources
void onCreate()

// Used to release your Service resources
void onDestroy()

onStartCommand()回调中,一旦需要处理服务请求的长时间计算任务,应明确实现并协调向后台线程的移交,以避免不希望的 ANR:

int onStartCommand (Intent intent, int flags, int startId){
    // Hand over the request processing to your
    // background tasks
...
}

当服务完成时,并且需要将结果发布到 UI,应使用适当的技巧与主线程进行通信。

绑定服务问题

绑定服务通常在需要 Android 组件与服务之间强交互时使用。

当服务在同一个进程中运行时,Android 组件(客户端)与绑定服务(服务器)之间的交互始终由onBind()返回的Binder类提供。有了Binder实例,客户端可以访问服务公开的方法,因此当任何组件调用绑定服务的公共方法时,该组件应该意识到以下内容:

  • 当预期在方法调用期间发生长时间运行的操作时,调用必须在单独的线程中发生

  • 如果方法在分离的线程中调用,并且服务想要更新 UI,服务必须在主线程上运行更新:

    public class MyService extends Service {
    
        // Binder given to clients
        private final IBinder mBinder = new MyBinder();
    
         public class MyBinder extends Binder {
             MyService getService() {
                 // Return this instance of MyService
                 // so clients can call public methods
                 return MyService.this;
            }
        }
        @Override
    
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
         /** Method for clients */
        public int myPublicMethod() {
          //
        }
    ...
    

在单独的进程中提供服务

当 Android 服务在其自己的进程中运行时,它在一个独立的进程中运行,有自己的地址空间,这使得与主进程 UI 线程的通信更难实现以下内容:

<service
  android:name="SynonymService"
  android:process=":my_synonnym_search_proc"
  android:icon="@drawable/icon"
  android:label="@string/service_name"
  >
</service>

要在不同的进程中实现服务,我们需要使用进程间通信IPC)技术来在您的应用程序和服务之间发送消息。

注意

IPC(进程间通信)是在多个进程之间共享数据的活动,通常使用一个定义良好的通信协议。它通常包括一个充当客户端的进程和一个充当服务器的进程。

Android SDK 中提供了两种技术来实现这一点,如下所示:

  • AIDL(Android 接口定义语言):这允许你在一系列原始类型上定义一个接口。它允许你创建多线程处理服务,但也会给你的实现增加其他复杂度。这仅推荐给高级程序员。

  • 信使(Messenger):这是一个简单的接口,在服务端为你创建一个工作队列。它会在一个由Handler管理的单线程上顺序执行所有任务。

我们还没有对这些技术给出更多细节;然而,这个结构的一个示例将在更高级的章节中展示,其中涉及的所有概念都更加成熟。

广播接收器并发问题

这个构建块是一个订阅系统和应用事件的组件,当这些事件在系统上发生时,它会收到通知。广播接收器在应用程序清单中静态定义,或者通过Context.registerReceiver()动态定义。

广播接收器通过onReceive()回调激活,此方法在主线程上运行,如果我们尝试执行耗时任务,会阻塞另一个 Android 组件的运行。

一旦onReceive()方法执行完毕,系统会认为该对象不再活跃,可以释放与该实例关联的资源,并回收整个对象。这种行为对我们内部能做什么有着巨大的影响,因为如果我们将一些处理任务交给一个并发线程,属于BroadcastReceiver的资源可能会被回收,从而不再可用,或者在极端情况下,如果没有在它上面运行重要组件,进程可能会被终止。

小贴士

Android 11 版本在广播接收器中引入了goAsync()方法,以便在从onReceive()函数返回后保持广播活跃。

Android 并发结构

好消息是,Android 平台提供了特定的结构来解决并发一般问题和解决 Android 提出的特定问题。

存在一些结构允许我们将任务推迟到稍后主线程上运行,便于协作线程之间的通信,并将工作分配给管理的工作线程池,并将结果重新集成到主线程中。

有解决方案可以解决 Activity 生命周期的限制,无论是涉及用户界面的中期操作,还是即使用户离开应用程序也必须完成的长久期工作。

虽然其中一些结构仅在新版本的 Android 平台上引入,但它们可以通过支持库使用,并且除了少数例外,本书中的示例针对的是运行 API 级别 8(Android 2.2)及更高版本的设备。

摘要

在本章中,我们详细探讨了可用的 Android 运行时、Android 进程和线程模型。

然后,我们介绍了在尝试实现健壮的并发程序时可能会遇到的并发问题。

最后,我们列出了 SDK 上可用的基本并发构建块,以设计并发程序。

在下一章中,我们将探讨一些特定于 Android 的低级构建块,其他并发机制都是基于这些构建块构建的:HandlerLooperLooperThread

第二章:使用 Looper、Handler 和 HandlerThread 执行工作

在前一章中,你被介绍到开发响应式和并发 Android 应用程序时开发者可能遇到的最基本的并发问题。由于最可交互的项目运行在主线程上,协调后台代码以处理工作而不产生任何影响用户体验的 UI 卡顿至关重要。

在本章中,我们将遇到在 Android 系统中执行任务和调度主线程或开发者创建的普通后台线程以执行和调度长时间运行操作的一些最基本构造。

我们将涵盖以下主题:

  • 理解 Looper

  • 理解 Handler

  • 将工作发送到 Looper

  • 使用 post 调度工作

  • 使用 Handler 延迟工作

  • 泄露隐式引用

  • 泄露显式引用

  • 使用 Handler 更新 UI

  • 取消挂起的消息

  • 使用 Handler 和 HandlerThread 进行多线程处理

  • Handler 和 HandlerThread 的应用

理解 Looper

在我们理解Looper之前,我们需要了解其名称的来源。

注意

循环是一组不断重复执行的指令,直到满足终止条件。

根据这个定义,Android 的Looper在一个具有MessageQueue的线程上执行,执行连续循环等待工作,并在没有挂起工作的情况下阻塞。当工作提交到其队列时,它将派发到在Message对象上显式定义的目标Handler

注意

消息是一个包含描述和任意数据对象的通告对象,可以发送到 Handler。

Android 中的 Looper 是实现了一个常见的 UI 编程概念,称为事件循环。稍后,在处理序列的末尾,Handler将处理Message并执行你的领域逻辑,以解决应用程序用户问题。

Android 中的Looper序列遵循以下步骤:

  1. 等待从其消息队列中检索到消息

  2. 如果启用了日志记录,请打印派发信息

  3. 将消息发送到目标处理器

  4. 回收消息

  5. 转到步骤 1

如前一章所述,主线程隐式创建自己的Looper,以顺序处理保持应用程序运行所需的所有内容,并管理应用程序组件之间的交互。

要访问主线程的 Looper,你需要访问主线程的Looper实例,使用静态方法getMainLooper()

Looper mainLooper = Looper.getMainLooper();

要设置我们自己的Looper线程,我们需要在线程内部调用Looper的两个静态方法——prepareloop——它们将处理连续循环。以下是一个简单的示例:

class SimpleLooper extends Thread {

       public void run() {
        // Attach a Looper to the current Thread
           Looper.prepare();
        // Start the message processing
           Looper.loop();
       }
}

在代码片段中,当通过调用 start() 方法创建并启动 SimpleLopper 对象时,在当前应用程序进程中创建了一个新线程,并且 run() 方法在新线程中自动被调用。当调用 run() 方法时,我们在调用静态 Looper.prepare() 方法时将一个 Looper 绑定到当前线程。随后,当调用 loop() 时,我们开始处理消息。prepare() 方法负责初始化 MessageQueue 并将其作为 ThreadLocal 参数绑定到当前线程。

当调用 loop() 时,run() 方法将阻塞,直到循环器被中断以处理添加到队列中的新消息。

注意

Looper.prepare() 必须在同一线程中只调用一次;否则,将抛出一个 RuntimeException,表示每个线程只能创建一个循环器。

当我们想要停止连续的 Looper 执行时,我们可以通过调用其成员函数 quit() 来停止它,而无需处理队列中剩余的消息,或者调用 quitSafely() 来处理队列中剩余的工作然后停止。

理解 Handler

Looper 一起,Handler 类是 Android 应用程序基础设施的基本组成部分。它支撑着主线程所做的所有事情——包括调用 Activity 生命周期方法。

Looper 在其消息循环线程上处理工作的时候,Handler 扮演两个角色:提供一个接口将消息提交到其 Looper 队列,并在消息被 Looper 分发时实现处理这些消息的回调。

还很重要的一点是,每个 Handler 都绑定到一个单一的 Looper,进而绑定到一个线程及其 LooperMessageQueue

为了绑定到当前线程的 Looper,我们需要在通过调用 prepare 方法初始化 Looper 之后,使用默认的 Handler() 构造函数来实例化它。由于我们在 SimpleLooper 线程中通过默认构造函数 Handler() 创建我们的处理器,因此 myHandler 将绑定到当前线程的 Looper 而不是主线程的 Looper

public class SimpleLooper extends Thread{

    private Handler myHandler;

    @Override
    public void run() {
        Looper.prepare();
        myHandler  =  new MyHandler();
        Looper.loop();
    }

    public Handler getHandler(){
        return myHandler;
    }
}

除了提供一个接口将工作提交给 Looper 线程外,Handler 还定义了处理提交的消息的代码。在下面的代码中,MyHandler 类重写了超类(Handler)的 handleMessage 成员方法来定义我们的消息处理代码:

public class MyHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
        // Add here your message handling
       // processing
    }
}

一旦启动,Looper 线程将在 Looper.loop() 内部等待,直到有消息添加到其队列中。

当另一个线程使用 submit 方法将 Message 添加到队列中时,等待的线程将调用处理器的 handleMessage() 方法,将消息分发到我们的目标 MyHandler

拥有Handler对象引用后,我们能够从任何线程向Handler发送消息,因此,消息总是被调度到Looper线程并由正确的Handler处理,如下面的图示所示:

理解 Handler

图 2.1:将工作发布到其他线程

我们已经看到我们可以创建自己的Looper线程,但如前所述,主线程也是一个Looper线程。为了更清楚地说明,我们将创建一个打印当前线程堆栈跟踪的StackTraceHandler

public class StackTraceHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
       // Prints the Stack Trace on the Android Log
       Thread.currentThread().dumpStack();
    }
}

由于活动的onCreate()函数在主线程上运行,我们将创建我们处理器的实例,它隐式地调用处理器的超构造函数,该构造函数将处理器绑定到当前线程的Looper

注意

如果当前线程没有Looper,并且我们尝试在超构造函数中创建处理器,则会抛出一个带有消息Can't create handler inside thread that has not called Looper.prepare()的运行时异常。

创建了Handler实例后,我们通过调用处理器的obtainMessage方法从其回收的消息池中检索消息,并将一个空消息发布到主线程的Looper。通过obtainMessage获取的消息将被缓存,并将处理器设置为目标的Handler对象:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    Handler handler = new StackTraceHandler();
    Message msg = handler.obtainMessage();
    handler.sendMessage(msg);
}

如前所述,当我们的handleMessage()被调度时,它打印出handleMessage()执行时的活动堆栈帧,正如我们可以在下面的堆栈跟踪中看到的那样:

.....StackTraceHandler.handleMessage(StackTraceHandler.java:18)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:137)
android.app.ActivityThread.main(ActivityThread.java:4424)
java.lang.reflect.Method.invokeNative(Native Method)
java.lang.reflect.Method.invoke(Method.java:511)

没错,handleMesage()是在由主Looper调用的dispatchMessage()调用中运行的,并且它被调度到主线程的执行行。

向 Looper 发送工作

在之前,StackTraceHandler隐式地绑定到当前主线程的Looper,因此为了使其更灵活,让我们再迈出一步,使其可以附加到任何Looper

在下面的代码中,我们将重写默认的Handler构造函数并定义一个接受将要进入队列的Looper的构造函数,然后我们将处理和调度消息:

public class StackTraceHandler extends Handler {

    StackTraceHandler(Looper looper){
        super(looper);
    }

我们的新构造函数基本上是将处理器(Handler)附加到作为参数传递的Looper,使得StackTraceHandler可以附加到任何Looper,而不是当前线程的Looper

我们的SimpleLooper也被扩展以提供getter方法来检索与其线程关联的Looper对象:

public class SimpleLooper extends Thread{
    // start condition
    boolean started = false;
    Object startMonitor =  new Object();
    Looper threadLooper = null;

    @Override
    public void run() {
        Looper.prepare();
        threadLooper = Looper.myLooper();
        synchronized (startMonitor){
            started = true;
            startMonitor.notifyAll();
        }
        Looper.loop();
    }

    Looper getLooper(){
        return threadLooper;
    }
    // Threads could wait here for the Looper start
    void waitforStart(){
         synchronized (startMonitor){
             while (!started){
                 try {
                    startMonitor.wait(10);
                 } catch (InterruptedException e) {
                    ...
                 }
             }
          }
     }

现在,从主线程开始,我们启动SimpleLooper及其自己的线程,当它启动时,我们获取Looper实例以将我们的Handler绑定到SimpleLooper线程和Looper

SimpleLooper looper = new SimpleLooper();
looper.start();
looper.waitforStart();
Handler handler = new StackTraceHandler(looper.getLooper());

现在,我们将像上一个示例中那样,从活动的onCreate()回调中发送消息,该回调在主线程中运行:

Message msg = handler.obtainMessage();
handler.sendMessage(msg);

如以下堆栈跟踪所示,堆栈底部的线程堆栈帧指向 SimpleLooper.run(),而在堆栈顶部,我们有我们的 Handler 回调,StackTraceHandler.handleMessage

at...activity.StackTraceHandler.handleMessage(StackTraceHandler.java:18)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at ...activity.SimpleLooper.run(SimpleLooper.java:23)

这里有趣的是,我们可以从主线程向由 SimpleLooper 管理的后台线程(甚至从后台线程向主线程)发送消息,并且在这个过程中,将后台线程的工作交给主线程——例如,用后台处理的结果更新用户界面。

使用 post 安排工作

如前一段所述,我们可以通过将 Looper 实例的引用传递给 Handler 构造函数来将工作提交给主线程或后台线程。

我们所说的“工作”可以通过 java.lang.Runnable 的子类或 android.os.Message 的实例来描述。我们可以将 Runnable 发布到 Handler 实例或向其发送消息,它将它们添加到相关 Looper 实例的 MessageQueue 中。

我们可以很容易地将工作发布到 Handler,例如,通过创建一个匿名内部 Runnable

final TextView myTextView = (TextView) findViewById(R.id.myTv);
// Get the main thread Looper by calling the Context
// function getMainLooper
Handler handler = new Handler(getMainLooper());

handler.post(new Runnable(){
    public void run() {
        String result = processSomething();
        myTextView.setText(result);
    }
});

绑定 HandlerLooper 实例会遍历队列,尽可能快地执行每个 Runnable。使用 post 方法简单地在队列末尾添加一个新的 Runnable

如果我们想让我们的 Runnable 在队列中的任何现有工作之上具有优先级,我们可以将其发布到队列的前面,在现有工作之前:

handler.postAtFrontOfQueue(new Runnable(){
public void run() {
      ...
   }
});

在单线程应用程序中,可能看起来通过将工作发布到主线程并没有获得太多好处,但将事情分解成可以交错和可能重新排序的小任务对于保持响应性非常有用。

此外,通过将工作封装成更细粒度的工作单元,我们鼓励组件的重用,提高代码的可测试性,并增加工作组合的能力:

使用  安排工作

图 2.2:Runnable 组合

使用 Handler 延迟工作

当我们使用正常的 post 工作函数时,工作会在所有之前的单元工作在 Looper 上处理完毕后立即处理——但如果我们想在 10 秒后安排一些工作,会发生什么呢?

使用 Thread.sleep 来阻塞主线程 10 秒意味着我们阻止主线程执行其他工作,并且我们保证会得到一个 ANR 对话框。另一种选择是使用提供延迟功能的处理程序函数:

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        // do some work
    }
};
// Defer work in the main Thread
// by 10 seconds time
   handler.postDelayed(new MyRunnable(), TimeUnit.SECONDS.toMillis(10));

我们仍然可以在同时发布其他工作以供执行,并且我们的延迟 Runnable 实例将在指定的延迟后执行。注意,我们正在使用 java.lang.concurrent 包中的 TimeUnit 类将秒转换为毫秒。

发布工作的一个进一步调度选项是postAtTime,它将Runnable调度到相对于系统运行时间的特定时间(自系统启动以来经过的时间)执行:

// Work to be run at a specific time
handler.postAtTime(new MyRunnable(),
                   SystemClock. uptimeMillis() +
                   TimeUnit.SECONDS.toMillis(10));

由于postAtTime()是通过从SystemClock的运行时间偏移量实现的,因此调度可能会受到一些延迟问题的影响,尤其是在设备最近进入某些深度睡眠状态时。考虑到这一点,并且当需要时间精度时,通常最好使用handler.postDelayed来延迟工作。

漏露隐式引用

使用Handler和匿名或非静态嵌套类延迟工作需要小心,以避免潜在的资源泄漏。在这些情况下,提交给处理器的对象通常创建了对它定义或创建的类的引用。由于 Looper 消息队列将保持Runnable对象在计划时间内的活动状态,对原始 Android 组件的间接引用可能会阻止整个组件及其对象被垃圾回收。

让我们通过以下示例来分析这个问题:

public class MyActivity extends Activity {
  // non-static inner class
  public class MyRunnable implements Runnable {

   @Override
   public void run() {
     // do some work
   }
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
        ...
    // Post Inner class instance Runnable
    handler.postDelayed(new MyRunnable(),
                         TimeUnit.MINUTES.toMillis(10));

    // Post an Anonymous class instance
    handler.postDelayed(new Runnable() {
     @Override
     public void run() {
     // do some work
     }
    }, TimeUnit.MINUTES.toMillis(20));
    ...
    }
   }

两个对象,即通过默认构造函数创建的MyRunnable对象和在第二个handler.postDelayed中创建的匿名Runnable类,都持有Activity对象的引用。

通过在活动内部声明一个匿名内部Runnable,我们已经对该包含Activity实例创建了隐式引用。然后我们向处理器发送了Runnable,并告诉它在 10 分钟后执行。

如果活动在 10 分钟内完成,它还不能被垃圾回收,因为我们的Runnable中的隐式引用意味着活动仍然可以通过活动对象访问。

因此,尽管这只是一个简洁的例子,但在实践中将非静态的 Runnables 发布到主线程的Handler队列(尤其是使用postDelayedpostAtTime)并不是一个好主意,除非我们非常小心地清理所有不活跃活动的引用。

如果MyActivity对象在 10 分钟内没有被垃圾回收,所有活动视图和资源将导致内存消耗增加,直到达到每个应用程序可用的最大堆空间。更糟糕的是,如果用户在应用程序中导航时创建了多个此活动实例,应用程序将瞬间耗尽内存。

注意

每个应用程序可用的堆大小限制因设备而异。当应用程序达到这个限制时,系统将抛出OutOfMemoryError异常。

减少这种问题的一种方法是在创建延迟的Runnable工作任务时,使用静态嵌套类或顶级类(它们自己的文件中的直接成员)来移除对原始Activity对象的引用。这意味着引用必须是显式的,这使得它们更容易被发现和置空:

public class MyActivity extends Activity {
    // static inner class
    public static class MyRunnable implements Runnable {

漏露显式引用

如果我们要与用户界面交互,我们至少需要一个指向View层次结构中对象的引用,我们可能将其传递到我们的静态或顶级可运行对象的构造函数中:

static class MyRunnable implements Runnable {
       private View view;
       public MyRunnable(View view) {
           this.view = view;
       }
       public void run() {
           // ... do something with the view.
       }
}

然而,由于我们保持了对View的强引用,如果我们的RunnableView存活时间更长,我们再次面临潜在的内存泄漏问题;例如,如果我们的Runnable执行之前,代码的某个其他部分已经将这个View从显示中移除。

解决这个问题的方法之一是使用弱引用并在使用引用的View之前检查null

static class MyRunnable implements Runnable {

    private WeakReference<View> view;

 public MyRunnable(View view) {
   this.view = new WeakReference<View>(view);
 }
 public void run() {
  View v = view.get(); // might return null
  if (v != null) {
    // ... do something with the view.
    }
  }
}

如果你之前没有使用过WeakReference,它为我们提供了一种方式,即只有当其他活动对象对它的引用更强(例如,一个普通的属性引用)时,我们才能引用一个对象(例如,一个普通的属性引用)。

当所有强引用都被垃圾回收时,我们的WeakReference也会失去对View的引用,get()将返回nullView将被垃圾回收。

这解决了资源泄漏问题,但我们必须在使用返回的对象之前始终检查null,以避免潜在的NullPointerException实例。

如果我们向Handler发送消息并期望它更新用户界面,它也需要一个指向视图层次结构的引用。一种很好的管理方式是在onResumeonPause中附加和分离Handler

private static class MyHandler extends Handler {
    private TextView view;
    public void attach(TextView view) {
        this.view = view;
    }
    public void detach() {
        view = null;
    }
    @Override
    public void handleMessage(Message msg) {
      // handle message
    }
}

@Override
protected void onResume() {
  super.onResume();
  myHandler.attach(myTextView);
}

@Override
  protected void onPause() {
    super.onPause();
    myHandler.detach();
}

使用 Handler 更新 UI

由于我们在主线程中实例化了我们的 handler,因此提交给它的所有工作都在主线程上执行。这意味着我们不应该向这个特定的 handler 提交长时间运行的操作,但我们可以安全地与用户界面交互:

handler.post(new Runnable(){
  public void run() {
    TextView text = (TextView) findViewById(R.id.text);
    text.setText("updated on the UI thread");
  }
});

这适用于无论哪个线程提交Runnable,这使得Handler成为将其他线程执行的工作结果发送到主线程的理想方式:

public void onCreate(Bundle savedInstanceState) {
    ...
    // Handler bound to the main Thread
    final Handler handler = new Handler();

    // Creates an assync line of execution
    Thread thread = new Thread() {
        public void run() {
            final String result = searchSynomym("build");
            handler.post(new Runnable() {
                public void run() {
                    TextView text = (TextView)
                          findViewById(R.id.text);
                    text.setText(result);
                }
            });
        }
    };
    // Start the background thread with a lower priority
    thread.setPriority(Thread.MIN_PRIORITY);
    thread.start();

注意

如果你为后台工作启动自己的线程,请确保将其优先级设置为Thread.MIN_PRIORITY,以避免耗尽主线程的 CPU 时间。系统 CPU 调度器会给优先级更高的线程更多的 CPU 周期时间。

Handler如此基本,以至于其 API 已经集成到View类的成员函数中:

  • View.post(Runnable)

  • View.postDelayed(action,delayMillis)

因此,我们可以将之前的示例重写如下:

final TextView text = (TextView) findViewById(R.id.text);
Thread thread = new Thread(){
  public void run(){
   final String result = searchSynonym("build"); 
   // Using the view post capabilities             
   text.post(new Runnable(){
     public void run() {
       text.setText(result);
       }
      });
    }
   };
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();

当在Activity类中编写代码时,可以使用ActivityrunOnUiThread(Runnable)方法提交主线程上的Runnable,正如前一章所解释的。如果当前线程是 UI 线程,则操作会立即执行。如果当前线程不是 UI 线程,则操作会被发送到主 UI 线程的事件队列。

取消挂起的 Runnable

在你的应用程序执行过程中,你可能会有想要取消已发布Runnable的情况,例如,当你在一个活动的onCreate()中提交一个延迟任务,并且当你执行onDestroy()时想要取消它,因为活动将被销毁。Handler函数removeCallbacks()可以通过从工作队列中移除已发布的Runnable任务来取消挂起的操作:

final Runnable runnable = new Runnable(){
  public void run() {
    // ... do some work
  }
};
handler.postDelayed(runnable, TimeUnit.SECONDS.toMillis(10));
Button cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new OnClickListener(){
  public void onClick(View v) {
  handler.removeCallbacks(runnable);
 }
});

注意,为了能够指定要移除的内容,我们必须保留对Runnable实例的引用,并且取消操作仅适用于挂起的任务——它不会尝试停止正在执行中的Runnable

备注

请记住,如果您多次发布同一个对象,removeCallbacks()将移除所有引用该对象的非运行条目。

使用发送进行工作调度

当我们发布一个Runnable时,我们可以像前例中看到的那样,在局部或成员范围内使用匿名Runnable定义工作。因此,Handler事先不知道可能被要求执行哪种类型的工作。

如果我们经常需要从不同的范围执行相同的工作,我们可以定义一个静态或顶级Runnable类,我们可以在应用程序的生命周期中的任何地方实例化它。

或者,我们可以通过向Handler发送消息并定义Handler以适当地对不同消息做出反应来颠倒这种做法。

以一个简单的例子来说明,假设我们想让我们的Handler根据接收到的消息类型显示hellogoodbye。为了做到这一点,我们将扩展Handler并重写其handleMessage()方法:

public static class SpeakHandler extends Handler {

    public static final int SAY_HELLO = 0;
    public static final int SAY_BYE = 1;

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SAY_HELLO:
                sayWord("hello");
                break;
            case SAY_BYE:
                sayWord("goodbye");
                break;
            default:
                super.handleMessage(msg);
        }
    }
    private void sayWord(String word) {
        // Say word
    }
}

在这里,我们实现了handleMessage()方法,以期望具有两个不同what值的消息,并相应地做出反应。除了用于标识消息内容的what属性外,消息对象还提供了三个额外的整数字段argarg2obj,可以用来标识和指定你的消息。

备注

如果你仔细查看前面解释的Speak处理器类示例,你会注意到我们将其定义为静态类。Handler的子类应始终声明为顶级或静态内部类,以避免意外的内存泄漏!

要将我们的Handler实例绑定到主线程,我们只需从任何在主线程上运行的方法中实例化它,例如Activity onCreate()回调:

private Handler handler;
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    handler = new SpeakHandler();
    ...
}

记住,我们可以从任何线程向这个Handler发送消息,它们将由主线程处理。我们向我们的Handler发送消息,如下所示:

  handler.sendEmptyMessage(SpeakHandler.SAY_HELLO);
  ...
  handler.sendEmptyMessage(SpeakHandler.SAY_BYE);

当我们通过之前的方法发布消息时,Handler会为我们创建一个消息,将消息的what属性填充为传入的整数,并将消息发布到处理器的Looper队列。当我们需要向处理器发送基本命令时,这种结构可能非常有用,尽管当我们需要更复杂的消息时,我们需要使用其他消息属性,如arg1arg2obj,来携带更多关于我们请求的信息。

由于消息可能携带一个对象有效负载作为消息执行的上下文,让我们扩展我们的示例,允许我们的Handler说出消息发送者想要说的任何词:

public static class SpeakHandler extends Handler {
    public static final int SAY_HELLO = 0;
    public static final int SAY_BYE = 1;
    public static final int SAY_WORD = 2;
    @Override
    public void handleMessage(Message msg) {
       switch(msg.what) {
           case SAY_HELLO:
               sayWord("hello"); break;
           case SAY_BYE:
               sayWord("goodbye"); break;
           case SAY_WORD:
                //  Get an Object
               sayWord((String)msg.obj); break;
           default:
               super.handleMessage(msg);
        }
    }
    private void sayWord(String word) { ... }
}

在我们的handleMessage方法中,我们可以通过访问公共的obj属性直接访问消息的有效负载。Message有效负载可以通过替代的静态obtain方法轻松设置:

Message msg =  Message.obtain(handler,
               SpeakHandler.SAY_WORD, "Welcome!");
handler.sendMessage(msg);

在上一个示例中,我们基本上创建了一条消息,其中what属性是SAY_WORD,而obj属性是Welcome!

虽然这段代码的功能应该相当清晰,但你可能想知道为什么我们没有通过调用其构造函数来创建一个新的Message实例,而是调用了它的静态方法obtain

原因是效率。消息仅被短暂使用——我们实例化、分发、处理,然后丢弃它们。因此,如果我们每次都创建新的实例,我们就是在为垃圾回收器创造工作。

垃圾回收是昂贵的,Android 平台会尽力在可能的情况下最小化对象分配。虽然我们可以根据需要实例化一个新的Message对象,但推荐的方法是获取一个从池中重用Message实例的实例,从而减少垃圾回收的开销。通过减少内存占用,可以回收 fewer 对象,从而实现更快和更频繁的垃圾回收。

注意

在你可以使用低成本整数参数构建消息的情况下,你应该使用它们,而不是像objdata这样的复杂参数,这些参数总是为 GC 创造额外的工作。

正如我们可以使用post方法的变体来安排可运行对象一样,我们也可以使用send方法的变体来安排消息:

handler.sendMessageAtFrontOfQueue(msg);
handler.sendMessageAtTime(msg, time);
handler.sendMessageDelayed(msg, delay);

为了方便起见,也存在空消息变体,当我们没有有效负载时:

handler.sendEmptyMessageAtTime(what, time);
handler.sendEmptyMessageDelayed(what, delay);

取消挂起的消息

取消已发送的消息也是可能的,实际上比取消已发布的可运行对象更容易,因为我们不需要保留我们可能想要取消的消息的引用——相反,我们可以通过它们的what值或what值和对象引用来取消消息:

String myWord = "Do it now!";
handler.removeMessages(SpeakHandler.SAY_BYE);
handler.removeMessages(SpeakHandler.SAY_WORD, myWord);

注意,就像已发布的可运行对象一样,消息取消仅从队列中移除挂起的操作——它不会尝试停止正在执行的操作。

除了取消功能之外,handler 还提供了验证队列中是否有挂起消息的功能。有了 handler 对象,我们可以通过消息的what值(hasMessages(what))和hasMethods(what,object)消息对象值来查询 handler。让我们结合之前的例子来放一些例子:

handler.hasMessages(SpeakHandler.SAY_BYE)
handler.hasMessages(SpeakHandler.SAY_WORD, myWord)

第一个例子将验证是否存在任何what代码为SAY_BYE的消息,第二个例子将验证是否存在任何what代码为SAY_WORD且对象指向与myWord相同引用的消息。

真的非常重要,要记住,带有对象参数的removeMessageshasMessages方法将会在队列中搜索,通过==引用比较而不是对象值比较(如(equals()))来比较对象。这里有一个简单的例子来解释这种情况:

String stringRef1 = new String("Welcome!");
String stringRef2 = new String("Welcome Home!");
Message msg1 =  Message.obtain(handler,
                    SpeakHandler.SAY_WORD,stringRef1);
Message msg2 =  Message.obtain(handler,
                    SpeakHandler.SAY_WORD, stringRef2);

// Enqueue the messages to be processed later
handler.sendMessageDelayed(msg1,600000);
handler.sendMessageDelayed(msg2,600000);

// try to remove the messages
handler.removeMessages(SpeakHandler.SAY_WORD,
                       stringRef1);
handler.removeMessages(SpeakHandler.SAY_WORD,
                       new String("Welcome Home!"));
// Create a Print Writer to Process StandardOutput
PrintWriterPrinter out =
     new PrintWriterPrinter(new PrintWriter(System.out,true));

// Dump the Looper State
handler.getLooper().dump(out,">> Looper Dump ");

如前所述,第二次remove调用不会移除之前添加的消息,因为stringRef1引用与传入的新引用不同,尽管字符串内容相同。

这里是 looper 转储的输出,其中包含未成功取消的消息:

>> Looper Dump Looper (main, tid 1) {a15844a}
>> Looper Dump Message 0: { when=+10m0s0ms
        what=2       
        obj=Welcome Home! target=...SpeakHandler }
>> Looper Dump (Total messages: 1,
        polling=false, quitting=false)

组合与继承

到目前为止,我们已经通过重写HandlerhandleMessage方法来子类化Handler,但这并不是我们的唯一选择。我们可以在构造Handler时通过传递Handler.Callback的实例来优先考虑组合而不是继承:

boolean handleMessage(Message msg)

假设我们想要扩展我们的演讲者而不改变原始的Handler,并且我们想在Handler.Callback类上添加新的操作:

public class Speaker implements Handler.Callback {

    public static final int SAY_WELCOME = 2;
    public static final int SAY_YES = 3;
    public static final int SAY_NO = 4;

    @Override
    public boolean handleMessage(Message msg) {
        switch(msg.what) {
            case SAY_WELCOME:
                sayWord("welcome"); break;
            case SAY_YES:
                sayWord("yes"); break;
            case SAY_NO:
                sayWord("no"); break;
            default:
                return false;
        }
        return true;
    }
    private void sayWord(String word) {  }
}

注意,这里的handleMessage签名略有不同——我们必须返回一个boolean值,表示是否处理了Message。要创建使用我们扩展的Handler,我们只需在构造Handler时传递Handler.Callback实现:

Handler handler = new SpeakHandler(new Speaker());

如果我们从回调的handleMessage方法返回false,Handler 将调用它自己的handleMessage方法,因此我们可以选择使用继承和组合的组合来实现Handler子类中的默认行为,并通过传递Handler.Callback的实例来混合特殊行为。

在上述代码中,我们使用组合来处理SAY_HELLO消息,使用继承来处理SAY_YES消息:

// will be handled by SpeakHandler Handler
handler.sendEmptyMessage(SAY_HELLO);
// will be handled by Speaker Handler.Callback
handler.sendEmptyMessage(SAY_YES);

注意

继承应该只在子类与超类之间的关系是永久且强烈,并且不能解耦时使用。另一方面,组合提供了更多增强和测试的灵活性。

使用 Handler 和 ThreadHandler 进行多线程

在典型的 Android 异步应用程序中,UI 线程将长时间计算操作交给后台线程,该线程随后执行任务并将结果回传到主线程。

到目前为止,我们只是使用Handler向主线程发送消息,所以下一步自然的步骤是设计一个多线程场景,其中interthread通信由Handler结构管理。

让我们扩展我们之前的示例,创建一个天气预报检索器。

想象一下这个场景:当我们点击 UI 按钮时,主线程将请求我们的后台线程检索天气预报,当收到天气预报响应时,后台线程将请求主线程展示收到的天气预报。

我们将首先创建WeatherRetriever,它负责接收天气预测请求,然后检索预测句子并将结果回传给mainHandler对象。

在场景组装过程中,WeatherRetriever处理器通过第一个构造函数参数附加到后台Looper上,以便在主线程之外单独的执行线上执行。第二个构造函数参数用于设置处理器以发布结果。

handleMessage方法中,处理器能够处理当前日的预测消息请求(GET_TODAY_FORECAST)或下一日的请求(GET_TOMORROW_FORECAST),最终调用长时间计算的getForecast()操作。

长时间计算的getForecast()可能会长时间阻塞线程执行,但这个问题现在不再是问题,因为我们将在一个优先级较低的背景线程中运行它,这样就不会阻塞 UI 及时渲染,从而防止 ANR 错误的发生,并使应用程序对用户交互更加响应:

public class WeatherRetriever extends Handler {

    private final Handler mainHandler;

    public static final int GET_TODAY_FORECAST = 1;

    public WeatherRetriever(Looper looper,Handler mainHandler){
        super(looper);
        this.mainHandler = mainHandler;
    }    
    // Long Computing Operation 
    String getForecast(){ ... }

    @Override
    public void handleMessage(Message msg) {
        switch(msg.what) {
            case GET_TODAY_FORECAST:
                ...
                final String sentence = getForecast();
                Message resultMsg =
                    mainHandler.obtainMessage(
                        WeatherPresenter.TODAY_FORECAST,sentence);
                this.mainHandler.sendMessage(resultMsg);
                break;
        }
    }
};

其次,我们将构建WeatherPresenter,它将处理来自后台操作的结果,在主线程上向用户展示:

public class WeatherPresenter extends Handler {
  public static final int TODAY_FORECAST = 1;

  @Override
  public void handleMessage(Message msg) {
    switch(msg.what) {
    case TODAY_FORECAST:
      readTodayWeather((String) msg.obj); break;
      ...
    }
  }
  private void readTodayWeather(String word) {
   // Present the weather forecast on the UI
   ...
 }
};

我们在本章前面详细描述了使用SimpleLooper类设置Looper线程的一种方法,但还有一种更简单的方法,使用 SDK 提供的专门为此目的而设计的类:android.os.HandlerThread

当我们创建一个HandlerThread时,我们指定两件事:线程的名称,这在调试时可能很有帮助,以及其优先级,这必须从android.os.Process类的静态值集中选择:

HandlerThread thread = new HandlerThread("background",    Process.THREAD_PRIORITY_BACKGROUND);

小贴士

Android 中的线程优先级映射到 Linux 的 nice 级别,这决定了线程运行频率。-20的 niceness 是最高优先级,而19是最低优先级。默认的 niceness 级别是0。除了优先级之外,Android 还使用 Linux cgroups 限制 CPU 资源。具有后台优先级的线程会被移动到bg_non_interactive cgroup 中,如果其他组的线程忙碌,则该 cgroup 仅限于可用 CPU 的 5%。

在配置HandlerThread时添加THREAD_PRIORITY_MORE_FAVORABLETHREAD_PRIORITY_BACKGROUND会将线程移动到默认的 cgroup,但始终考虑这是否真的必要——通常并不是!

在下一个表中,详细说明了从 Android 线程优先级到 Linux nice 级别的映射;然而,在常规应用程序中不建议使用低于-2的 nice 级别:

Java 优先级 线程优先级 Nice 级别
THREAD_PRIORITY_URGENT_AUDIO -19
THREAD_PRIORITY_AUDIO -16
MAX_PRIORITY THREAD_PRIORITY_URGENT_DISPLAY -8
THREAD_PRIORITY_DISPLAY -4
THREAD_PRIORITY_FOREGROUND -2
NORM_PRIORITY THREAD_PRIORITY_DEFAULT 0
THREAD_PRIORITY_BACKGROUND 10
MIN_PRIORITY THREAD_PRIORITY_LOWEST 19

HandlerThread扩展了java.lang.Thread,我们必须在它实际开始处理其队列之前使用start()启动它:

thread.start();

现在,从一个Activity回调中,我们将详细说明如何提升我们的场景,构建提交和处理请求所需的所有实例和对象:

// Background Thread    
private HandlerThread thread;

protected void onCreate(Bundle savedInstanceState) {
   ...
   WeatherPresenter presHandler = new WeatherPresenter();

   // Creates a Thread with a looper attached
   handlerThread = new HandlerThread("background",
             Process.THREAD_PRIORITY_BACKGROUND);
   // start The Thread and waits for work
   handlerThread.start();

   // Creates the Handler to submit requests
   final WeatherRetriever retHandler =
       new WeatherRetriever(handlerThread.getLooper(),presHandler);

正如我们之前看到的,检索器处理程序正在使用HandlerThreadLooper而不是主线程的Looper,因此它在后台线程上处理预测请求,允许我们在WeatherRetriever上运行长时间计算操作。

我们手中持有WeatherRetriever对象(retHandler)引用,能够通过发送带有WeatherRetriever处理器的消息将新的预测请求入队到后台线程。在下一个示例中,我们监听 UI 的today按钮的点击以启动预测请求:

todayBut.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {     
    retHandler.sendEmptyMessage(WeatherRetriever.
                                GET_TODAY_FORECAST);
  }
}

当通过WeatherRetriever回调的背景线程处理预测时,会通过WeatherPresenter引用将消息发送到主Looper,并在主线程上调用readTodayWeather(String)以向用户展示预测。

如以下跟踪输出所示,预测检索器在低优先级的背景线程上运行,TID 为120,而预测结果则在 TID 为1的主 UI 线程上展示:

I/MTHandler(17666): Retrieving Today Forecast at Thread[background,120]
I/MTHandler(17666): Presenting Today Forecast at Thread[main,1]

如果我们创建一个HandlerThread为特定的Activity执行后台工作,我们希望将HandlerThread实例的生命周期紧密绑定到活动上,以防止资源泄漏。

可以通过调用quit()来关闭HandlerThread,这将停止HandlerThread处理其队列中的任何更多工作。API 级别 18 添加了quitSafely方法,它会导致HandlerThread在关闭前处理所有剩余的任务。一旦HandlerThread被告知关闭,它将不接受任何进一步的任务:

protected void onPause() {
  super.onPause();
  if (( handlerThread != null) && (isFinishing()))
    handlerThread.quit();
}

Looper 消息分发调试

当你想跟踪消息的分发和处理时,当你的任何消息被 Looper 路由,以及处理器完成处理时,在 Android 日志中打印一条消息可能很有用。Looper 对象为我们提供了一个设置消息分发调试打印工具的方法,因此,从我们的 HandlerThread 中,我们能够设置它并启用跟踪我们的请求所需的额外日志:

    ...
// Creates a Print writer to standard output
PrintWriterPrinter out= new PrintWriterPrinter(
   new PrintWriter(System.out,true)
);
handlerThread.getLooper().setMessageLogging(out);
    ...
reqHandler.sendEmptyMessageDelayed (
    WeatherRetriever.GET_TODAY_FORECAST,
    10000
);

以下是一个示例,展示了当我们的预测请求被 Looper 处理时打印的调试信息:

>>>>> Dispatching to Handler (…WeatherRetriever) {a15844a} null: 1
<<<<< Finished to Handler (...WeatherRetriever) {a15844a} null

分发调试信息的格式为 (<目标 _Handler>) {回调对象} : <内容>

在我们的简单示例中,我们打印了进程的标准输出流(java.io.OutputStream)的消息,但在更复杂的情况下,我们可以打印到任何类型的 OutputStream 子类(文件、网络等)。

发送消息与发布可运行对象

值得花几分钟时间考虑发布可运行对象和发送消息之间的区别。

运行时差异主要在于效率。每次我们想让我们的处理器做些什么时,都创建新的 Runnable 实例会增加垃圾收集开销,而发送消息则重用 Message 实例,这些实例来自应用程序范围内的池。

对于原型设计和小型一次性任务,发布可运行对象既快又简单,而发送消息的优点往往随着应用程序规模的增加而增加。

应该说,消息发送更符合 Android 的方式,并且在整个平台上用于将垃圾收集量降到最低,使应用程序运行顺畅。

Handler 和 HandlerThread 的应用

Handler 类非常灵活,这使得其应用范围非常广泛。

到目前为止,我们已经在 Activity 生命周期的背景下探讨了 HandlerHandlerThread,这限制了此结构可能被使用的应用类型——理想情况下,我们在这个上下文中根本不想执行长时间运行的操作(超过一秒钟)。

考虑到这个限制,合适的用途包括执行计算、字符串处理、在文件系统中读取和写入小文件,以及使用后台 HandlerThread 读取或写入本地数据库。

概述

在本章中,我们学习了如何使用 Handler 为主线程排队工作,以及如何使用 Looper 为我们的 Thread 构建排队基础设施。

我们看到了使用 Handler 定义工作的不同方式:在调用点使用 Runnable 定义的任意工作,或者在 Handler 本身中实现并触发消息发送的预定义工作。

同时,我们学习了如何正确地推迟工作,而不会在过程中泄漏内存。

我们学习了如何在多线程应用程序中使用Handler,以便在协作线程之间传递工作和结果,在普通后台线程上执行阻塞操作,并将结果返回到主线程以更新用户界面。

在下一章中,我们将通过应用AsyncTask实例来构建响应式应用程序,通过使用线程池在后台执行工作,并将进度更新和结果返回到主线程。

第三章。探索 AsyncTask

在 第二章 中,使用 Looper、Handler 和 HandlerThread 执行工作,我们熟悉了 Android 平台上最基本的后台和并发结构:HandlerLooper。这些结构支撑了主线程用于渲染 UI 和运行 Android 组件生命周期的几乎所有事件和顺序处理。

在本章中,我们将探讨 android.os.AsyncTask,这是一个高级结构,它为我们提供了一个整洁且精简的接口来执行后台工作,并将结果发布回主线程,而无需管理线程创建和处理器的操作。

在本章中,我们将涵盖以下主题:

  • 介绍 AsyncTask

  • 声明 AsyncTask 类型

  • 执行 AsyncTasks

  • 提供不确定的进度反馈

  • 提供确定的进度反馈

  • 取消 AsyncTask

  • 处理异常

  • 控制并发级别

  • 常见的 AsyncTask 问题

  • AsyncTask 的应用

介绍 AsyncTask

AsyncTask 是在 Android 平台的 Android Cupcake(API 级别 3)中引入的,其明确目的是帮助开发者避免阻塞主线程。这个类名中的 Async 部分来自异步这个词,字面上意味着阻塞的任务不是在我们调用它的时候发生的。

AsyncTask 将后台线程的创建、与主线程的同步以及执行进度的发布封装在一个结构中。

HandlerLooper 结构相比,AsyncTask 使开发者免于管理低级组件、线程创建和同步。

AsyncTask 是一个抽象类,因此必须进行子类化才能使用。我们的子类至少必须为抽象的 doInBackground 方法提供一个实现,该方法定义了我们想要在主线程之外完成的工作。

protected Result doInBackground(Params... params)

doInBackground 将在当前进程中以 THREAD_PRIORITY_BACKGROUND(友好级别 10)的优先级在一个并行线程中执行,并且名称遵循以下格式 AsyncTask #<N>

除了 doInBackground 方法之外,该结构还提供了开发者可以在子类中实现的不同方法,用于设置任务、发布进度和将最终结果发布到主线程。

AsyncTask 有五种其他方法,我们可以选择重写:

   protected void onPreExecute()
   protected void onProgressUpdate(Progress... values)
   protected void onPostExecute(Result result)
   protected void onCancelled(Result result)
   protected void onCancelled()

尽管我们可以重写这五个方法中的一个或多个,但我们不会直接从自己的代码中调用它们。这些是回调方法,意味着它们将在 AsyncTask 生命周期的适当时间为我们调用(回调)。

doInBackground() 和其他四个方法之间的关键区别是它们执行的线程。

在任何后台工作开始之前,onPreExecute()将被调用,并且当我们调用 execute (Params…)方法时,它将在主线程上同步运行到完成。

onPreExecute()方法中,我们可以设置任务或任何进度对话框在 UI 上,以向用户指示任务刚刚开始。

一旦onPreExecute()完成,doInBackground()将被安排,并将在后台线程上开始工作。

在后台工作期间,开发者可以从doInBackground()发布进度更新,这会触发主线程执行我们提供的进度值的onProgressUpdate。内部,AsyncTask使用绑定到主线程LooperHandler来在主线程上发布结果,如第二章中所述,使用 Looper、Handler 和 HandlerThread 执行工作

通过在主线程上调用此方法,AsyncTask使我们能够轻松地更新用户界面以显示进度(记住我们只能从主线程更新用户界面)。

当后台工作成功完成时,doInBackground()可能返回一个结果。这个结果传递给onPostExecute(),它将在主线程上为我们调用。在onPostExecute()接收到结果后,我们可以使用后台处理的结果更新用户界面:

注意

这种从一条线程向另一条线程传递数据的方式非常重要,因为它允许我们将密集和长时间的任务从关键的主线程中移开。这种结构简化了主线程中的通信,并为在后台线程上执行异步工作提供了一个高级 API。

我们的AsyncTask可以操作封装的 Activity 类的字段,但那时我们必须采取额外的预防措施,例如添加同步来防止竞态条件和确保更新的可见性。

介绍 AsyncTask

图 3.1:AsyncTask 回调执行函数

前面的图显示了AsyncTask执行的方法调用序列,说明了哪些方法在主线程上运行,哪些在AsyncTask后台线程上运行。

注意

由于onPreExecute()onProgressUpdate()onPostExecute()onCancelled()方法是在主线程上调用的,因此在这些方法中我们不应执行长时间运行/阻塞的操作。

doInBackground()完成之前,通过AsyncTask引用调用cancel方法,onPostExecute()将不会被调用。相反,替代的onCancelled()回调方法将在 UI 线程上被调用,这样我们就可以为成功完成和取消完成实现不同的行为:

介绍 AsyncTask

图 3.2:AsyncTask 取消任务执行序列

前面的图显示了在 doInBackground() 完成之前取消任务时的方法调用序列。就像我们在前面的图中所示,cancel() 可能由主线程或从任何可以访问 AsyncTask 对象引用的其他线程调用。

声明 AsyncTask 类型

AsyncTask 是一个泛型类,它公开了三个泛型类型参数:

abstract class AsyncTask<Params, Progress, Result>

为了使用泛型类型,我们必须为泛型类型中声明的每个类型参数提供一个类型参数。

注意

泛型类型类提供了一种方法,可以针对不同的输入类型重用相同的泛型算法。一个泛型类型可以有一个或多个类型参数。

当我们声明 AsyncTask 子类时,我们将指定 Params、Progress 和 Result 的类型;例如,如果我们想将 String 参数传递给 doInBackground,以 Float 报告进度,并返回 Boolean 结果,我们就会声明我们的 AsyncTask 子类如下:

    public class MyTask extends AsyncTask<String, Float, Boolean>

如果我们不需要传递任何参数,或者不想报告进度,对于这些参数使用 java.lang.Void 是一个好的选择,因为这可以清楚地表达我们的意图,因为 Void 是一个不可实例化的类,代表 void 关键字。

只能使用引用类型作为泛型类型的类型参数。这包括类、接口、枚举类型、嵌套和内部类型,以及数组类型。不允许使用原始类型作为类型参数。以下声明在泛型类型类定义中被认为是非法的:

   // Error
   public class MyTask extends AsyncTask<String, float, boolean>

让我们看看我们的第一个例子,在后台执行昂贵的图像下载并将结果报告到当前 UI:

public class DownloadImageTask
  extends AsyncTask<URL, Integer, Bitmap> {

  // Weak reference to the UI View to update
  private final WeakReference<ImageView> imageViewRef;

  public DownloadImageTask(ImageView imageView) {
    this.imageViewRef = new WeakReference<ImageView>(imageView);
  }

  // Retrieves the image from a URL
  private Bitmap downloadBitmap(URL url) {
    // elided for brevity ...
    ...
  }

  @Override
  protected Bitmap doInBackground(URL... params) {
    URL url = params[0];
    // The IO operation invoked will take a significant ammount
    // to complete
    return downloadBitmap(url);
  }
  ...

  @Override
  protected void onPostExecute(Bitmap bitmap) {
    ImageView imageView = this.imageViewRef.get();
    if (imageView != null) {
      imageView.setImageBitmap(bitmap);
    }
  }
}

在这里,DownloadImageTask 扩展了 AsyncTask,指定 Params 类型为 URL,以便我们可以根据其 URL 获取图像,Progress 为 Integer,Result 类型为 Bitmap。

我们将 ImageView 传递给构造函数,这样 DownloadImageTask 就有一个弱引用到它应该在其完成后更新的用户界面。

我们实现了 doInBackground 以在后台下载图像,其中 url 是一个带有图像资源位置的 URL 参数。

onPostExecute 中,当视图弱引用不为 null 时,我们只需将位图加载到我们在构造函数中存储的视图中。

WeakReference 在创建视图的活动不再活跃时,不会阻止视图被垃圾回收。

执行 AsyncTask

在实现了 doInBackgroundonPostExecute 之后,我们希望让我们的任务运行。我们可以使用两种方法来实现这一点,每种方法都提供了不同级别的控制,以控制任务执行的并发程度。让我们先看看两种方法中较简单的一种:

  public final AsyncTask<Params, Progress, Result> execute(Params...
  params)

返回类型是 AsyncTask 子类的类型,这只是为了方便,这样我们就可以使用方法链在一个单行中实例化和启动一个任务,同时仍然记录实例的引用:

   class MyTask implements AsyncTask<String,Void,String>{ ... }
   MyTask task = new MyTask().execute("hello");

Params... params 参数与我们在类声明中使用的 Params 类型相同,因为我们提供给 execute 方法的值稍后会被传递给我们的 doInBackground 方法作为其 Params... params 参数。请注意,它是一个可变参数(variable number of parameters)参数,这意味着我们可以传递任何数量的该类型参数(包括零个)。

注意

每个 AsyncTask 实例都是一个单次使用的对象——一旦我们启动了一个 AsyncTask,它就永远不能再次启动,即使我们取消它或等待它完成。

这是一个安全特性,旨在保护我们免受并发问题,如竞态条件的影响。

执行 DownloadImageTask 是直接的——我们需要 Activity,它使用一个视图来构建 DownloadImageTask 的实例,然后我们使用适合 URL 的值调用 execute 方法:

  public class ShowMyPuppyActivity extends Activity {

    @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.show_my_puppy);

  // Get the show button reference
  Button showBut = (Button) findViewById(R.id.showImageBut);
  showBut.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
      ...
      // My Puppie Image URL
      URL url = new URL("http://img.allw.mn/" +
                    "content/www/2009/03/april1.jpg");  
      // Get the Reference to Photo UI Image View               
      ImageView iv = (ImageView) findViewById(R.id.photo);
        // Download the Image in background and
        // load the image on the view
        new DownloadImageTask(iv).execute(url);
        ...
      }
  });
}

一旦我们点击 UI 显示按钮,就会创建一个新的 DownloadAsyncTask 并将其附加到一个 imageView 上,然后我们调用 execute() 方法以在后台启动异步任务。当我们对任务调用 execute() 方法时,这将导致调用 onPreExecute() 方法,然后调用 doInBackground() 方法。

正如我们之前解释的,一旦下载完成,就会调用 onPostExecute() 来在图像视图中加载下载的图像(Bitmap)。

提示

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。

提供不确定进度反馈

既然我们已经启动了一个可能需要很长时间才能完成的任务,我们可能想让用户知道正在发生某些事情。有很多种方法可以做到这一点,但一种常见的方法是显示一个显示相关消息的对话框。

AsyncTaskonPreExecute() 方法中展示我们的对话框是一个好地方,因为 AsyncTask 在主线程上执行,因此允许与用户界面交互。

修改后的 DownloadImageTask 需要一个 Context 的引用,以便它可以准备一个 ProgressDialog,它将在 onPreExecute() 中显示并在 onPostExecute() 中取消显示。由于 doInBackground() 没有改变,为了简洁起见,以下代码中没有显示:

public class DownloadImageTask
  extends AsyncTask<URL, Integer, Bitmap> {
  ...
 private final WeakReference<Context> ctx;
 private ProgressDialog progress;
  ...
  public DownloadImageTask(Context ctx, ImageView imageView) {
    this.imageView = new WeakReference<ImageView>(imageView);
    this.ctx = new WeakReference<Context>(ctx);
  }

 @Override
 protected void onPreExecute() {
 if ( ctx !=null && ctx.get()!= null ) {
 progress = new ProgressDialog(ctx.get());
 progress.setTitle(R.string.downloading_image);
 progress.setIndeterminate(true);
 progress.setCancelable(false);
 progress.show();
 }
 }

  // ... doInBackground elided for brevity ...
  @Override
  protected void onPostExecute(Bitmap bitmap) {
    ...
 if ( progress != null ) { progress.dismiss(); }
 ...
  }
}

剩下的只是将一个 Context 传递给修改后的 DownloadImageTask 的构造函数。由于 ActivityContext 的子类,我们可以简单地传递主机 Activity 的引用:

showBut.setOnClickListener(new View.OnClickListener() {

  @Override
  public void onClick(View v) {
      ...
      // Pass in the Context and the image view to load
      // the image
      new DownloadImageTask(
        ShowMyPuppyActivity.this, iv).execute(url);
         ...   
  }
});

提供不确定进度反馈

图 3.3:不确定进度对话框

一旦异步任务开始,onPreExecute()回调将创建一个不确定性的进度对话框,并如图 3.3 所示显示它。这个不可取消的对话框将以不透明的层覆盖在 UI 屏幕上,并显示标题。我们所说的“不确定”是指在此之前,我们无法估计任务完成还需要等待多长时间。

直到下载完成,对话框在onPostExecute()中被关闭,用户无法与应用程序交互,对话框将保持在前景。

注意

在你能够向你的应用程序 UI 展示内容之前,需要执行任何长时间的计算时,你必须向用户显示一个指示,表明后台正在发生某些操作,而用户正在等待。

提供确定性的进度反馈

知道正在发生某些事情对用户来说是一种极大的安慰,但他们可能会变得不耐烦,想知道他们还需要等待多长时间。让我们通过向我们的对话框添加进度条来向他们展示我们的进度。

记住,我们不允许直接从doInBackground()更新用户界面,因为我们不在主线程上。那么,我们如何告诉主线程为我们执行这些更新呢?

AsyncTask提供了一个方便的回调方法来完成这个任务,我们在本章开头看到了它的签名:

protected void onProgressUpdate(Progress... values)

我们可以重写onProgressUpdate()来从主线程更新用户界面,但它在何时被调用,以及它从哪里获取Progress...值doInBackground()onProgressUpdate()之间的粘合剂是 AsyncTask 的另一个方法:

   protected final void publishProgress(Progress... values)

为了更新用户界面以反映我们的进度,我们只需通过在doInBackground()中调用publishProgress()从后台线程发布进度更新。每次我们调用publishProgress()时,主线程都会被安排调用onProgressUpdate(),并带上这些进度值。

对我们的运行示例进行修改以显示确定性进度条非常简单。由于我们已经将DownloadImageTask的 Progress 类型定义为 Integer,现在,我们必须将设置进度值从 0(setProgress)到 100(setMax)的范围,并设置进度条的样式和边界。我们可以在onPreExecute()中通过以下添加来实现:

@Override
protected void onPreExecute() {
    ...
    // Sets the progress bar style
    progress.setProgressStyle(
        ProgressDialog.STYLE_HORIZONTAL);
    progress.setIndeterminate(false);
    progress.setProgress(0);
    progress.setMax(100);
    progress.setCancelable(false);
    progress.show(); 
}

我们还需要实现onProgressUpdate回调,以便从主线程更新进度条:

@Override
protected void onProgressUpdate(Integer... values) {
  progress.setProgress(values[0]);
  }

最后的修改是在for循环的每次迭代中计算进度,并调用publishProgress(),以便主线程知道调用onProgressUpdate()

private Bitmap downloadBitmap(URL url) {
  InputStream is = null;
  ...
  // Before Download starts
  publishProgress(0);
  downloadedBytes = 0;
  // Creates a Connection to the image URL
  HttpURLConnection conn = (HttpURLConnection) url.
                             openConnection();
  ...
  // Retrieves the image total length
  totalBytes = conn.getContentLength();
    ...
  BufferedInputStream bif = new BufferedInputStream(is) {

    int progress = 0;

      public int read(byte[] buffer, int byteOffset,
                      int byteCount) throws IOException {      
        // The number of bytes read in each stream read
      int readBytes = super.read(buffer, byteOffset,
                                 byteCount);
      ..
      // Actual number of bytes read from the file
      downloadedBytes += readBytes;
      // Percent of work done
      int percent = (int)((downloadedBytes * 100f) /
                      totalBytes);
      // Publish the progress to the main thread
      if (percent > progress) {
        publishProgress(percent);
        progress = percent;
      }     
  ...
}

重要的是要理解,调用publishProgress()并不会直接调用主线程,而是将一个任务添加到主线程的队列中,这个任务将在不久的将来由主线程处理。

注意,我们非常小心地只在百分比实际改变时发布进度,以避免任何不必要的开销:

注意

重要的是要知道,每次你在后台线程上调用publishProgress(),在downloadBitmat()中,都会自动发送一个新的 Handler 消息,将进度推送到主线程。

提供确定性的进度反馈

图 3.4:确定性进度对话框显示任务进度

如图 3.4 所示,在onPreExecute()中创建的确定性对话框在doInBackground()中会持续更新,显示任务的当前进度。进度是按比例计算的,如下面的除法所示:

对于本例以及任何没有太多 UI 工作要处理的程序,发布进度和看到用户界面更新之间的延迟将非常短。进度条将根据黄金法则平滑更新,即不阻塞主线程的任何代码,因为我们只在百分比变化时派发进度更新。

取消AsyncTask

我们可以为用户提供另一个很好的可用性功能,即在任务完成之前取消任务——例如,如果用户在执行开始后不再对操作结果感兴趣。AsyncTask通过cancel方法提供取消支持。

public final boolean cancel(boolean mayInterruptIfRunning)

mayInterruptIfRunning参数允许我们指定处于可中断状态的AsyncTask线程是否可以被实际中断——例如,如果我们的doInBackground代码正在执行一个阻塞的可中断函数,如Object.wait()。当我们设置mayInterruptIfRunningfalse时,AsyncTask不会中断当前的阻塞可中断操作,并且AsyncTask的后台处理将仅在阻塞操作终止后完成。

注意

在行为良好的可中断阻塞函数中,例如Thread.sleep()Thread.join()Object.wait(),当线程被Thread.interrupt()中断时,执行会立即停止,并抛出InterruptedException。应该适当地处理InterruptedException,并且只有当你知道后台线程即将退出时才应该吞没它。

简单地调用取消并不足以使我们的任务提前完成。我们需要通过定期检查isCancelled返回的值,并在doInBackground中适当地做出反应来积极支持取消。

首先,让我们设置我们的ProgressDialog,通过在onPreExecute中添加几行代码来触发AsyncTaskcancel方法:

@Override
protected void onPreExecute() {
 ...
 progress.setCancelable(true);
 progress.setOnCancelListener(
 new DialogInterface.OnCancelListener() {
 public void onCancel(DialogInterface dialog) {
 DownloadImageTask.this.cancel(false);
 }
 });
 ...
}

现在,我们可以通过触摸进度对话框外部或按设备上的返回按钮来触发取消操作,当对话框可见时。

我们将使用false调用cancel,因为我们不希望在执行网络读取或检查Thread.interrupted()函数的返回值时立即挂起当前的 IO 操作。我们仍然需要在doInBackground中检查取消操作,因此我们将对其进行如下修改:

private Bitmap downloadBitmap(URL url) {
  Bitmap bitmap = null;
  BufferedInputStream bif = new BufferedInputStream(is) {
    ...

    public int read(byte[] buffer, int byteOffset,
                    int byteCount) throws IOException {

      // Read the bytes from the Connection
      int readBytes = super.read(buffer, byteOffset, byteCount);

      // Verify if the download was cancelled
      if ( isCancelled() ) {
        // Returning -1 means that there is
        // no more data and the stream has just ended
        return -1;
      }
      ...
    }
  }
  // If the download is cancelled the Bitmap is null
  if ( !isCancelled() ) {
    bitmap = BitmapFactory.decodeStream(bif);
  }
  return bitmap;
  }

在上面的代码中,在我们的 BufferInputStream 匿名子类中,我们能够拦截连接上发生的每次读取操作。当这个拦截机制到位后,一旦我们取消 AsyncTask,我们就可以通过简单地返回读取调用的结果为 -1(流结束)来停止数据流。一旦 BitmapFactory.decodeStream 接收到流结束信号,它将立即返回,并且我们将 downloadBitmap 调用的结果返回为 null。

被取消的 AsyncTask 不会收到 onPostExecute 回调。相反,我们可以通过实现 onCancelled 来实现取消执行的不同行为。这个回调方法有两种变体:

protected void onCancelled(Result result);
protected void onCancelled();

参数化的 onCancelled(Result result) 方法的默认实现是在完成之后委托给 onCancelled() 方法。

如果 AsyncTask 无法提供部分结果(例如部分图像数据)或没有任何结果,那么我们可能需要重写无参数的 onCancelled() 方法。

另一方面,如果我们正在 syncTask 中执行增量计算,并且部分结果对应用程序有实际意义时,我们可能会选择重写 onCancelled(Result result) 版本。

在这两种情况下,由于在取消的 AsyncTask 上不会调用 onPostExecute(),我们想要确保我们的 onCancelled() 回调适当地更新用户界面——在我们的例子中,这包括关闭我们在 onPreExecute() 中打开的进度对话框,并使用应用程序包中可用的默认图像更新图像视图。

在我们的例子中,当任务被取消时,doInBackground() 的结果是 null 对象,因此我们将重写无参数的 onCancelled() 函数来添加之前描述的行为:

@Override
protected void onCancelled() {
  if ( imageView !=null && imageView.get() != null &&
       ctx !=null && ctx.get() != null ) {

   // Load the Bitmap from the application resources
    Bitmap bitmap = BitmapFactory.decodeResource(
                      ctx.get().getResources(),
                      R.drawable.default_photo
                    );
    // Set the image bitmap on the image view
    this.imageView.get().setImageBitmap(bitmap);
  }
  // Remove the dialog from the screen
  progress.dismiss();
}

另一个需要注意的情况是当我们取消尚未开始 doInBackground() 方法的 AsyncTask 时。如果发生这种情况,doInBackground() 将不会被调用,尽管 onCancelled() 仍然会在主线程上被调用。

AsyncTask 执行状态

execute() 方法可能以取消状态或完成状态结束,然而如果用户尝试第二次调用 execute(),任务将失败并抛出 IllegalStateException 异常,表示:

无法执行任务,任务只能执行一次/任务已经在运行

拥有 AsyncTask 对象的引用后,我们可以通过 getStatus() 方法确定任务的状态,并根据状态结果做出反应。让我们看一下下一个代码片段:

// Create a download task object
DownloadImageTask task  = new DownloadImageTask(
                            ShowMyPuppyActivity.this, iv);
...
if ( task.getStatus() == AsyncTask.Status.PENDING ) {
  // DownloadImageTask has not started yet so
  // we can can invoke execute()
} else if (task.getStatus() == AsyncTask.Status.RUNNING) {
  // DownloadImageTask is currently running in
  // doInBackground()
} else if (task.getStatus() == AsyncTask.Status.FINISHED
           && task.isCancelled()) {
  // DownloadImageTask is done OnCancelled was called
} else {
  // DownloadImageTask is done onPostExecute was called
}

通过使用 AsyncTask 提供的 getStatus() 实例方法,我们可以跟踪后台任务的执行情况,并确切知道后台工作的当前状态。

注意

如果你想要重复后台操作,你必须实例化一个新的任务并再次调用 execute() 方法。

异常处理

AsyncTask定义的回调方法规定我们无法抛出受检异常,因此我们必须将任何抛出受检异常的代码用 try/catch 块包裹起来。从AsyncTask的方法中传播出来的未检查异常会导致我们的应用程序崩溃,因此我们必须仔细测试并在必要时处理这些异常。

对于在主线程上运行的回调方法——onPreExecute()onProgressUpdate()onPostExecute()onCancelled()——我们可以在方法中捕获异常并直接更新用户界面以提醒用户。

当然,异常也可能出现在我们的doInBackground()方法中,因为AsyncTask的大部分工作都是在那里完成的,但不幸的是,我们无法从doInBackground()更新用户界面。一个简单的解决方案是让doInBackground()返回一个可能包含结果或异常的对象。首先,我们将创建一个用于存储操作结果的泛型类和一个用于存储异常的成员:

public class Result<T> {
    public T result;
    public Throwable error;
}

在下一步中,我们将创建一个新的下载AsyncTask,称为SafeDownloadImageTask,它负责异常处理,并有一个类型为Result<Bitmap>的结果,而不是Bitmap

public class SafeDownloadImageTask extends
  AsyncTask<URL, Integer, Result<Bitmap>> {

   // Method executed on the Background Thread
   protected Result<Bitmap> doInBackground(URL... params) {
     Result<Bitmap> result = new Result<Bitmap>();
     try {
           // elided for brevity ...
          ...
       result.result = bitmap;
     } catch (Throwable e) {
       result.error = e;
     } ...     
   }
   return result;
}

现在,我们可以在onPostExecute中检查Result对象中是否存在Exception。如果有,我们可以处理它,例如通过提醒用户;否则,我们只需像平常一样使用实际的结果,并使用结果中的位图:

@Override
protected final void onPostExecute(Result<Bitmap> result) {
  ...
  if ( result.error!= null) {
    // ... alert the user ...
    ...
    Log.e("SafeDownloadImageTask",
          "Failed to download image ",result.exception);
    loadDefaultImage(imageView);
  } else {
    // ... success, continue as normal ...
    imageView.setImageBitmap(result.actual);
  }
}

使用上述安全实现,后台线程上抛出的任何错误都会安全地转发到主线程,并且不会影响AsyncTask的正常生命周期。让我们尝试检索一个不存在的图像,看看异常是否被正确处理:

URL url = new URL("http://img.allw.mn" +
                  "/content/www/2009/03/notfound.jpg");
new SafeDownloadImageTask(ShowMyPuppyActivity.this, iv)
.execute(url);

如预期的那样,错误被捕获,封装在一个Result对象中,并在 Android 日志中打印出来,堆栈跟踪指向SafeDownloadImageTask.doInBrackground方法:

...downloadBitmap(SafeDownloadImageTask.java:85)
...doInBackground(SafeDownloadImageTask.java:60)
...

84: if (responseCode != HttpURLConnection.HTTP_OK){
85:      throw new Exception(...);
86: }

控制并发级别

到目前为止,我们已小心翼翼地避免过于具体地说明当我们调用AsyncTask的 execute 方法时会发生什么。我们知道doInBackground()将在主线程之外执行,但那究竟意味着什么呢?

AsyncTask的原始目标是帮助开发者避免阻塞主线程。在其 API 级别 3 的初始形式中,AsyncTask被排队并按顺序(即一个接一个)在一个单独的后台线程上执行,保证了它们会按照启动的顺序完成。

这在 API 级别 4 中发生了变化,使用最多 128 个线程的线程池来并发执行多个AsyncTask——并发级别高达 128。乍一看,这似乎是个好事,因为AsyncTask的一个常见用途是执行阻塞 I/O,其中线程的大部分时间都在空闲地等待数据。

然而,正如我们在第一章中看到的,构建响应式 Android 应用程序,在并发编程中经常会出现许多问题,实际上,Android 团队意识到,通过默认并发执行AsyncTasks,他们使开发者暴露于潜在的编程问题(例如,当并发执行时,没有保证AsyncTasks 将按它们启动的顺序完成)。

因此,在 API 级别 11 上进行了进一步的更改,默认切换回串行执行,并引入了一个新的方法,将并发控制权交回给应用开发者:

   public final AsyncTask<Params, Progress, Result>
       executeOnExecutor(Executor exec, Params... params)

从 API 级别 11 开始,我们可以使用executeOnExecutor启动 AsyncTasks,并且通过提供 Executor 对象,我们可以自己选择并发的级别。

执行器是 JDK 中java.util.concurrent包的一个接口,如第一章中更详细地描述的,构建响应式 Android 应用程序。它的目的是提供一个提交任务以供执行的方式,而无需精确说明执行将如何或何时进行。Executor的实现可能使用单个线程顺序运行任务,使用有限线程池来控制并发级别,或者甚至为每个任务直接创建一个新线程。

AsyncTask类提供了两个执行器实例,允许你选择本节之前描述的并发级别:

  • SERIAL_EXECUTOR: 这个执行器将任务排队,并确保任务按提交的顺序由AsyncTask线程池顺序执行。

  • THREAD_POOL_EXECUTOR: 这个Executor通过使用线程池来运行任务以提高效率(启动新线程会带来一些开销,这些开销可以通过池化和重用来避免)。THREAD_POOL_EXECUTOR是 JDK 类ThreadPoolExecutor的一个实例,它使用一个线程池,该线程池根据需求增长和缩减。在AsyncTask的情况下,线程池被配置为至少维护五个线程,并扩展到 128 个线程。在 Android Lollipop 5.0(API 级别 21)中,线程的最大数量减少到 CPU 核心数乘以 2 再加 1,并且ThreadPool的全局入队容量增加了。

要使用特定的执行器执行AsyncTask,我们调用executeOnExecutor方法,提供我们想要使用的执行器的引用,例如:

task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
                       params);

由于 API 级别 11 以来执行默认行为是在单个后台线程上串行运行 AsyncTasks,以下两个语句是等价的:

   task.execute(params);
   task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params);

在下一张图中,我们将展示串行执行器和线程池在处理按顺序入队的AsyncTask组时的区别:

new SleepAsyncTask(1).execute(1000);
...
new SleepAsyncTask(4).execute(1000);

控制并发级别

如前图所示,串行执行器使用AsyncTask线程池中的可用线程,但它们只有在之前的AsyncTask完成时才会处理下一个AsyncTask。或者,ThreadPoolExecutor将立即开始处理下一个任务,只要它有可用的线程来完成工作,而不保证它们会按照启动的顺序完成:

注意

重要的是要提到,系统中的所有AsyncTasks都将共享同一个静态执行器AsyncTask.THREAD_POOL_EXECUTOR。对于SerialExecutor来说,情况更糟,因为如果一个AsyncTask长时间占用单个执行器,那么下一个任务将等待在队列中以便处理。

除了AsyncTask提供的默认执行器和java.util.concurrent中可用的执行器之外,我们还可以选择创建自己的。例如,我们可能希望通过操作一小批线程来允许一些并发性,并且当所有线程都忙碌时允许许多任务排队。

这可以通过将我们的ThreadPoolExecutor实例配置为我们的某个类(例如我们的Activity类)的静态成员来轻松实现。以下是如何配置一个具有四到八线程池和有效无限队列的执行器的方法:

  private static final Queue<Runnable> QUEUE =
     new LinkedBlockingQueue<Runnable>();
   public static final Executor MY_EXECUTOR =
     new ThreadPoolExecutor(4, 8, 1, TimeUnit.MINUTES, QUEUE);

构造函数的参数表示核心池大小(4),最大池大小(8),空闲额外线程在池中被移除之前可以存活的时间(1),时间单位(分钟),以及当池线程忙碌时附加工作的队列。

使用我们自己的 Executor 就像如下调用我们的AsyncTask一样简单:

task.executeOnExecutor(MY_EXECUTOR, params);

常见的AsyncTask问题

就像任何强大的编程抽象一样,AsyncTask并非完全没有问题和妥协。在接下来的几节中,我们将列出我们在应用中使用此结构时可能遇到的一些陷阱。

碎片化问题

在“控制并发级别”部分,我们看到了AsyncTask如何随着 Android 平台的新版本发布而发展,导致任务运行设备的平台行为各异,这是碎片化更广泛问题的一部分。

事实上,如果我们针对广泛的 API 级别,我们的AsyncTask的执行特性——因此,我们应用的行为——在不同设备上可能会有很大的差异。那么我们如何减少由于碎片化而遇到AsyncTask问题的可能性?

最明显的方法是故意针对至少运行 Honeycomb 的设备,通过在 AndroidManifest 文件中设置minSdkVersion为 11。这巧妙地将我们归类到默认情况下以串行方式执行AsyncTasks的设备类别,因此行为更加可预测。

到 2015 年 10 月写作时,只有 4%的安卓设备运行在 API 级别 4 和 10 之间的危险区域,因此将你的应用程序定位到 11 级不会显著减少你的市场覆盖范围。

当使用ThreadPoolExecutor作为执行器时,Lollipop(API 级别 21)引入的变化也可能导致与旧版本(API 级别>10)相关的行为漂移。现代AsyncTaskThreadPoolExecutor限制为设备的 CPU 核心数乘以 2 加 1 个并发线程,还有一个额外的 128 个任务的队列来排队工作。

第二种选择是精心设计我们的代码,并在一系列设备上彻底测试——当然,这是值得赞扬的做法,但正如我们所看到的,在没有碎片化增加复杂性的情况下,并发编程就已经足够困难了,而且不可避免地,微小的错误仍然会存在。

安卓开发社区提出的第三种解决方案是在自己的项目中重新实现AsyncTask,然后在 SDK 版本之外扩展自己的AsyncTask类。这样,你就不再受用户设备平台的摆布,可以重新控制你的AsyncTasks。由于AsyncTask的源代码很容易获得,这并不难做到。

内存泄漏

在我们保留对ActivityView的引用的情况下,我们可以在活动被销毁时防止整个对象树被垃圾回收。开发者需要确保取消任务并移除对已销毁活动或视图的引用。

活动生命周期问题

我们故意将任何长时间运行的任务从主线程移除,这使得我们的应用程序变得非常响应——主线程可以快速响应用户的任何交互。

不幸的是,我们也为自己创造了一个潜在的问题,因为主线程能够在我们的后台任务完成之前完成 Activity。Activity 可能因为多种原因而结束,包括用户旋转设备导致的配置更改(Activity 被销毁并再次创建,带有新的内存地址),用户将设备连接到坞站,或任何其他类型的上下文更改。

如果我们在活动结束后继续处理后台任务,我们可能正在进行不必要的操作,因此浪费 CPU 和其他资源(包括电池寿命),这些资源本可以用于更好的用途。

在设备旋转后的某些情况下,AsyncTask仍然有意义,并且有有效的内容要交付,然而,它引用了一个已销毁的活动或视图,因此不再能够更新 UI 并完成其工作以及交付其结果。

此外,AsyncTask持有的任何对象引用在任务明确 null 化这些引用或完成并自身符合GC垃圾回收)条件之前,都不符合垃圾收集的资格。由于我们的AsyncTask可能引用了活动或视图层次结构的一部分,我们很容易以这种方式泄露大量的内存。

AsyncTask的常见用法是将它声明为主活动的一个匿名内部类,这会创建对活动的隐式引用,并导致更大的内存泄漏。

防止这些资源浪费问题有两种方法。

使用早期取消处理生命周期问题

首先,我们可以通过在活动结束时取消正在运行的任务来同步我们的AsyncTask生命周期与活动。

当一个Activity结束时,其生命周期回调方法会在主线程上被调用。我们可以检查生命周期方法被调用的原因,如果活动正在结束,则取消后台任务。最适合此目的的Activity生命周期方法是onPause,它在活动结束时一定会被调用:

   protected void onPause() {
     super.onPause();
     if ((task != null) && (isFinishing()))
       task.cancel(false);
   }

如果Activity没有结束——比如说,因为它启动了另一个Activity并且仍然在后台栈上——我们可能简单地允许我们的后台任务继续完成。

这个解决方案简单明了,但远非理想,因为你可能会在不了解你可能已经有一个有效结果或你的AsyncTask仍在运行的情况下,再次启动后台工作,从而浪费宝贵的资源。

此外,当你启动多个AsyncTasks,并在设备旋转时再次启动它们时,浪费会大幅增加,因为我们不得不再次取消并启动相同数量的任务。

使用保留的无头Fragments处理生命周期问题

如果Activity 因为配置更改而结束,仍然可能有用的是使用后台任务的结果并在重新启动的Activity 中显示它们。实现这一点的模式之一是通过使用保留的Fragments

Fragments是在 Android API 级别 11 中引入的,但可以通过支持库提供给针对更早 API 级别的应用程序。所有可下载的示例都使用支持库,并针对 API 级别 7 到 23。要使用Fragment,我们的Activity必须扩展FragmentActivity类。

Fragment的生命周期与宿主Activity 的生命周期紧密相连,并且当活动重新启动时,Fragment通常会销毁。然而,我们可以通过在Fragment上调用setRetainInstance(true)来显式地防止这种情况,使其能够在活动重新启动后继续存在。

通常,一个Fragment将负责创建和管理Activity用户界面的一部分,但这不是强制性的。不管理自身视图的Fragment被称为无头Fragment。由于它们没有与之相关的 UI,因此当用户旋转设备时,它们不需要被销毁和重新创建。

将我们的AsyncTask隔离在保留的无头Fragment中,使得我们意外泄露对诸如View层次结构等对象的引用的可能性降低,因为AsyncTask将不再直接与用户界面交互。为了演示这一点,我们首先定义一个我们的Activity将实现的接口:

public interface AsyncListener {
    void onPreExecute();
    void onProgressUpdate(Integer... progress);
    void onPostExecute(Bitmap result);
    void onCancelled(Bitmap result);
}

接下来,我们将创建一个保留的无头Fragment,它封装了我们的AsyncTask。为了简洁,省略了doInBackground,因为它与前面的示例没有变化——请参阅可下载的示例以获取完整的代码。

public class DownloadImageHeadlessFragment extends Fragment {

  // Reference to the activity that receives the
  // async task callbacks
  private AsyncListener listener;   
  private DownloadImageTask task;

  // Function to create new instances
  public static DownloadImageHeadlessFragment
    newInstance(String url) {
    DownloadImageHeadlessFragment myFragment = new
                   DownloadImageHeadlessFragment();
    Bundle args = new Bundle();
    args.putString("url", url);
    myFragment.setArguments(args);
    return myFragment;
  }
  // Called to do initial creation of fragment
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    task = new DownloadImageTask();
    url = new URL(getArguments().getString("url"));
    task.execute(url);
  }
  // Called when an activity is attached
  public void onAttach(Activity activity) {
    super.onAttach(activity);
  listener = (AsyncListener)activity;
}

public void onDetach() {
    super.onDetach();
    listener = null;
}
// Cancel the download
public void cancel() {
  if (task != null) {
      task.cancel(false);
  }
}

private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {

   // ... doInBackground elided for brevity ...        }

}

如你所知,一个片段的生命周期与其自身的Activity紧密相连,因此回调函数会按照当前活动生命周期事件的顺序有序调用。例如,当活动停止时,所有附加到该活动的片段都将被分离并通知Activity状态变化。

在我们的示例中,我们使用Fragment生命周期方法(onAttachonDetach)在保留片段中保存或删除当前的Activity引用。

Activity附加到我们的片段时,会调用onCreate方法来创建私有的DownloadImageTask对象,然后调用execute方法在后台开始下载。

newInstance静态方法用于初始化和设置一个新的片段,无需调用其构造函数和 URL 设置器。一旦我们创建了片段对象实例,我们就会使用setArguments函数将图像 URL 保存到由片段参数成员存储的 bundle 对象中。如果 Android 系统恢复我们的片段,它将调用不带参数的默认构造函数,并且还可以利用旧的 bundle 来重新创建片段。

在配置更改期间,每当活动被销毁和重新创建时,setRetainInstance(true)强制片段在活动回收过渡期间存活。正如你所感知的,这种技术在某些情况下可能非常有用,在这些情况下我们不希望重建那些难以重新创建的对象,或者当活动通过配置更改被销毁时具有独立生命周期的对象。

注意

重要的是要知道,retainInstance()只能与不在返回栈中的片段一起使用。在保留片段上,当活动重新附加到新的Activity时,onCreate()onDestroy()不会被调用。

接下来,我们的Fragment必须管理和执行一个DownloadImageTask,该任务通过AsyncListener接口代理进度更新和结果回传到Activity

private class DownloadImageTask extends AsyncTask<URL, Integer, Bitmap> {
  ...
  protected void onPreExecute() {
    if (listener != null)
      listener.onPreExecute();
  }
  protected void onProgressUpdate(Integer... values) {
    if (listener != null)
      listener.onProgressUpdate(values);
  }
  protected void onPostExecute(Bitmap result) {
    if (listener != null)
      listener.onPostExecute(result);
  }
  protected void onCancelled(Bitmap result) {
    if (listener != null)
      listener.onCancelled(result);
  }
}

如前所述,AsyncListener 是负责使用从我们的后台任务中获取的结果更新 UI 的实体。

现在,我们只需要一个实现了 AsyncListener 并使用 DownloadImageHeadlessFragment 来实现其长时间运行任务的主 Activity。完整的源代码可以从 Packt Publishing 网站下载,所以我们将只关注亮点:

public class ShowMyPuppyHeadlessActivity
    extends FragmentActivity implements     
    DownloadImageHeadlessFragment.AsyncListener {

  private static final String DOWNLOAD_PHOTO_FRAG =        
                         "download_photo_as_fragment";
   ..
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    FragmentManager fm = getSupportFragmentManager();
    downloadFragment = (DownloadImageHeadlessFragment)
       fm.findFragmentByTag(DOWNLOAD_PHOTO_FRAG);

    // If the Fragment is non-null, then it is currently being
    // retained across a configuration change.
    if (downloadFragment == null) {
     downloadFragment = DownloadImageHeadlessFragment.
          newInstance("http://img.allw.mn/content" +
                      "/www/2009/03/april1.jpg");
           fm.beginTransaction().add(downloadFragment,    
       DOWNLOAD_PHOTO_FRAG).
     commit();     
    }
  }

首先,当活动在 onCreate 回调中创建时,我们检查 Fragment 是否已存在于 FragmentManager 中,并且只有在它缺失时才创建其实例。

当 Fragment 创建时,我们通过 newInstance 方法构建一个 Fragment 实例,然后将 Fragment 推送到 FragmentManager,该实体将存储并执行转换。

如果我们的 Activity 已经被重新启动,当接收到进度更新回调时,它需要重新显示进度对话框,因此我们在更新进度条之前检查并显示它(如果需要的话):

@Override
public void onProgressUpdate(Integer... value) {
  if (progress == null)
    prepareProgressDialog();

  progress.setProgress(value[0]);
}

最后,Activity 需要实现 AsyncListener 定义的 onPostExecuteonCancelled 回调。onPostExecute 将更新 resultView,就像之前的示例一样,两者都会进行一些清理工作——关闭对话框并移除 Fragment,因为其工作已经完成:

@Override
public void onPostExecute(Bitmap result) {
  if (result != null) {
    ImageView iv = (ImageView) findViewById(
                     R.id.downloadedImage);
    iv.setImageBitmap(result);
  }
  cleanUp();
}

// When the task is cancelled the dialog is dimissed
@Override
public void onCancelled(Bitmap result) {
  cleanUp();
}

// Dismiss the progress dialog and remove the
// the fragment from the fragment manager
private void cleanUp() {
  if (progress != null) {
    progress.dismiss();
    progress = null;
  }
  FragmentManager fm = getSupportFragmentManager();
  Fragment frag = fm.findFragmentByTag(DOWNLOAD_PHOTO_FRAG);
  fm.beginTransaction().remove(frag).commit();
}

这种技术,在 Android 开发社区中广为人知,被称为无头 Fragment,简单且一致,因为它在每次配置更改发生时将重新创建的活动附加到无头 Fragment 上。在 Fragment 上维护一个活动引用,并在 Fragment 附加(活动创建)和分离(活动销毁)时更新。

利用这种模式,AsyncTask 永远不必跟随配置更改的不可预测发生,或者担心在完成工作后进行 UI 更新,因为它将生命周期回调转发到当前的 Activity

AsyncTask 的应用

现在我们已经看到了如何使用 AsyncTask,我们可能会问自己何时应该使用它。

适用于 AsyncTask 的良好候选应用通常是相对短暂的操作(最多一秒或两秒),这些操作直接关联到特定的 FragmentActivity,并需要更新其用户界面。

AsyncTask 对于运行短时间、CPU 密集型任务(如数值计算或在大文本字符串中搜索单词)非常理想,这样可以将其从主线程移除,以便保持对输入的响应性并维持高帧率。

阻塞 I/O 操作,如读取和写入文本文件,或使用 BitmapFactory 从本地文件加载图像,也是 AsyncTask 的良好用例。

当然,对于 AsyncTask 来说不太适合的场景也存在。对于任何需要超过一秒或两秒的操作,如果用户旋转设备,或者在不同应用或活动之间切换,或者发生我们无法控制的其他情况,我们应该权衡重复执行此操作的成本。

考虑到这些因素,以及我们在尝试处理它们时复杂性的增加速度(例如,保留的无头片段!),AsyncTask 在处理长时间操作时开始失去其光泽。

AsyncTask 通常用于从远程网络服务器获取数据,但这可能会遇到我们之前探讨的活动生命周期问题。最终用户可能在使用不稳定的 3G 或 HSDPA 连接,其中网络延迟和带宽可能差异很大,一个完整的 HTTP 请求-响应周期可能轻易跨越数秒。当我们上传大量数据时,例如图像,这一点尤为重要,因为可用带宽通常是不对称的。

虽然我们必须在主线程之外执行网络 I/O,但 AsyncTask 并不一定是最理想的选择——正如我们稍后将会看到的;还有更多适合从主线程卸载此类工作的结构。

当我们想要在 AsyncTasks 上组合或链式处理后台处理时,我们可能会陷入一个难以管理回调和协调工作的情况,因此 AsyncTask 在这里不会帮到你。

在下一章中,我们将介绍并详细说明其他技术,以清晰的方式处理这些类型的问题。

摘要

在本章中,我们详细探讨了 AsyncTask 及其如何用于编写响应式应用程序,这些应用程序可以在不阻塞主线程的情况下执行操作。

我们看到了如何让用户了解进度,甚至允许他们提前取消操作。我们还学习了如何处理当活动生命周期与我们后台任务作对时可能出现的问题。

最后,我们考虑了何时使用 AsyncTask,以及何时可能不合适。

在下一章中,我们将学习关于 Loader 的内容——这是一个旨在简化 Android 平台上异步加载数据的结构。

第四章:探索 Loader

在上一章中,我们熟悉了最简单和高级的、特定于 Android 的异步构造;即android.os.AsyncTaskAsyncTask是一个轻量级构造,用于创建后台工作,它提供了一个简单的接口来发布结果并将进度发送到主线程。在本章中,我们将把重点转移到android.content.Loader上,这是一个高级的、特定于 Android 的模式,用于通过工作线程异步从内容提供者或数据源加载数据,并具有内容变更能力和组件生命周期意识。

在本章中,我们将涵盖以下主题:

  • 介绍 loaders

  • Loader API

  • Loader 生命周期

  • 使用 Loader 加载数据

  • 使用 AsyncTaskLoader 构建响应式应用

  • 使用 CursorLoader 构建响应式应用

  • 组合 loaders

  • Loaders 的应用

介绍 Loaders

如其名所示,Loader的工作是代表应用的其他部分加载数据,并使该数据在相同进程内的活动(Activity)和片段(Fragment)之间可用。Loader 框架被创建出来是为了解决与 Activity 和 Fragment 中异步加载相关的一些问题:

  • 后台处理:繁重的工作自动在后台线程上执行,并在完成后安全地将结果引入主线程。

  • 结果缓存:加载的数据可以被缓存,并在重复调用时重新发送以提高速度和效率。

  • 生命周期意识:该框架让我们能够控制 Loader 实例何时被销毁,并允许 Loader 在Activity生命周期之外存活,使得它们的数据可以在整个应用和Activity重启之间可用。

  • 数据变更管理:Loaders 监控其底层数据源,并在必要时在后台重新加载数据。框架包括生命周期回调,允许我们正确地处理 Loader 持有的任何昂贵资源。

Loader API

Loader API 是在 API 级别 11 引入到 Android 平台的,但通过支持库向后兼容。本章中的示例使用支持库针对 API 级别 7 到 23。

框架定义了接口、抽象类和 loader 实现,以创建适用于您应用的 Android 一级数据加载器。

Loaders 能够监控内容并传递新的变更,并且能够在活动过渡或由配置更改触发的替换活动之间存活。此框架提供的 API 类和接口包括:

  • android.content.Loader<DataType>:非功能性(抽象)基类,定义了基本方法

  • android.app.LoaderManager:管理 Activity 和 Fragment 中的 loader

  • android.app.LoaderManager.LoaderCallbacks:用于监听 Loader 事件的回调

  • android.content.AsyncTaskLoader<DataType>:执行加载操作的Loader子类

  • android.content.CursorLoader:用于处理 Android 内部数据库和内容提供者数据源的加载器实现

最后两个类是非抽象子类,我们将在本章的下一节通过示例详细说明。

Loader

Loader 是一个泛型类型类,它本身不实现任何异步行为,并公开一个泛型类型参数:

public class Loader<DataType>

<DataType> 泛型类型定义了 Loader 将要提供的结果类型,并且应该由实现特定领域 Loader 的任何子类定义。

当你创建自己的加载器时,Loader 有五种方法我们必须实现以创建一个完全功能的加载器:

protected void onStartLoading()
protected void onStopLoading()
protected void onForceLoad()
protected void onReset()
protected void onCancelLoad()

onStartLoading() 方法是子类必须实现的方法,用于开始加载数据,onStopLoading() 是一个用于实现当请求停止加载因为相关联的活动或片段停止时的行为的方法。在此状态下,Loader 可能会继续处理,但不应在再次调用 onStartLoading() 之前向 Activity 提供更新。

onForceLoad() 是一个你应该实现的方法,用于忽略之前加载的数据集并加载一个新的数据集,就像清除缓存一样,而 onReset() 方法是 LoaderManager 自动调用以释放任何未再次调用的加载器的资源的方法。

onCancelLoad() 是在主线程上调用,用于实现调用 Loader.cancelLoad() 后取消加载时的行为。

虽然我们可以直接扩展 Loader,但更常见的是根据我们的需求使用提供的两个子类之一,AsyncTaskLoaderCursorLoader

AsyncTaskLoader 是一个通用目的的 Loader,当我们想要从任何类型的源加载几乎任何类型的数据,并且希望在主线程之外完成时,我们可以从它派生子类。

CursorLoader 扩展了 AsyncTaskLoader,专门用于高效地从本地数据库获取数据并正确管理相关的数据库 Cursor

Loader Manager

当我们使用加载器时,我们不会孤立地使用它们,因为它们是小型框架的一部分。加载器是管理对象,由 LoaderManager 负责管理,它负责协调加载器生命周期事件与 FragmentActivity 生命周期,并在整个应用程序中使 Loader 实例对客户端代码可用。在 android.support.v4.content.LoaderManagerandroid.app.LoaderManager 中定义的 LoaderManager 抽象类可以通过所有活动和片段的成员函数 getLoaderManager 访问:

LoaderManager getLoaderManager()
// android.support.v4
LoaderManager getSupportLoaderManager();

LoaderManager 提供了一个 API,客户端(活动或片段)可以使用它来设置、初始化、重新启动和销毁加载器,而无需绑定到客户端生命周期。当你检索客户端管理的 LoaderManager 实例时,Loader Manager 的最相关的方法是:

Loader<D> initLoader(int id, Bundle args,
                     LoaderManager.LoaderCallbacks<D> callback)

Loader<D> restartLoader(int id,Bundle args, 
                        LoaderManager.LoaderCallbacks<D> callback)
Loader<D> getLoader(int id);
void destroyLoader(int id);

LoaderManager 定义的 所有方法中,id 参数用于标识客户端上下文中的 Loader,并且还用于所有 LoaderManager API 中以触发特定 Loader 的任何操作。

initLoader 方法用于初始化特定的 Loader,但如果 LoaderManager 中已经存在具有相同 ID 的 Loader,则不会创建新的 Loader。

restartLoader 方法启动或重新启动一个 Loader,但如果传入 ID 的 Loader 已经存在,则当它完成工作后,旧 Loader 将被销毁。

destroyLoader 方法停止并从 LoaderManager 中显式移除由参数 id 指定的 Loader

LoaderManager.LoaderCallbacks

要与 LoaderManager 交互,客户端需要实现 LoaderCallbacks<D> 接口并接收事件以创建具有给定 ID 的新 Loader、接收 Loader 结果或重置 Loader:

Loader<D> onCreateLoader(int id, Bundle args)
void onLoadFinished(Loader<D> loader, D data)
void onLoaderReset(Loader<D> loader)

如我们之前详细说明的,D 泛型类型指定了 Loader 返回的数据类型,并且当 Loader 的生命周期达到特定状态时,由 LoaderManager 调用这些回调:

  • onCreateLoader: 这是一个创建方法,用于为指定的 ID 和给定的 Bundle 对象启动一个 Loader。Bundle 对象用于传递 Loader 创建的参数。当客户端调用 initLoader 而且 LoaderManager 中不存在具有该 ID 的 Loader 时,将调用此方法。

  • onLoadFinished: 当 Loader 获取到结果时,将调用此方法;回调将使用结果和检索结果的 Loader 引用被调用。当 Loader 检测到请求的数据内容发生变化时,它将报告新的结果,因此此方法可能会被多次调用。此方法通常用于使用加载的数据更新 UI。

  • onLoaderReset: 当给定 ID 的 Loader 即将被销毁时,将调用此方法。这是释放与指定 ID 关联的一些资源和引用的最佳位置。

Loader 生命周期

LoaderManager 管理的任何 Loader 对象都可以处于六个不同的标志中,这些标志定义了 Loader 的状态:

  • 重置: 当你创建 Loaders 实例时,将设置此标志。如果调用 reset() 方法,标志将结束于此。当重置移动到此状态时,将调用 onReset(),开发者必须使用此方法释放 Loader 上分配的资源以及重置任何缓存结果。

  • 启动: 当调用你的 loaderstartLoading() 方法时,将设置此标志。在 Loader 进入此状态后,onStartLoading 方法会被调用以设置加载资源。如果 Loader 已经提供了结果,你可以调用 forceLoad() 来重新启动新的加载。你的 Loader 应该在标志处于开启状态时才提供结果。

  • 停止:当加载器停止且无法提供新结果或内容更改的交付时,会设置此标志。在此状态下,加载器可以存储结果以在加载器重新启动时交付。为了实现加载器具有此状态时的行为,开发者必须实现 onStopLoading 并释放为加载结果分配的所有资源。

  • 废弃:这是一个可选的中间标志,用于确定 Loader 是否被废弃。与其他方法一样,子类必须实现 onAbandon() 以在客户端不再对来自加载器的新数据更新感兴趣时实现行为。在此状态下,在重置之前,加载器不得报告任何新的更新,但它可以保留在加载器重新启动时交付的结果。

  • 内容更改:这是一个用于通知 Loader 内容已更改的标志。当检测到 Load 上的内容更改时,会调用 onContentChanged 回调。

  • 处理更改:这是一个用于通知 Loader 内容正在处理其内容更改的标志。以下函数 takeContentChanged()commitContentChanged()rollbackContentChanged() 用于管理数据内容更改及其处理状态。Loader 生命周期

    图 4.1:Loader 生命周期

使用 Loader 加载数据

到目前为止,我们只描述了理论实体和 API 上可用的类,因此现在是时候通过一个简单的示例来展示这些概念了。

在我们的示例中,我们将向您展示如何使用 LoaderManagerLoaderCallback 和一个 Loader 来展示一个列出当前在线用户名称的 Activity,适用于聊天应用程序。

首先,我们将创建一个 Activity,它将充当 LoaderManager 的客户端,并将有三个按钮,初始化重启销毁;分别用于初始化加载器、重启加载器和销毁加载器。Activity 将直接接收 LoaderCallbacks 回调,因为它实现了该接口作为成员函数:

public class WhoIsOnlineActivity extends FragmentActivity
  implements LoaderCallbacks<List<String>> {
  public static final int WHO_IS_ONLINE_LOADER_ID = 1;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
   ...
    final LoaderManager lm =  getSupportLoaderManager();
    final Bundle bundle =new Bundle();
    bundle.putString("chatRoom", "Developers");
    initButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
 lm.initLoader(WHO_IS_ONLINE_LOADER_ID, bundle,
 WhoIsOnlineActivity.this);
      }
    });
    restartButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
 lm.restartLoader(WHO_IS_ONLINE_LOADER_ID, bundle,
 WhoIsOnlineActivity.this);
      }
    });
    destroyButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
 lm.destroyLoader(WHO_IS_ONLINE_LOADER_ID);
      }
    });
 }
}

点击 初始化 按钮将使用指定的 ID 和一个包含我们用于传递到 Loader 的参数的 bundle 对象初始化 Loader,正如之前所说的,重启 按钮将销毁一个已存在的加载器并创建一个新的加载器,而 销毁 按钮将销毁具有给定 ID 的加载器(如果它已经在 LoaderManager 中存在)。这些按钮在这里仅用于帮助我们解释 LoaderManagerLoader 之间的交互和流程。

在这个特定的用例中,我们将加载聊天室开发者的在线用户列表。

现在让我们看看 LoaderCallback 函数,并在我们的 Activity 上实现该接口。

onCreateLoader 开始,这个 LoaderCallback 回调只有在加载器之前不存在或通过调用 LoaderManager.restartLoader() 重新启动加载器时才会被调用。

当我们通过 LoaderManagerinitLoader 方法初始化 WhosOnlineLoader 时,它将返回具有给定 ID (LOADER_ID) 的现有 Loader,或者如果没有具有该 ID 的 Loader,它将调用第一个 LoaderCallbacks 方法——onCreateLoader

这意味着此方法不会在配置更改时被调用,因为已经有一个具有此 ID 的先前加载器可用并已初始化。

@Override
public Loader<List<String>> onCreateLoader(int id, Bundle args) {
  Loader res = null;
  switch (id) {
    case WHO_IS_ONLINE_LOADER_ID:
      res = new WhosOnlineLoader(this,
                                 args.getString("chatRoom"));
      break;
  }
  return res;
}

此方法通过调用 WhosOnlineLoader 构造函数并传递我们试图加载的聊天组名称来创建 Loader 实例。

下一个实现的 LoaderCallback 函数回调是 onLoadFinished;此回调在加载器获得新结果、数据更改或配置更改已存在于 LoaderManager 中的 Loader 时被调用。

@Override
public void onLoadFinished(Loader<List<String>> loader,
                  List<String> users) {
     switch (loader.getId()) {
       case WHO_IS_ONLINE_LOADER_ID:
      ListView listView = (ListView) findViewById(R.id.list);
      ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1,
        android.R.id.text1,
        users);
      listView.setAdapter(adapter);
      break;
    }
  }

在我们的例子中,当 onLoadFinished 被调用时,我们使用从加载器接收到的用户列表更新 ListView 适配器。

OnLoaderReset,我们最后的 LoaderCallback 函数回调,在加载器被销毁时被调用,在我们的例子中,它只是在其适配器中清理列表视图数据:

@Override
public void onLoaderReset(Loader<List<String>> loader) {
  ...
  ListView listView = (ListView)findViewById(R.id.list);
  listView.setAdapter(null);
}

当调用 LoaderManager.destroyLoader(id) 或当 Activity 被销毁时,将调用加载器重置。如前所述,加载器重置不会销毁加载器,而是告诉加载器不要发布进一步的更新。因此,它可以跨越多个 Activity。

这个蛋糕的最后一块是我们的自定义 Loader,即 WhosOnlineLoader,用于检索在线用户列表。我们的 WhosOnlineLoader 不会加载任何异步结果,因为 Loader 子类不管理用于加载结果的后台线程。因此,这个 Loader 应仅用于示例目的,并解释 LoaderManager 和自定义 Loader 的交互特性。

为了调试目的,方法 onStartLoadingonStopLoadingonResetonForceLoad 在每次进入函数时都会打印一条日志消息。deliverResult(),即向已注册的监听器传递加载结果的 Loader 函数,也会将包含在线用户信息的消息打印到 Android 日志中。

public class WhosOnlineLoader extends Loader<List<String>> {

  private final String mChatRoom;
  private List<String> mResult = null;

  public WhosOnlineLoader(Context context, String chatRoom) {
    super(context);
    this.mChatRoom = chatRoom;
  }
  @Override
  protected void onStartLoading() {
    Log.i("WhoIsOnlineLoader", "onStarting WhoIsOnlineLoader["
          + Integer.toHexString(hashCode()) + "]");
    ...
    forceLoad();   
  }
  // Elided for brevity
  @Override
  public void deliverResult(List<String> data) {
    Log.i("WhoIsOnlineLoader", "DeliverResult WhoIsOnlineLoader["
          + Integer.toHexString(hashCode()) + "]");
    ...
    super.deliverResult(data);   
  }
  @Override
  protected void onReset() {
    Log.i("WhoIsOnlineLoader", "onReset WhoIsOnlineLoader["
          + Integer.toHexString(hashCode()) + "]");
    onStopLoading();
    ...
  }

}

故意省略了 WhosOnlineLoader 代码的部分内容,尽管 WhosOnlineLoader 的源代码可以从 Packt Publishing 网站下载。

注意

一切准备就绪后,如果我们启动 Activity,在线用户列表将为空,尽管点击 初始化 按钮会导致 LoaderManager.init 调用。

由于我们在每个 Loader 生命周期开始处有一些跟踪消息,我们可以跟踪加载器回调调用:

I ... LoaderManager.init [1]
I ... LoaderCallbacks.onCreateLoader[1]
I ... Loader.new[ee07113]
I ... Loader.onStarting[ee07113]
I ... Loader.onForceload[ee07113]
I ... Loader.deliverResult[ee07113]
I ... LoaderCallbacks.onLoadFinished[1]

如日志输出所示,当我们调用 LoaderManager.init 函数并且在此期间调用 onCreateLoader 时,将创建一个新的 Loader 对象实例,其 hashCodeee07113。之后,加载器开始运行,并在 onLoadFinished 回调中加载结果,传递用户列表。

由于现在具有该 ID 的 Loader 已经存在于 LoaderManager 中,让我们检查当我们点击 重启 按钮时会发生什么:

I ... LoaderManager.restart [1]
I ... LoaderCallbacks.onCreateLoader[1]
I ... Loader.new[fb61f50]
I ... Loader.onStarting[fb61f50]
I ... Loader.onForceload[fb61f50]
I ... Loader.deliverResult[fb61f50]
I ... LoaderCallbacks.onLoadFinished[1]
I ... Loader.onReset[ee07113]
I ... Loader.onStopping[ee07113]

由于之前创建了 Loader ee07113,它将被停止并重置,并且将创建并启动一个新的加载器实例,就像在 init 中做的那样。

现在我们将点击 销毁 按钮并检查结果:

I ... LoaderManager.destroy [1]
I ... LoaderCallbacks.onLoaderReset[1]
I ... Loader.onAbandon[fb61f50]
I ... Loader.onReset[fb61f50]
I ... Loader.onStopping[fb61f50]

如预期,LoaderManager.destroy 被调用,之后调用了 onAbandononResetonStopping Loader 成员方法来停止发送结果,释放加载器资源,并停止加载数据。当加载器停止时,我们必须取消任何加载,但它仍然可以监控数据源的变化。

另一个非常重要的情况需要解释的是配置更改。在这种情况下,LoaderManager 将继续接收结果并将它们保存在本地缓存中。一旦新的活动变得可见,缓存的结果将通过 LoaderCallbacks.onLoadFinished 方法传递。

在没有涉及配置更改的典型 Activity 转换中,LoaderManager 会自动重置 Loader,导致调用加载器停止和重置功能。

既然我们现在已经了解了如何使用 LoaderManager 来管理活动中的加载器,现在我们可以集中精力研究如何使用子类 AsyncTaskLoaderLoaderCursor 来创建异步加载器。

使用 AsyncTaskLoader 构建响应式应用

AsyncTaskLoader 是一个使用 AsyncTasks 来执行其后台工作的加载器实现,尽管当我们实现自己的子类时,这部分对我们来说是隐藏的。

我们不需要担心 AsyncTasks——它们被 AsyncTaskLoader 完全隐藏——但根据我们之前关于 AsyncTask 的了解,值得注意的是,任务默认情况下是使用 AsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) 执行的,以确保在多个加载器使用时具有高度的并发性。

注意

兼容包中的 AsyncTaskLoader (android.support.v4.content) 并不依赖于平台中的公共 AsyncTask。相反,兼容包使用一个内部的 ModernAsyncTask 实现来避免 Android 的碎片化。ModernAsyncTask 会创建名为 ModernAsyncTask #<N> 的线程。

在下一节中,我们将使用 AsyncTaskLoader 在后台加载货币到比特币的汇率,并在我们的 BitcoinExchangeRateActivity 中使用 LoaderManager 显示更新的汇率。

将会使用 onContentChanged() 加载器方法持续刷新汇率,这个方法在此情况下用于在后台强制进行新的汇率更新。

加载器是泛型类型,因此当我们实现它时,需要指定它将加载的对象类型——在我们的例子中是 Double

public class BitcoinExchangeRateLoader extends
  AsyncTaskLoader<Double> {
  // ...
}

Loader 抽象类要求在构造函数中传递一个 Context,因此我们必须将 Context 传递到链中。我们还需要知道要检索哪种货币汇率以及刷新时间间隔,因此我们还将传递一个用于标识货币的字符串和一个表示间隔(毫秒)的整数:

private Double       mExchangeRate = null;
private final long   mRefreshinterval;
private final String mCurrency;

BitcoinExchangeRateLoader(Context ctx,
                          String currency,
                          int refreshinterval) {
  super(ctx);
  this.mRefreshinterval = refreshinterval;
  this.mCurrency = currency;
}

我们不需要保留自己的 Context 对象引用——Loader 提供了一个 getContext() 方法,我们可以在类的任何可能需要 Context 的地方调用它。

注意

我们可以安全地将 Activity 实例的引用作为 Context 参数传递,但不应期望 getContext() 返回相同的对象!Loader 可能比单个 Activity 存活时间更长,因此 Loader 类的父类只保留对应用程序 Context 的引用,这是一个与应用程序关联的 Context,以防止内存泄漏。

我们需要覆盖几个方法,我们将逐个处理。最重要的是 loadInBackground——我们的 AsyncTaskLoader 的主要工作马,并且是唯一不在主线程上运行的方法:

@Override
public Double loadInBackground() {
  //...
}

AsyncTaskLoader 是基于 AsyncTask 的 Loader 子类。在底层,它在一个 AsyncTask 的后台线程中调用 loadInBackground 函数。

我们将从互联网上获取实时比特币汇率,更确切地说,从 blockchain.info 网站上获取,以建立连接的延迟,在设备与远程端点之间传输数据以及由接入网络暴露的一些延迟。由于延迟可以从毫秒到秒不等,这项任务是一个很好的候选任务,可以在主线程之外执行。

下图显示了 Loader 的生命周期,显示了由 LoaderManager 触发的回调以及典型的 AsyncTaskLoader 实现:

使用 AsyncTaskLoader 构建响应式应用

由于从网络加载汇率涉及到由于从网络进行阻塞 I/O 读取而导致的延迟,并且远程网站可能还没有足够的资源来发送响应,因此,作为不想生成令人烦恼的 Android ANR 的有意识的开发者,我们必须将这些操作转移到由系统后台线程执行的 AsyncTaskLoader.loadInBackground 方法中。

在我们收到包含汇率的响应后,我们需要解码 HTTP 响应中包含的 JSON 响应,因此这也是我们肯定希望在主线程之外执行的操作!

public Double loadInBackground() {
  Double result = null;
  StringBuilder builder = new StringBuilder();
  URL url = new URL("https://blockchain.info/ticker");

  // Create a Connection to the remote Website
  HttpURLConnection conn = (HttpURLConnection)
                           url.openConnection();
  ...
  conn.setRequestMethod("GET");
  conn.setDoInput(true); 
  conn.connect();
  // ! Read the response with the exchange rate to a String
  ...
  // Decode the Response Received by Blockchain Website
  JSONObject obj = new JSONObject(builder.toString());
  result = obj.getJSONObject(mCurrency)
                      .getDouble("last");
  return result;
}

在前面的代码中,我们执行了之前建议的阻塞操作,因此我们返回了在 Loader 构造中指定的货币的汇率。

我们将想要缓存我们传递的 Double 对象的引用,以便任何未来的调用都可以立即返回相同的 Double。我们将通过覆盖在主线程上调用的 deliverResult 方法来实现这一点:

@Override
public void deliverResult(Double result) {
  this.mExchangeRate = result;
  super.deliverResult(result);
}

要使我们的Loader真正工作,我们仍然需要重写由Loader基类定义的一些生命周期方法。最重要的是onStartLoading

@Override
protected void onStartLoading() {

  if (mExchangeRate != null) {
    // If we currently have a result available, deliver it
    // immediately.
    deliverResult(mExchangeRate);
  }
  if (takeContentChanged() || mExchangeRate == null) {
    // If the exchange rate has changed since the last time
    // it was  loaded or is not currently available, start a load.
    forceLoad();
  }
}

在这里,我们检查我们的缓存(mExchangeRate)以查看我们是否有之前加载的结果,我们可以通过deliverResult立即交付。如果内容数据最近已更改,contentChanged标志为真,并且我们没有缓存的结果,我们将强制进行后台加载——否则我们的Loader将永远不会加载任何内容。如前所述,此回调在主线程上运行,加载将在后台线程上的loadInBackground()中触发新的加载。

现在,我们有一个最小的工作Loader实现,但如果我们想让Loader与框架良好协作,还需要做一些维护工作。

首先,我们需要确保在Loader被丢弃时清理汇率。Loader提供了一个用于此特定目的的回调——onReset

@Override
protected void onReset() {
  // Ensure the loader is stopped
  onStopLoading();
  mExchangeRate = null; 
}

框架将确保在Loader被丢弃时调用onReset,这将在应用退出或通过LoaderManager显式丢弃Loader实例时发生。

还有另外两个生命周期方法,如果我们想使我们的应用尽可能响应,则必须正确实现:onStopLoadingonCanceled(请注意onCanceled在这里的拼写与大多数地方的onCancelled不同)。

框架将通过调用onStopLoading回调来告诉我们它不想浪费周期加载数据。尽管如此,它可能仍然需要我们已加载的数据,并且它可能告诉我们再次开始加载,因此我们不应清理资源。在AsyncTaskLoader中,我们希望如果可能的话取消后台工作,所以我们只需调用超类cancelLoad方法:

@Override
protected void onStopLoading() {
  // Attempt to cancel the current load task.
  cancelLoad();
}

Loader被取消时,我们不会停止当前汇率加载;尽管如此,在其他类型的用例中,我们可能在loadInBackground()上有一个取消行为,通过检查isAbandoned()成员函数来停止当前加载。

最后,我们需要实现onCancelled来清理在取消发出后可能在后台加载的任何数据:

@Override
public void onCanceled(Double data) {
   // For our data there is nothing to release, at this method
   // we should release the resources associated with 'data'.
}

根据 Loader 产生的数据类型,我们可能不需要担心清理取消工作的结果——普通的 Java 对象将在它们不再被引用时由垃圾收集器清理。

到目前为止,我们已经实现了异步汇率加载,现在我们必须实现刷新功能,以便持续从 blockchain.info 网站获取值。为了为当前汇率加载新值,我们应该强制加载器再次运行loadInBackground并检索当前汇率值。Loader抽象类为我们提供了onContentChanged()方法,该方法将强制在Loader处于启动状态时进行新的加载。

在我们的示例中,一旦使用startLoading()启动 Loader,我们必须连续调用onContentChanged来模拟值变化并强制重新加载。我们将通过使用 handler 和发布一个简单地调用我们的Loader上的onContentChangeRunnable来实现这一点。

  1. 首先,我们将创建Runnable并在我们的 Loader 中创建 handler:

    public class BitcoinExchangeRateLoader extends
                 AsyncTaskLoader<Double> { 
    
      private Handler mHandler;
    
     // Use to force a exchange rate value change
      private final Runnable refreshRunnable = new Runnable() {
        @Override
        public void run() { onContentChanged(); }
      };
    
      BitcoinExchangeRateLoader(Context ctx,
                                String currency,
                                int refreshinterval) {
        ...
        this.mHandler = new Handler();   
      }
    }
    
  2. 第二,我们需要提交一个延迟任务来强制下一次重新加载,每次调用forceLoad()时。当Loader被重置时,我们不提交下一次重新加载:

    @Override
    protected void onForceLoad() {
      mHandler.removeCallbacks(refreshRunnable);
    
      if (!isReset())
        mHandler.postDelayed(refreshRunnable, mRefreshinterval);
    }
    
  3. 第三,为了在 Loader 被取消并在之后重新启动任务时强制重新加载,onCanceled()通过调用onContentChanged()来设置ContentChange标志:

      @Override
        public void onCanceled(Double data) {
           ...
           onContentChanged();
        }
    
  4. 最后,我们必须在 Loader 停止或取消时取消下一次重新加载:

    @Override
    protected void onReset() {
      ...
      mHandler.removeCallbacks(refreshRunnable);
    }
    

到目前为止一切顺利——我们有一个Loader。现在我们需要将其连接到客户端 Activity 或 Fragment。由于在之前的示例中我们将 Loader 附加到了 Activity 上,这次我们将使用不同的LoaderManager客户端并将 Loader 连接到 Fragment 对象。

我们由BitcoinExchangeRateActivity加载的Fragment将初始化 Loader 并在片段 UI 上显示 Loader 的结果。让我们首先处理这些简单的部分:

public class BitcoinExchangeRateFragment extends Fragment 
implements LoaderManager.LoaderCallbacks<Double> {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    LoaderManager lm = getActivity().getSupportLoaderManager();
    Bundle bundle = new Bundle();
    bundle.putString(CURRENNCY_KEY, "EUR");
    bundle.putInt(REFRESH_INTERNAL, 5000);
    lm.initLoader(BITCOIN_EXRATE_LOADER_ID, bundle,
                   BitcoinExchangeRateFragment.this);
  }
  ...
}

在前面的代码中,我们主要加载用于在屏幕上显示汇率的 UI 布局,并在onActivityCreated成员函数上实现我们的 loader 初始化。onActivityCreated成员类回调在活动被创建或配置更改后活动被重新创建时调用。

正如我们在前面的章节中解释的,我们调用initLoader,传递一个int标识符作为第一个参数,一个包含值Bundle的第二个参数来配置我们希望在屏幕上显示的货币汇率,以及onContextChange调用之间的刷新率间隔。第三个参数是实现LoaderCallbacks的对象,在这种情况下,是我们的BitcoinExchangeRateFragment实例,我们在片段上直接实现 loader 回调。

我们在 Fragment 上实现的onCreateLoader回调方法与我们在之前的WhoIsOnlineActivity Loader 上创建的方法类似,因此它基本上使用传递给Bundle对象的参数创建一个新的BitcoinExchangeRateLoader实例。

public Loader<Double> onCreateLoader(int id, Bundle args) {
  Loader res = null;
  switch (id) {
  case BITCOIN_EXRATE_LOADER_ID:
    res = new BitcoinExchangeRateLoader(getActivity(),
          args.getString(CURRENNCY_KEY),
          args.getInt(REFRESH_INTERNAL));
    break;
  }
  return res;
}

onLoadFinished的实现必须获取加载的汇率并在TextView中显示:

@Override
public void onLoadFinished(Loader<Double> loader, Double data) {
  switch (loader.getId()) {
  case BITCOIN_EXRATE_LOADER_ID:
    TextView tv  = (TextView) getView().
                   findViewById(R.id.temperature);
    tv.setText(data.toString());
    break;
  }
}

为了简洁,我们省略了LoaderCallbacks.onLoaderReset,因为该方法体为空。此方法应用于释放与 Loader 生命周期直接绑定的任何资源。

小贴士

完整的源代码,包括活动和android.xml布局,可在 Packt Publishing 网站上找到。

AsyncTask相比,这里的情况更复杂——我们不得不编写更多的代码并处理更多的类,但回报是数据被缓存以供Activity重启使用,并且可以从其他 Fragment 或 Activity 中使用。

在我们的BitcoinExchangeRateLoader中,连续的汇率更新由我们的刷新率间隔控制;然而,在其他类型的AsyncTaskLoaders中,内容变更发生的速率可能会导致大量的onLoadFinished调用,从而可能用 UI 更新主导 UI 线程执行,并降低 UI 响应性。

为了克服这个问题,AsyncTaskLoader提供了一个名为setUpdateThrottle的成员函数,用于控制连续数据交付之间的最小间隔,从而调整连续onLoadFinished调用的间隔:

public void setUpdateThrottle(long delayMS)

当你觉得你的 loader 内容变更率可能会超载 UI 并影响应用程序的流畅性时,必须调用此方法。如果你的数据不需要更高的更新频率,开发者可以利用此功能来减少 Loader 内容变更的交付频率。

在下一节中,我们将详细概述 Android SDK 中随盒提供的最后一个 Loader 子类类型,即CursorLoader

使用 CursorLoader 构建响应式应用程序

CursorLoaderAsyncTaskLoader的一个特殊子类,它使用其生命周期方法来正确管理与数据库Cursor相关的资源。

数据库中的Cursor有点像迭代器,因为它允许你滚动浏览数据集,而无需担心数据集的确切来源或它所属的数据结构。

我们将使用CursorLoader查询 Android 设备上可用的音乐专辑列表。因为CursorLoader已经实现了正确处理与数据库Cursor相关的所有细节,所以我们不需要对其子类化。我们可以简单地实例化它,传递给它需要的信息,以便它为我们管理Cursor。我们可以在onCreateLoader回调中这样做:

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  String[] columns = new String[] {
    MediaStore.Audio.Albums._ID,
    MediaStore.Audio.Albums.ARTIST,
    MediaStore.Audio.Albums.ALBUM
  };
  return new CursorLoader(this, 
    MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
    columns, // projection
    null, // selection
    null, // selectionArgs
    null // sortOrder
  );
}

正如先前的示例一样,我们将在我们的Activity子类中实现回调。我们将使用GridView来显示我们的专辑列表,因此我们将实现一个Adapter接口来为其单元格提供视图,并将Adapter连接到由我们的Loader创建的Cursor

public class AlbumListActivitySimple extends FragmentActivity
  implements LoaderCallbacks<Cursor> {

  public static final int ALBUM_LIST_LOADER = "album_list".
                                                 hashCode();
  private SimpleCursorAdapter mAdapter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.phone_list_layout);
    GridView grid = (GridView) findViewById(R.id.album_grid);
    mAdapter = new AlbumCursorAdapter(getApplicationContext());
    grid.setAdapter(mAdapter);

    // Prepare the loader. 
    // Either re-connect with an existing one, or start a new one.
    getSupportLoaderManager().
      initLoader(ALBUM_LIST_LOADER,
                 null,
                 AlbumListActivitySimple.this);
  }

  @Override
  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Swap the new cursor in.  (The framework will take
    //  care of closing the old cursor once we return.)
    mAdapter.changeCursor(data);
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
    // This is called when the last Cursor provided to
    // onLoadFinished() above is about to be closed. 
    //  We need to make sure we are no longer using it.
    mAdapter.changeCursor(null);
  }
}

请看前面代码中加粗的部分。我们创建了一个AlbumCursorAdapter,并将其传递给GridView,然后初始化我们的CursorLoader。当加载完成时,我们将加载的 Cursor 传递给 Adapter,任务就完成了。

需要实现的是AlbumCursorAdapter,它将从一个非常简单的类开始。我们的CursorAdapter的工作只是将Cursor中的数据行映射到单个行View中的每个View

Android SDK 提供了非常方便的SimpleCursorAdapter类,它正好符合我们的需求;将数据库数据行映射到专辑项视图中。所以现在我们只需继承它,并通过构造函数参数指定每个单元格的布局填充以及映射到该布局中每个ViewCursor字段:

public static class AlbumCursorAdapter extends SimpleCursorAdapter {
  private static String[] FIELDS = new String[] {
    MediaStore.Audio.Albums.ARTIST,
    MediaStore.Audio.Albums.ALBUM
  };
  private static int[] VIEWS = new int[] {
    R.id.album_artist, R.id.album_name
  };

  public AlbumCursorAdapter(Context context) {
    super(context, R.layout.album_item,
          null, FIELDS, VIEWS, 0);
  }
}

布局文件和源代码可在附带的网站上找到。当你运行此Activity时,你会看到一个网格列表,其中每个单元格包含每张专辑的艺术作品、专辑艺术家和专辑名称。

滚动到列表的中间位置,然后旋转你的设备,你会注意到 Activity 立即重新启动并立即重新显示网格,而不会丢失位置——这是因为CursorLoader在重启中幸存下来,并且仍然持有具有相同行的Cursor对象。

从技术上讲,这一切都非常有趣,但看起来并不怎么样。在下一节中,我们将结合我们的两个Loaders来实现一个可滚动的网格,显示每张专辑的艺术作品。

结合加载器

在前面的章节中,我们开发了一个CursorLoader,用于加载系统上所有可用的音乐专辑列表,以及一个AsynTaskLoader,它在后台执行阻塞 IO 操作。现在我们将使用我们之前的CursorLoaderAsyncTaskLoader结合,从专辑 ID 加载缩略图,以创建一个应用,将设备上所有音乐专辑的艺术作品以可滚动的网格形式平铺,所有加载都在后台进行。

多亏了我们的CursorLoader,我们已经可以访问需要加载的专辑 ID——我们只显示专辑名称和专辑艺术家——所以我们只需将这些 ID 传递给我们的AlbumArtworkLoader,让它为我们异步加载图像。

我们的AlbumArtworkLoader可以在构造函数或之后接收专辑 ID,以加载特定albumId的图像:

public class AlbumArtworkLoader extends AsyncTaskLoader<Bitmap> {

  private int mAlbumId = -1; // The album Identifier
  Bitmap mData = null;

  public AlbumArtworkLoader(Context context, int albumId) {
    super(context);
    this.mAlbumId = albumId;
  }

我们将通过设置一个新的albumId来使AlbumArtworkLoader加载新的图像而不是当前的图像。由于位图已缓存(mData),仅设置新的 ID 是不够的——我们还需要通过使用Loader.onContentChanged来触发重新加载:

  public void setAlbumId(int newAlbumId) {

    if (  isDifferentMedia(newAlbumId) || mData == null ) {

      // Album Id change will force the artwork reload
      this.mAlbumId = newAlbumId;
      onContentChanged();     

    } else if (!isDifferentMedia(newAlbumId) ) {
      // we already have the Bitmap for this album
      deliverResult(mData);
    }
  }

如前所述,onContentChanged是抽象Loader超类的一个方法,如果我们的Loader当前处于启动状态,它将强制执行后台加载。如果我们当前处于停止状态,将设置一个标志,并在下次Loader启动时触发后台加载。无论如何,当后台加载完成后,onLoadFinished将触发并带有新数据。

我们需要实现onStartLoading方法,以正确处理在调用onContentChanged时我们处于stopped状态的情况。让我们回顾一下它曾经的样子:

@Override
protected void onStartLoading() {

  if (mData != null) {
    deliverResult(mData);
  }
  if (takeContentChanged() || mData == null) {
    forceLoad();
  }
}

onStartLoading方法再次立即提供其数据(如果有的话)。

然后它调用 takeContentChanged 来查看我们是否需要丢弃缓存的 Bitmap 并加载一个新的。如果 takeContentChanged 返回 true,我们调用 forceLoad 来触发后台加载和重新发送。

现在,我们可以让我们的 AlbumArtworkLoader 加载和缓存不同的图像,但单个 AlbumArtworkLoader 只能一次加载和缓存一个图像,所以我们需要多个活动实例。

让我们来看看修改 AlbumCursorAdapter 的过程,以便为 GridView 中的每个单元格初始化一个 AlbumCursorAdapter,并使用这些 Loader 异步加载专辑艺术作品并显示它们:

public class AlbumCursorAdapter extends CursorAdapter {

    Context ctx;
    private LayoutInflater inf;
    private LoaderManager mgr;
    private List<Integer> ids;
    private int count;

    public AlbumCursorAdapter(Context ctx, LoaderManager mgr) {
        super(ctx.getApplicationContext(), null, true);
        this.ctx = ctx;
        this.mgr = mgr;
        inf = (LayoutInflater) ctx.
                getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        ids = new ArrayList<Integer>();
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
      ..
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
      ...
    }

我们有两种实现方法——newViewbindViewGridView 将会调用 newView 直到它有足够的 View 对象来填充所有可见的单元格,然后它将回收这些相同的 View 对象,通过将它们传递给 bindView 来为不同的单元格异步加载数据,随着网格的滚动。当一个视图滚动出视野时,它就变得可以重新绑定。

这对我们来说意味着我们有一个方便的方法来初始化我们的 AlbumArtworkLoaders——newView,还有一个方便的方法来重新分配 Loader 以加载一个新的缩略图——bindView

newView 首先为行填充专辑项布局并将其传递给父视图,一个基于适配器类 hashcode() 方法生成的 ID 和基于当前加载器数量的唯一 ID。

之后,唯一的 ID 和 imageView 被传递给一个 ArtworkLoaderCallbacks 类,我们稍后会遇到它。ArtworkLoaderCallbacks 然后用于初始化一个新的 Loader,它共享父 View 的 ID。这样我们就在网格中的每一行初始化了一个新的 Loader

@Override
public View newView(Context context, Cursor cursor,
                    ViewGroup parent) {

  View view = (View) inf.inflate(R.layout.album_item,
                                 parent, false);
  ImageView imageView = (ImageView) view.
                                    findViewById(R.id.album_art);
        ...
  int viewId = AlbumCursorAdapter.class.hashCode() + count++;
  view.setId(viewId);
  mgr.initLoader(viewId, null,
                 new ArtworkLoaderCallbacks(ctx, imageView));
  ids.add(viewId);
  return view;
}

bindView 中,我们正在回收每个现有的 View 以更新显示的图像、专辑名称和专辑艺术家。所以我们首先清除旧的 Bitmap

接下来,我们通过 ID 查找正确的 Loader,从 Cursor 中提取下一个要加载的图像的 ID,并通过将 ID 传递给 AlbumArtworkLoader 的方法——setAlbumId 来加载它:

@Override
public void bindView(View view, Context context, Cursor cursor) {
  ImageView imageView = (ImageView) view.
                           findViewById(R.id.album_art);
  imageView.setImageBitmap(null);

  Loader<?> loader = mgr.getLoader(view.getId());
  AlbumArtworkLoader artworkLoader = (AlbumArtworkLoader) loader;
  int albumId = cursor.getInt(
     cursor.getColumnIndex(MediaStore.Audio.Albums._ID));
  ...
  // Sets the album id bound to this imageView,
  // this could force the loader to retrieve a new image
  artworkLoader.setAlbumId(albumId);

}

我们需要向我们的 Adapter 添加一个额外的方法,以便在我们不再需要它们时清理 AlbumArtworkLoaders。当我们不再需要这些 Loaders 时——例如,当我们的 Activity 正在结束时——我们将自己调用这些方法:

 public void destroyLoaders() {
   for (Integer id : ids) {
     mgr.destroyLoader(id);
   }
 }

那就是我们的完成后的 Adapter。接下来,让我们看看 ArtworkLoaderCallbacks,正如你可能猜到的,它只是 LoaderCallbacks 的一个实现:

public static class ArtworkLoaderCallbacks implements
  LoaderManager.LoaderCallbacks<Bitmap> {

  private Context context;
  private ImageView image;

  public ArtworkLoaderCallbacks(Context context,
                                ImageView image) {
    this.context = context.getApplicationContext();
    this.image = image;
  }

  @Override
  public Loader<Bitmap> onCreateLoader(int i, Bundle bundle) {
    return new AlbumArtworkLoader(context);
  }

  @Override
  public void onLoadFinished(Loader<Bitmap> loader, Bitmap b) {
    image.setImageBitmap(b);
  }

  @Override
  public void onLoaderReset(Loader<Bitmap> loader) {}
}

ArtworkLoaderCallbacks 唯一有趣的事情是创建一个 AlbumArtworkLoader 的实例,并将加载的 Bitmap 设置到其 ImageView 中。

我们的 Activity 几乎没有变化——我们需要在实例化 AlbumCursorAdapter 时传递一个额外的参数,为了避免泄漏它创建的 Loaders,我们需要在 onPauseonStop 中调用 AlbumCursorAdapterdestroyLoaders 方法,如果 Activity 正在结束时:

@Override
protected void onStop() {
  super.onStop();
  if (isFinishing()) {
    // Destroy the main album list Loader
    getSupportLoaderManager().destroyLoader(ALBUM_LIST_LOADER);
    // Destroy album artwork loaders
    mAdapter.destroyLoaders();
  }
}

完整的源代码可在 Packt Publishing 网站上找到。查看完整的源代码,以欣赏实际上有多么简洁,并在设备上运行它,以了解Loaders只需相对较少的努力就能提供多少功能!

Loader的应用

显而易见的用途包括从设备本地的文件或数据库中读取任何类型的数据,或者从 Android 内容提供者中读取,正如我们在本章的示例中所做的那样。

与直接使用AsyncTask相比,Loaders的一个显著优势是它们的生命周期在ActivityFragment生命周期方面非常灵活。我们无需额外努力就能处理配置更改,例如方向变化。

我们甚至可以在一个Activity中开始加载数据,在应用中导航,并在一个完全独立的Activity中收集结果,如果这对我们的应用有意义的话。

在某些方面,这种从Activity生命周期中解耦使得LoaderAsyncTask更适合执行网络传输,如 HTTP 下载;然而,它们需要更多的代码,并且仍然不是完美的选择。

该框架在管理异步数据加载方面非常强大;然而,它不提供显示加载进度的机制,正如我们在AsyncTask框架中所做的那样,也没有错误处理回调函数来管理加载错误或异常。

为了克服这些问题,开发者必须扩展基本的Loader框架类并实现这些模式以匹配他的需求。

概述

Android 中的Loader框架在使后台加载数据变得容易并将数据在准备好时传递到主线程方面做得非常出色。

在本章中,我们学习了所有Loader的基本特征——后台加载、加载数据的缓存以及管理生命周期。

我们详细探讨了AsyncTaskLoader作为执行任意后台加载的手段,以及CursorLoader用于从本地数据库游标异步加载。

我们看到Loader可以让我们摆脱Activity生命周期强加的一些限制,并利用这一点在Activity重启时继续在后台工作。

在下一章中,我们将完全摆脱Activity生命周期的限制,并使用Service执行后台操作,即使我们的应用不再处于前台。

第五章。与服务交互

在前面的章节中,我们关注了基本的、高级的、Android 特定的结构,用于在独立的执行线(后台线程)上异步加载数据;android.os.AsyncTaskandroid.content.Loader

如果我们想要提供一个通用的操作集,这些操作在集中化的单一实体上实现任何类型的业务逻辑,该实体可以被不同的客户端重用,并且其生命周期不绑定到客户端生命周期,那会怎样?在 Android 中,我们指的是任何类型的 UI 实体,如ActivityFragment对象、BroadcastReceiver或任何想要执行业务逻辑的对象。

Android 中这种模式的解决方案以android.app.Service的形式提供。

在 Android 中,Service 编程模式,在企业架构中广为人知,并不一定意味着后台工作,因此为了避免任何类型的 UI 响应性下降,我们应该尽量保持 Service 的主线程执行尽可能简洁。

因此,我们必须使用异步技术来协调主线程和其他线程之间的Service工作,这些线程有助于实现Service目标,以保持响应性在相当不错的水平并提供结果给 Service 请求。

在本章中,我们将涵盖以下主题:

  • 介绍 Service

  • 启动服务

  • 使用 IntentService 构建响应式应用

  • 使用待定意图发布结果

  • 在通知抽屉中报告进度

  • IntentService 的应用

  • 绑定服务

  • 与本地服务通信

  • 使用意图广播结果

  • 服务应用

介绍 Service

如前所述,Android 中的Service是一个没有用户界面、可以用来执行应用在执行期间所需的任何类型业务逻辑的实体。

如果可见应用的基本单元是Activity,那么非可见组件的等效单元是Service。就像活动一样,服务必须在 AndroidManifest 文件中声明,以便系统了解它们并为我们管理它们:

<service android:name=".MyService"/>

Service 具有与 Activity 类似的生命周期回调方法,这些方法始终在应用程序的主线程上调用。以下是用户在通过扩展 Service 基类创建服务时必须定义的最重要回调:

void onCreate();
void onDestroy()
void onStartCommand(Intent intent, int flags, int startId)  
IBinder onBind(Intent intent)   
boolean onUnbind(Intent intent)

onCreate()是在服务创建时调用的生命周期回调,可能用于分配 Service 资源。

onDestroy()是在服务即将被销毁时调用的生命周期回调,可能用于释放 Service 资源。

onStartCommand()是在使用startService()命令显式启动已启动的服务时调用的生命周期回调。

onBind()是在服务绑定到 Service 客户端时调用的生命周期回调,即bindService()

当服务从客户端取消绑定时,会调用onUnbind()回调 - unbindService()

介绍服务

此外,就像Activity一样,Service不会自动涉及单独的后台线程或进程,因此,在Service回调方法中执行密集型或阻塞操作可能会导致令人烦恼的 ANR 对话框。

然而,服务与活动不同,以下列出了这些不同之处:

  • 服务不提供用户界面。

  • 在一个应用程序中可以同时存在多个活动服务。

  • 即使托管它的应用程序不是当前的前台应用程序,服务也可以保持活动状态,这意味着可以有多个应用程序的服务同时处于活动状态。

  • 因为系统知道进程内运行的服务,它可以避免在绝对必要时杀死这些进程,从而允许后台工作继续。服务比非活动或不可见活动具有更高的优先级。

根据客户端如何与之交互,Android 中的服务可以根据以下形式进行分类:

  • 已启动服务:这是一种在系统上的任何对象调用startService()时明确启动的服务,它将一直运行,直到通过调用stopSelf()或通过stopService()显式销毁它而停止。

  • 绑定服务:这是一种在第一个客户端绑定到它时启动的服务,并且它将一直运行,直到所有客户端连接。服务客户端通过调用bindService()附加到服务,当所有客户端取消绑定并调用unbindService()时,服务将被销毁。

  • 混合服务:当系统上的对象调用startService()时启动此服务,在其生命周期内可能连接到它的一些客户端,通过调用bindService()。就像已启动的服务一样,它无限期地运行,直到服务停止,自行停止或被系统杀死。介绍服务

服务也可以根据其边界进行分类,以下列出了这些形式:

  • 本地服务LS):服务与其他 Android 组件在同一个进程中运行,因此可以使用共享内存来在客户端和服务器之间发送 Java 对象。

  • 内部远程服务IRS):服务在单独的进程中运行,但它只能由定义它的应用程序的组件使用。要访问它,需要使用 IPC 技术(信使或 AIDL)与远程进程交互。

  • 全局服务GS):服务在单独的进程中运行,并且可以被其他应用程序访问。例如,使用 IRS,客户端必须使用 IPC 通信技术来访问它。

已启动服务

如前所述,启动的服务是在任何实体调用 Context 方法的 startService() 时启动的,这些实体可以访问上下文对象或本身就是上下文,例如 Activity:

         ComponentName startService(Intent service)

注意

意图(Intent)是一个可以携带数据(动作、类别、额外信息等)的消息对象,你可以用它来请求另一个 Android 组件执行动作。

startService() 函数基本上是使用意图启动一个服务,并返回给用户一个组件名称,该名称可用于验证是否正确解析并调用了服务。

为了简化服务解析,我们传递一个由当前上下文和需要启动的服务类创建的意图:

     startService(new Intent(this,MyStartedService.class));

当系统接收到第一个 startService(intent) 请求时,它会通过调用 onCreate() 来构建服务,并将第一个 startService 和随后的意图转发到 onStartCommand 函数进行处理,按照 startService 调用的顺序:

  int onStartCommand(Intent intent, int flags, int startId)

onStartCommand 应返回一个 int 值,该值定义了系统在杀死服务以释放资源时应用的 Service 重新启动行为。如前所述,系统维护一个按等级排序的 Android 运行实体列表,一旦可用系统资源低,它将首先销毁等级较低的实体以释放资源。

最常见的重新启动 int 值由以下服务静态字段定义:

  • START_STICKY:如果服务进程被系统终止,服务将被重新启动,并且不会将已处理的意图发送到 onStartCommand 函数。当没有待处理的启动意图要发送时,将一个空意图传递给 onStartCommand 函数。如果启动请求在系统杀死服务之前没有返回,则将启动请求再次提交到重新启动的服务,并在 onStartCommand 的第二个参数上传递 START_FLAG_RETRY

  • START_NOT_STICKY:如果服务被系统终止,服务只有在至少有一个待处理的启动请求要发送时才会重新启动。

  • START_REDELIVER_INTENT:如果服务被系统终止,服务将被重新启动,并重新发送最后发送的启动意图和任何挂起的请求。这种服务类似于 START_STICKY,但不是在启动命令中发送一个空意图,而是发送最后成功发送的意图。当启动请求被重新发送时,onStartCommand 的第二个参数会传递 START_FLAG_REDELIVERY 标志。

在主 Thread 上执行的 onStartCommand 是服务的入口点,因此在你需要在 Service 上执行长时间运行的操作时,将操作卸载到后台线程是强制性的,以保持应用程序的响应性在可忍受的水平。

在下一个代码片段中,我们将创建一个基本的Service,将Intent处理卸载到后台线程。SaveMyLocationService服务子类将接收一个字符串形式的地址位置,并在可能占用 CPU 长时间的操作中消耗它。首先,我们将创建一个后台线程,该线程将从队列中检索位置并消耗它们,直到它收到停止信号:

public class SaveMyLocationService extends Service {
  boolean shouldStop = false;
  Queue<String> jobs = new LinkedList<String>();

  Thread thread = new Thread() {
    @Override
    public void run() {
      while (!shouldStop) {
        String location = takeLocation();
        if (location != null) {
          consumeLocation(location);
        }
      }
    }
  };

  @Override
  public void onCreate() {
    super.onCreate();
    thread.start();
}

String takeLocation() {
  String location = null;
  synchronized (jobs) {
    if (jobs.isEmpty()) {
      try {
       jobs.wait();
      } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
       return null;
      }
     }
     location = jobs.poll();
    }
    return location;
  }
void consumeLocation(String location) {...}
}

在前面的代码中,我们基本上构建了异步处理的基础。当在主Thread上调用服务的onCreate()回调时启动的单个线程,将监控作业队列以查找新的位置请求。线程将高效地在后台等待,使用 Java 监视器,直到它通过notify()被通知有新的位置提交。

当我们的后台线程在队列中找到新的位置时,等待在 Java 监视器上的takeLocation()返回,并将新的请求转发到consumeLocation()以执行请求的业务逻辑。作业将按插入顺序顺序处理。

一旦将shouldStop设置为 true,run()函数将返回,线程将被终止。

我们需要在 AndroidManifest 文件中注册服务,使用以下<service>元素:

   <service android:name=".chapter5.SaveMyLocationService"/>

在下一步中,我们将实现onStartCommand,该函数将首先接收来自系统的请求并将其转发到我们的线程以在后台进行处理:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
  super.onStartCommand(intent, flags, startId);
  String location = intent.getStringExtra(LOCATION_KEY);
  synchronized (jobs) {
    jobs.add(location);
    jobs.notify();
  }
  return START_STICKY;
    }

onStartCommand中,我们从系统接收了 intent 对象和 Intent 附加信息中传递的字符串形式的地址。接下来,我们将其推送到我们的作业队列中,该队列用于按顺序存储作业。稍后,我们返回START_STICKY标志,这意味着我们希望在系统关闭我们的应用程序和传递挂起的 intent 后,系统关闭系统以释放资源时重新启动服务。

最后,我们必须实现回调函数以停止我们的后台处理基础设施。当系统强制服务终止或任何组件发送stop命令时,将调用此函数:

@Override
public void onDestroy() { 
  super.onDestroy();
  synchronized (jobs) {
    shouldStop = true;
    jobs.notify();
  }
}

onDestroy()函数中,我们主要要求我们的线程终止,将shouldStop设置为 true,并通过通知线程完成run()函数。如果你在onCreate()函数中分配了对象,则应使用此回调来释放在服务生命周期中创建的任何资源。

现在,我们将创建一个简单的 Activity,该 Activity 能够启动服务并停止服务:

public class SaveMyLocationActivity extends Activity {

  ...

  void onStartServiceClick() {
    Intent intent = new Intent(this, SaveMyLocationService.class);
    intent.putExtra(SaveMyLocationService.LOCATION_KEY,
                    getCurrentLocation());
    startService(intent);
  }

  void onStopServiceClick() {
    Intent intent = new Intent(this,SaveMyLocationService.class);
    stopService(intent);
  }
}

在我们的 Activity 中,我们创建了一个启动按钮,该按钮调用onStartServiceClick(),以及一个停止按钮,该按钮调用onStopServiceClick(),但为了简洁,我们省略了代码。

一旦点击开始按钮,我们的 Activity 将通过调用startService()将一个新的保存位置请求提交给我们的服务,其中当前位置是通过getCurrentLocation()函数检索的。

点击停止按钮将导致stopService(),这会向我们的服务发送停止请求,导致我们的服务调用onDestroy()

必须提到的是,如果服务被停止和重复启动,将会创建一个新的线程来替换旧的线程。如前所述,线程创建是一个昂贵的操作,因此为了减少线程创建的负担,开发者应该让服务尽可能长时间地运行。

我们的自定义服务能够很好地异步处理onStartCommand(),但在下一节中,我们将关注IntentService类,这是 Android SDK 中的一个特殊用途的Service子类,它使得实现任务队列以在单个后台线程上处理工作变得非常容易。

使用 IntentService 构建响应式应用

IntentService类是一个特殊的Service子类,它使用单个HandlerThread实现后台工作队列。当工作被提交给IntentService时,它将在单个HandlerThread上排队等待处理,并在onHandleIntent函数中按提交顺序处理。

abstract void onHandleIntent(Intent intent);

如果用户在队列中的工作完全处理之前退出应用程序,IntentService将继续在后台工作。当IntentService的队列中没有更多工作可处理时,它将自行停止以避免消耗不必要的资源。

注意

如果系统确实需要(为了回收足够的内存来运行当前的前台进程),它仍然可以杀死具有活动IntentService的后台应用程序,但它将首先杀死优先级较低的进程,例如,没有活动服务的其他非前台应用程序。

IntentService类的名称来源于我们通过调用带有 Intent 的startService来提交工作给它的方式:

startService(new Intent(context, MyIntentService.class));

正如我们在先前的示例中所做的那样,我们可以像我们喜欢的那样多次调用startService,这将启动IntentService(如果它尚未运行),或者如果已经有一个正在运行的实例,则简单地将其工作入队。

如果我们想要向Service传递一些数据,我们可以通过提供数据 URI 或通过 Intent 提供额外的数据字段来实现:

Intent intent = new Intent(context, MyIntentService.class);
intent.setData(uri); intent.putExtra("param", "some value");
startService(intent);

我们可以通过扩展android.app.IntentService并实现抽象的onHandleIntent方法来创建IntentService的子类。

我们必须使用一个名称来调用单参数构造函数,为它的后台线程命名(命名线程使得调试和性能分析变得容易得多)。

public class MyIntentService extends IntentService {

  public MyIntentService() {
    super("myIntentService");
  }
  protected void onHandleIntent(Intent intent) {
    // executes on the background HandlerThread.
  }
}

我们需要在 AndroidManifest 文件中注册IntentService,使用以下方式使用<service>元素:

   <service android:name=".chapter5.MyIntentService"/>

如果我们希望我们的IntentService只被我们自己的应用程序的组件使用,我们可以通过一个额外的属性来指定它不是公开的:

   <service android:name=".chapter5.MyIntentService"
            android:exported="false"/>

让我们从实现一个用于从特定手机号码检索收件箱中短信数量的 IntentService 开始:

public class CountMsgsIntentService extends IntentService {

public static final String NUMBER_KEY = "number";

public CountMsgsIntentService() {
    super("CountThread");
  }

@Override
protected void onHandleIntent(Intent intent) {
  String phoneNumber = intent.getStringExtra(NUMBER_KEY);
  Cursor cursor = getMsgsFrom(phoneNumber);
  int numberOfMsgs = cursor.getCount();
    // Return will be adressed later
    ...
  }
  // Retrieve the number of messages in the inbox for a
  // specific number
  private Cursor getMsgsFrom(String phoneNumber) {
    String[] select = {
      Telephony.Sms._ID,
      Telephony.Sms.ADDRESS,
      Telephony.Sms.BODY,
    };
  String whereClause =
    Telephony.Sms.ADDRESS + " = '" + phoneNumber + "'";
    Uri quri = Uri.parse("content://sms/inbox");
    return getContentResolver().query(
        quri,
        select, // Columns to select
        whereClause, // Clause to filter results
        null, // Arguments for the whereClause
        null);
  }
}

一旦在 IntentService 上收到请求,该请求就会被推送到内部 Looper 队列,一旦有机会处理它,IntenService 就会调用 onHandleIntent 方法,该方法带有我们在 startService() 方法中传递的 Intent。

接下来,我们使用接收到的 phoneNumber 查询短信收件箱内容提供者,然后统计检索到的记录数。

注意,我们正在声明一个公共静态常量名称作为参数,只是为了使任何想要调用服务的客户端 Activity 能够轻松地使用正确的名称。

我们现在可以按以下方式调用此 IntentService

void triggerIntentService(String phone) {
    Intent intent = new Intent(this,
                               CountMsgsIntentService.class);
    intent.putExtra(CountMsgsIntentService.NUMBER_KEY, phone);
    startService(intent);
}

上述代码接收 phoneNumber 作为参数,并向 IntentService 提交一个新的启动请求,以便按提交顺序顺序处理。

到目前为止一切顺利,但你可能已经注意到我们没有对检索到的结果做任何事情。在下一节中,我们将探讨一些从服务向活动或片段发送结果的方法。

处理结果

任何 Service(包括 IntentService 的子类)都可以用来启动后台工作,而原始的 FragmentActivity 不期望得到响应。

然而,通常需要返回结果或将后台工作的结果显示给用户。我们为此有几种选择:

  • 从原始 ActivityService 发送 PendingIntent,允许 Service 通过其 onActivityResult 方法回调到 Activity

  • 发布系统通知,让用户知道后台工作已完成,即使应用程序不再处于前台。

  • 使用 Messenger 向原始 Activity 中的 Handler 发送消息。

  • Intent 的形式广播结果,允许任何 FragmentActivity(包括原始调用者)接收后台处理的结果。

我们将在稍后学习 BroadcastReceiver 和使用 Service 的长时间运行任务,但现在我们将使用 PendingIntent 返回结果,并通过系统通知提醒用户。

使用 PendingIntent 发布结果。

当我们调用 IntentService 时,它并没有自动有任何方式来响应调用 Activity;因此,如果 Activity 想要接收结果,它必须为 IntentService 提供一些回复的方式。

毫无疑问,最简单的方法是使用 PendingIntent,这对于任何使用过 startActivityForResultonActivityResult 方法的 Android 开发者来说都很熟悉,因为模式本质上是一样的。

备注

PendingIntent 是你提供给外部应用程序组件(服务、广播接收器或其他应用程序)的令牌,它允许外部实体使用你的应用程序的权限来执行预定义的代码。

首先,我们将在CountMsgsIntentService中添加一些静态成员,以确保我们在它和调用Activity之间使用一致的价值:

    public static final String PENDING_RESULT = "pending_result";
    public static final String RESULT = "result";
    public static final int RESULT_CODE = "countMsgs".hashCode();

我们还需要在我们的Activity中定义一个用于REQUEST_CODE常量的静态成员,我们可以使用它来正确地识别返回给我们的onActivityResult方法的结果:

private static final int REQUEST_CODE = 0;

现在,当我们想要从我们的Activity调用CountMsgsIntentService时,我们将为当前的Activity创建一个PendingIntent,它将作为回调来调用 Activity 的onActivityResult方法。

我们可以使用ActivitycreatePendingResult方法创建一个PendingIntent,它接受三个参数:一个int结果代码,一个用作默认结果的 Intent,以及一个int,它编码了一些用于如何使用PendingIntent的标志(例如,是否可以多次使用):

 PendingIntent pending = createPendingResult(REQUEST_CODE,
                                             new Intent(), 0);

我们通过将PendingIntent添加为我们启动IntentService的 Intent 的额外内容来将其传递给IntentService

private void triggerIntentService(String phone) {
    PendingIntent pending = createPendingResult(
                              REQUEST_CODE, new Intent(), 0);
    Intent intent = new Intent(this,CountMsgsIntentService.class);
    intent.putExtra(CountMsgsIntentService.NUMBER_KEY, phone);
    intent.putExtra(CountMsgsIntentService.PENDING_RESULT, 
                   pending);
    startService(intent);
}

为了处理在调用此PendingIntent时返回的结果,我们需要在Activity中实现onActivityResult,并检查结果代码:

protected void onActivityResult(int req, int res, Intent data) {

    if (req == REQUEST_CODE &&
        res == CountMsgsIntentService.RESULT_CODE) {

     // Retrieve the count from result Intent
      int result = data.getIntExtra(
                  CountMsgsIntentService.RESULT, -1);

     // Update UI View with the result
       TextView msgCountBut = (TextView) findViewById(
                               R.id.msgCountTv);
  msgCountBut.setText(Integer.toString(result));
    }
    super.onActivityResult(req, res, data);
}

IntentService现在可以通过调用适当的请求代码的PendingIntent发送方法之一来回复调用Activity。我们的更新后的onHandleIntent方法如下所示:

@Override
protected void onHandleIntent(Intent intent) {

  String phoneNumber = intent.getStringExtra(NUMBER_KEY);
  Cursor cursor = countMsgsFrom(phoneNumber);
  int numberOfMsgs = cursor.getCount();

  try {
 Intent result = new Intent();
 result.putExtra(RESULT, numberOfMsgs);
 PendingIntent reply = intent.getParcelableExtra(
                           PENDING_RESULT);
    reply.send(this, RESULT_CODE, result);
  } catch (PendingIntent.CanceledException exc) {
 Log.e("CountMsgsIntentService", "reply cancelled", exc);
  }
}

附加的代码创建了一个新的 Intent 对象,并用从游标中检索到的计数结果填充它,然后使用接收到的PendingIntent将结果发送回调用Activity。此外,我们还处理了CanceledException,以防调用Activity决定不再对结果感兴趣并取消了PendingIntent

这就是全部内容——当IntentService完成其工作后,我们的Activity将通过其onActivityResult方法被调用。作为额外的好处,即使Activity已经重新启动,例如由于设备旋转等配置更改,我们也会收到结果。

如果用户在后台工作正在进行时离开了Activity(甚至离开了应用程序),会怎样呢?在下一节中,我们将使用通知来提供反馈,而不会打断用户的当前上下文。

发布结果作为系统通知

系统通知最初在通知区域以图标的形式出现,通常在设备屏幕的顶部。一旦通知,用户就可以打开通知抽屉来查看更多详细信息。

通知是通知用户服务结果或状态更新的理想方式,尤其是当操作可能需要很长时间才能完成,并且用户在此期间可能在做其他事情时。

让我们将消息计数器的结果作为通知发布,消息中包含用户打开通知抽屉时可以阅读的结果。我们将使用支持库来确保广泛的 API 级别兼容性,并在CountMsgsIntentService中添加一个方法,如下所示:

private void notifyUser(String phoneNumber, int msgsCount) {

  String msg = String.format(
    "Found %d from the phone number %s", msgsCount, phoneNumber);

  NotificationCompat.Builder builder =
    new NotificationCompat.Builder(this)
      .setSmallIcon(R.drawable.ic_sms_counter_not)
      .setContentTitle("Inbox Counter")
      .setContentText(msg);

  // Gets an instance of the NotificationManager service
  NotificationManager nm = (NotificationManager) getSystemService(
                             Context.NOTIFICATION_SERVICE);
  // Sets an unique ID for this notification
  nm.notify(phoneNumber.hashCode(), builder.build());
}

每个通知都有一个标识符,我们可以用它来控制是否发布新的通知或重用现有通知。标识符是一个 int,是 notify 方法的第一个参数。由于我们的 countMsgsFrom 值是一个 int,并且我们希望能够发布多个通知,因此使用 phoneNumber 作为我们通知的 ID 是有意义的,这样每个不同的请求都可以产生其自己的单独通知。

要发布包含我们的服务请求结果的提示,我们只需更新 onHandleIntent 以调用 notifyUser 方法:

@Override
protected void onHandleIntent(Intent intent) {

  String phoneNumber = intent.getStringExtra(NUMBER_KEY);
  Cursor cursor = countMsgsFrom(phoneNumber);
  int numberOfMsgs = cursor.getCount();
  notifyUser(phoneNumber,numberOfMsgs);
  ...
}

现在我们已经学习了使用 IntentService 的基础知识,让我们考虑一些实际应用。

IntentService 的应用

IntentService 的理想应用包括几乎所有长时间运行的任务,其中工作并不特别依赖于 FragmentActivity 的行为,尤其是当任务必须完成其处理,无论用户是否退出应用程序时。

然而,IntentService 只适合于只需要一个工作线程来处理工作负载的情况,因为其工作是由一个 HandlerThread 按提交顺序顺序处理的,并且我们不能启动同一 IntentService 子类的多个实例。

一个非常适合使用 IntentService 的用例是单次执行、长时间运行的任务,这些任务可以在后台处理而无需用户干预:

  • 将数据上传到远程服务器

  • 数据库或数据备份

  • 耗时的文件数据处理

  • 与 Web 服务资源(WSDL 或 REST)的通信

  • 定期的时间操作,如闹钟处理、日历事件处理等

IntentService 适合用于上传数据到远程服务器的用例,因为:

  • 上传通常必须完成,即使用户离开了应用程序

  • 一次只上传一个文件通常能更好地利用可用连接,因为带宽往往是不对称的(上传的带宽比下载小得多)

  • 一次只上传一个文件,给我们更好的机会在丢失数据连接之前完成每个单独的上传

让我们看看如何实现一个非常简单的 IntentService,通过 HTTP POST 将图片上传到简单的 Web 服务。

使用 IntentService 进行 HTTP 上传

对于这个例子,我们将创建一个新的 ActivityUploadArtworkActivity,允许用户选择要上传的专辑封面。我们将从 第四章 中创建的 AlbumListActivity 代码开始,探索 Loader

我们的新 UploadArtworkActivity 只需要稍作修改,为图像的 GridView 添加一个 OnItemClickListener 接口,以便点击图像触发其上传。我们可以在 onCreate 中添加监听器,如下所示:

grid.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> parent, View view,
                          int position, long id) {
    Cursor cursor = (Cursor) mAdapter.getItem(position);
    int albumId = cursor.getInt( 
      cursor.getColumnIndex(MediaStore.Audio.Albums._ID));
    Uri sArtworkUri = Uri.parse(
      "content://media/external/audio/albumart");
    Uri albumArtUri = ContentUris.
                        withAppendedId(sArtworkUri, albumId);
    Intent intent = new Intent(UploadArtworkActivity.this,
                               UploadArtworkIntentService.class);
    intent.setData(albumArtUri);
    startService(intent);
  }
});

这看起来像是一段相当密集的代码,但它实际上只是使用被点击的缩略图的当前位置将Cursor移动到其结果集的正确行,提取被点击的相册的 ID,为其艺术作品文件创建一个Uri,然后使用包含该 Uri 的 Intent 启动UploadArtworkIntentService

我们将上传的详细信息提取到一个单独的类中,因此UploadArtworkIntentService本身只是一个相当稀疏的IntentService实现。在onCreate中,我们将设置我们的ImageUploader类的一个实例,该实例将用于处理在此服务生命周期内添加到队列的所有上传:

public void onCreate() {
  super.onCreate();
  mImageUploader = new ImageUploader(getContentResolver());
}

ImageUploader的实现本身并不那么有趣——我们只是使用 Java 的HTTPURLConnection类将图像数据发送到服务器。完整的源代码可在 Packt Publishing 网站上找到,所以我们只列出两个关键方法——上传和泵送——并省略其他维护工作:

public boolean upload(Uri data, ProgressCallack callback) {
  HttpURLConnection conn = null;
  try {
    int len = getContentLength(data);
    URL destination = new URL(UPLOAD_URL);
    conn = (HttpURLConnection) destination.openConnection();
    conn.setRequestMethod("POST");
    ...
    OutputStream out = null;
    try {
      pump(in = mContentResolver.openInputStream(data),
           out = conn.getOutputStream(),
           callback, len);
    } finally {
      if (in != null )
        in.close();
      if (out != null )
        out.close();
      int responseCode = conn.getResponseCode();
      return (( responseCode >= 200) &&
              (responseCode < 400));
    }
  } catch (IOException e) {
    Log.e("Upload Service", "upload failed", e);
    return false;
  } finally {
    conn.disconnect();
  }
}

pump方法只是将 1 KB 的数据块从InputStream复制到OutputStream,将数据泵送到服务器,并调用进度回调函数,如下所示:

private void pump(InputStream in, OutputStream out,
                  ProgressCallack callback, int len)
throws IOException {

  int length, i = 0, size = 1024;
  byte[] buffer = new byte[size]; // 1kb buffer
  while ((length = in.read(buffer)) > -1) {   
    out.write(buffer, 0, length);
    out.flush();
    if (callback != null)
      callback.onProgress(len, ++i * size);
  }
}

每当 1 KB 的数据块被推送到OutputStream时,我们就会调用ProgressCallback方法,我们将在下一节中使用该方法向用户报告进度。

报告进度

对于长时间运行的过程,报告进度非常有用,这样用户就可以安心地知道实际上有事情在进行。

要从IntentService报告进度,我们可以使用发送结果时使用的相同机制——例如,发送包含进度信息的PendingIntents,或者发布带有进度更新的系统通知。

我们还可以使用本章后面将要介绍的其他技术,向已注册的接收器广播意图。

注意

无论我们采取何种方法来报告进度,我们都应该小心不要过于频繁地报告进度,否则我们会在完成工作本身的同时浪费资源更新进度条!

让我们来看一个例子,该例子在抽屉中的通知上显示进度条——这是 Android 开发团队预见到的情况,因此通过NotificationCompat.BuildersetProgress方法使我们能够轻松实现:

Builder setProgress(int max, int progress, boolean indeterminate);

在这里,max 设置我们的工作将完成的靶值,progress 是我们已经到达的位置,而 indeterminate 控制显示哪种类型的进度条。当 indeterminate 为 true 时,通知显示一个进度条,指示有事情在进行,但没有指定操作进行到哪一步,而 false 显示我们需要的那种进度条——显示我们已经完成的工作量以及剩余的工作量。

我们需要计算进度并在适当的时间间隔内发送通知,我们已经通过我们的 ProgressCallback 类实现了这一点。现在我们需要实现 ProgressCallback 并将其连接到 UploadArtworkIntentService

private class ProgressNotificationCallback
  implements ImageUploader.ProgressCallack {
  private NotificationCompat.Builder builder;
  private NotificationManager nm;
  private int id, prev;

  public ProgressNotificationCallback(
    Context ctx, int id, String msg) {
    this.id = id;
    prev = 0;
    builder = new NotificationCompat.Builder(ctx)
      .setSmallIcon(android.R.drawable.stat_sys_upload_done)
      .setContentTitle("Uploading Artwork")
      .setContentText(msg)
      .setProgress(100, 0, false);
    nm = (NotificationManager)
         getSystemService(Context.NOTIFICATION_SERVICE);
    nm.notify(id, builder.build());
  }

  public void onProgress(int max, int progress) {
    int percent = (int) ((100f * progress) / max);
    if (percent > (prev + 5)) {
      builder.setProgress(100, percent, false);
      nm.notify(id, builder.build());
      prev = percent;
    }
  }

  public void onComplete(String msg) {
    builder.setProgress(0, 0, false);
    builder.setContentText(msg);
    nm.notify(id, builder.build());
  }
}

ProgressNotificationCallback 构造函数包含用于发布带有进度条的通知的熟悉代码。

onProgress 方法限制了发送通知的速率,以确保我们只在总数据上传增加额外的 5% 时发布更新,以免系统被通知更新淹没。

onComplete 方法发布一个通知,将整数进度参数设置为零,从而移除进度条。

为了完成代码,我们实现 onHandleIntent 以显示通知抽屉并传递上传结果:

@Override
protected void onHandleIntent(Intent intent) {
  Uri data = intent.getData();

  // Unique id per upload, so each has its own notification
  int id = Integer.parseInt(data.getLastPathSegment());
  String msg = String.format("Uploading %s.jpg", id);

  ProgressNotificationCallback progress =
    new ProgressNotificationCallback(this, id, msg);

  // On Upload sucess
  if (mImageUploader.upload(data, progress)) {
    progress.onComplete(
      String.format("Upload finished for %s.jpg", id)); 
  // On Upload Failure
  } else {
    progress.onComplete(
      String.format("Upload failed %s.jpg", id));
  }
}

点击艺术品图像开始上传,你会看到一个通知出现。滑动打开通知抽屉,并观察进度条随着你的图像上传而跳动。

我们已经完成了启动服务,现在应该转向另一种类型的服务,即绑定服务。

绑定服务

绑定服务是一个定义客户端接口的 Android 服务,它允许通过调用 bindService() 并创建与每个订单之间的联系,从而方便与请求-响应模型进行交互的多个实体绑定它:

当第一个客户端尝试连接到服务时,将创建 Service 实例,并且它将一直存活,直到最后一个客户端使用 unbindService() 函数断开连接。

为了在客户端和服务器之间建立连接,服务必须实现 onBind() 函数并返回一个实现轻量级远程过程调用机制的 IBinder 对象,以执行进程内或跨进程调用:

IBinder onBind(Intent intent)

当所有客户端从服务断开连接并调用 unbindService() 时,将调用服务的 onUnbind() 成员方法:

boolean onUnbind (Intent intent)

绑定服务可能位于同一进程(LS)、属于应用程序的不同进程(LIS)或另一个应用程序进程(GS)中,因此与该服务通信的技术和返回的 IBinder 类型完全取决于服务进程的位置,如前所述。

在下一节中,我们将解释如何与本地服务进行交互和绑定,以在服务上启动异步操作。

使用 AIDL 或使用信使进行远程绑定是其他在需要进程间通信的高级用例中使用的技巧,尽管在这本书中我们不会涉及它。

为了顺利启动,我们首先将介绍本地 Service 绑定:

与本地服务通信

本地绑定的服务是最常见的绑定 service 类型,鉴于服务器和客户端在同一个进程中运行,因此不需要使用 进程间通信IPC) 技术在它们之间发送请求和接收响应。此外,服务客户端和服务器在进程内部共享相同的地址内存空间,这使得使用 Java 对象交换请求和响应变得相当容易。

由于我们处于同一个进程中,onBind() Service 方法返回的 Binder 对象可能定义了一个方法来返回 Service 类实例对象。这样,我们可以使用公共的 Service 类函数以与调用常规对象方法相同的方式向同一 Service 提交新请求。

让我们用一个例子来演示这一点,创建一个绑定的服务,该服务可以从我们在 UI EditText中输入的字符串生成 SHA1 加密摘要。

主要的,我们将从实现自己的 Binder 开始:

public class Sha1HashService extends Service {

  // Instance Binder given to clients
  private final IBinder mBinder = new LocalBinder();

  public class LocalBinder extends Binder {
    Sha1HashService getService() {
      // Return this instance of LocalService
      // so clients can call public methods
      return Sha1HashService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
}

我们的绑定器 LocalBinderBinder 类扩展,并提供一个 getService() 方法来检索我们的 Service 实例。然后,当任何客户端连接到我们的 Service 时,onBind() 函数将返回我们的 LocalBinder 实例对象。

想要直接与这个 Service 交互的 ActivityFragment 首先需要使用 bindService 方法将其绑定,然后提供 ServiceConnection 来处理 onServiceConnected()/onServiceDisconnected() 回调。

ServiceConnection 实现简单地将接收到的 IBinder 强制转换为 Service 定义的具体系列类,获取 Service 的引用,并将其记录在 Activity 的成员变量中:

public class Sha1Activity extends Activity {

  Sha1HashService mService;
  boolean mBound = false;

  // Defines callbacks for service binding,
  // passed to bindService()
  private ServiceConnection mConnection = new ServiceConnection()   
  {
    @Override
    public void onServiceConnected(ComponentName name,
                                   IBinder service) {

      // We've bound to LocalService,
      // cast the IBinder and get LocalService instance
      Sha1HashService.LocalBinder binder =
        (Sha1HashService.LocalBinder) service;
      mService = binder.getService();
      mBound = true;

      // After this the Activity can invoke the Service methods
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
      mBound = false;
      mService = null;
    }
  };
}

当我们意外地与服务失去连接,由于服务崩溃或 Android 系统中的意外错误时,onServiceDisconnected 被调用以通知客户端认为与服务的连接已丢失。

我们可以在 ActivityonStart()onStop() 生命周期方法中绑定和解绑 Activity,因为我们只需要在 Activity 在屏幕上可见时与服务交互。我们应该尽量避免在 onResume()onPause() Activity 回调中进行绑定和解绑,以减少应用程序生命周期中的连接和断开连接转换次数:

@Override
protected void onStart() {
    super.onStart();
    // Bind to LocalService
    Intent intent = new Intent(this, Sha1HashService.class);
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}

@Override
protected void onStop() {
    super.onStop();
    // Unbind from the service
    if (mBound) {
        unbindService(mConnection);
        mBound = false;
    }
}

一旦 Activity 开始,我们调用 Context.bindService(),传递一个 Intent,该 Intent 明确定义了我们想要绑定的 Service 类,我们的 ServiceConnection 实例,以及可选的标志 Context.BIND_AUTO_CREATE,这意味着只要这个绑定存在,系统就会保持 Service 运行。

在混合服务(绑定/启动)中,在我们绑定到服务之后,我们可以通过调用 startService(Intent) 来访问服务,并在 onStartCommand(Intent, int, int) 中处理服务调用。

这很棒——一旦建立了绑定,我们就有了对Service实例的直接引用,可以调用其方法!然而,我们还没有在我们的Service中实现任何方法,所以目前它是无用的。

让我们在Sha1HashService上创建一个方法,在后台计算摘要并将结果返回给Activity

首先,为了在后台执行此任务,我们需要设置执行引擎,为此我们将基于java.util.concurrent中提供的ThreadPool类设置自己的Executor。执行器将支持从两个到四个并发线程的并发性,以及最多 32 个排队任务:

public class Sha1HashService extends Service {

  private static final int CORE_POOL_SIZE = 2;
  private static final int MAXIMUM_POOL_SIZE = 4;
  private static final int MAX_QUEUE_SIZE = 32;
  private static final BlockingQueue<Runnable> sPoolWorkQueue =
    new LinkedBlockingQueue<Runnable>(MAX_QUEUE_SIZE);

  private ThreadPoolExecutor mExecutor;

  // Factory to set the Thread Names
  private static final ThreadFactory sThreadFactory =
  new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);
    public Thread newThread(Runnable r) {
      Thread t = new Thread(r, "SHA1HashService #" +
                                mCount.getAndIncrement());
      t.setPriority(Thread.MIN_PRIORITY);
      return t;
    }
  };

  @Override
  public void onCreate() {
    super.onCreate();
    mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                       MAXIMUM_POOL_SIZE, 1,
                                       TimeUnit.SECONDS, 
                                       sPoolWorkQueue,
                                       sThreadFactory);
    mExecutor.prestartAllCoreThreads();
  }

当服务创建后,在第一次绑定之后,立即启动ThreadPool,并使用prestartAllCoreThreads启动核心线程(2 个),以便在服务中请求到达时立即处理。如果客户端以这样的速度提交请求,以至于核心线程无法处理,线程池将增加池中的工作线程数量,直到达到四个线程。

现在我们已经有了执行器,我们将创建一个公共方法来接收对摘要String的请求:

void getSha1Digest(final String text) {

  Runnable runnable = new Runnable() {
    @Override
    public void run() {
     try {
        // Execute the Long Running Computation
        final String digest = SHA1(text);
      } catch (Exception e) {
        Log.e("Sha1HashService", "Hash failed for "+ text, e);
      }
    }
  };
  // Submit the Runnable on the ThreadPool
  mExecutor.execute(runnable);
}

private String SHA1(String text) throws Exception {
  MessageDigest md = MessageDigest.getInstance("SHA-1");
  md.update(text.getBytes("iso-8859-1"), 0, text.length());
  byte[] sha1hash = md.digest();
  return convertToHex(sha1hash);
}
private String convertToHex(byte[] data) {
   ...
}

由于Sha1ActivitySha1HashService有直接的对象引用,我们现在可以直接调用其getSha1Digest方法——当然要确保首先绑定Service

// Invoke the Sha1Hash Service to calculate the digest
//  when the hash button is pressed
queryButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    EditText et = (EditText)findViewById(R.id.text);
    if (mService != null) {
      mService.getSha1Digest(et.getText().toString());
    }
  }
});

从我们的EditText视图中检索到的文本,我们调用我们的ServicegetSha1Digest来计算输入文本的摘要。这是一种非常方便且高效地向Service提交工作的方式——不需要在 Intent 中打包请求,因此不需要额外的对象创建或通信开销。

由于getSha1Digest是异步的,我们不能直接从方法调用中返回结果,并且Sha1HashService本身没有用户界面,所以我们如何向用户展示结果?

一种可能性是将回调传递给Sha1HashService,这样我们就可以在后台工作完成后调用我们的Activity的方法。让我们为活动定义一个通用的回调接口:

public interface ResultCallback<T> {
    void onResult(T data);
}

存在一个严重风险,即通过传递ActivityService,我们将暴露自己于内存泄漏。ServiceActivity的生命周期不一致,因此ServiceActivity的强引用可能会阻止它及时被垃圾回收。

防止此类内存泄漏最简单的方法是确保Sha1HashService只保留对调用Activity的弱引用,这样当其生命周期结束时,Activity可以被垃圾回收,即使服务中正在进行计算。

真的非常重要,每次我们在ResultCallback.onResult期间更新 UI 时,我们必须在 UI 线程中执行;因此,创建一个带有结果的Runnable对象并将其发布在主Looper上是非常必要的。

修改后的 Sha1HashService 在以下代码中展示:

private void postResultOnUI(final String result,
  final WeakReference<ResultCallback<String>> refCallback) {

  // Retrieve the main Thread Looper
  Looper mainLooper = Looper.getMainLooper();
  final Handler handler = new Handler(mainLooper);
  handler.post(new Runnable() {
    @Override
    public void run() {
      if ( refCallback.get() != null ) {
        refCallback.get().onResult(result);
      }
    }
  });
}

public void getSha1Digest(final String text, 
                          ResultCallback<String> callback) {

  final WeakReference<ResultCallback<String>> ref =
    new WeakReference<ResultCallback<String>>(callback);

  Runnable runnable = new Runnable() {
    @Override
    public void run() {
      try {
        // Execute the Long Running Computation
        final String digest = SHA1(text);     
        // Execute the Runnable on UI Thread
        postResultOnUI(digest, ref);
      } catch (Exception e) {
        Log.e("Sha1HashService", "Hash failed", e);
      }
    }
  };
  // Submit the Runnable on the ThreadPool
  mExecutor.execute(runnable);
}

我们使用 postResultOnUI 在主线程上调用回调,这样 Sha1Activity 就可以直接在回调方法中与用户界面交互。我们可以将回调实现为 Sha1Activity 的一个方法:

public class Sha1Activity extends Activity
  implements ResultCallback<String> {

   @Override
    public void onResult(String data) {
        // Updates the result view with the digest string
        TextView et = (TextView)findViewById(R.id.hashResult);
        et.setText(data);
    }
   }

现在,我们可以直接调用 Sha1HashService 中的方法,并通过传递 Activity 本身作为回调,通过 Sha1Activity 的回调方法返回结果:

if ( mService != null ) {
  mService.getSha1Digest(et.getText().toString(),
                         Sha1Activity.this);
}

我们的服务使用它们的本地 ThreadPool 执行者以异步方式处理请求,尽管我们可能已经使用了 AsyncTask 的公共静态执行者:SERIAL_EXECUTOR 以序列化方式执行我们的摘要计算,或 THREAD_POOL_EXECUTOR 以并发和独立地计算摘要:

void getSha1Digest(final String text,
                   ResultCallback<String> callback) {

  AsyncTask.SERIAL_EXECUTOR.execute(runnable);
  // or
  AsyncTask.THREAD_POOL_EXECUTOR.execute(runnable);
}

注意,AsyncTask 执行者是系统共享资源,是一个共享的线程组,由系统中的所有 AsyncTasks 使用;因此,当所有执行者线程都忙于工作时,我们的处理可能会延迟。在大多数用例中,没有必要创建我们自己的自定义工作线程组,而应该使用 AsyncTask 执行者。

Sha1ActivitySha1HashService 之间的这种直接通信非常高效且易于处理。然而,也存在一个缺点:如果 Activity 由于配置更改(如设备旋转)而重新启动,回调的 WeakReference 将被垃圾回收,Sha1HashService 就无法发送结果。

在下一节中,我们将探讨一种机制,即使配置更改后,也能将结果发送回 Activity 或应用程序的其他部分——广播 Intent。

使用意图广播结果

广播 Intent 是将结果发送给任何注册接收它们的人的一种方式。如果我们选择的话,这甚至可以包括在单独进程中的其他应用程序;但如果 ActivityService 是同一进程的一部分,广播最好使用本地广播来完成,因为这更高效且更安全:

使用意图广播结果

我们可以通过添加几行额外的代码来更新 Sha1HashService,使其能够广播其结果。首先,让我们定义两个常量,以便轻松注册广播接收器并从广播 Intent 对象中提取结果:

    public static final String DIGEST_BROADCAST =
            "asynchronousandroid.chapter5.DIGEST_BROADCAST";
    public static final String RESULT = "digest";

现在,我们可以实现一个方法,使用 LocalBroadcastManager 发送包含计算结果的 Intent 对象,来完成大部分工作。我们在这里使用支持库类 LocalBroadcastManager 以提高效率和安全性——本地发送的广播不会产生进程间通信的开销,并且不能泄露到我们的应用程序之外:

    private void broadcastResult(String digest) {
        Intent intent = new Intent(DIGEST_BROADCAST);
        intent.putExtra(RESULT, digest);
        LocalBroadcastManager.getInstance(this).
          sendBroadcast(intent);
    }

sendBroadcast 方法是异步的,将立即返回,而无需等待消息被广播并由接收者处理。最后,我们从 getSha1Digest 中调用我们新的 broadcastResult 方法:

void getSha1Digest(final String text) {
  Runnable runnable = new Runnable() {
    @Override
    public void run() {
      try {
        // Execute the Long Running Computation
        final String digest = SHA1(text);
        // Broadcast Result to Subscribers
        broadcastResult(digest);       
  ...
}

太好了!我们正在广播后台计算的成果。现在我们需要在 Sha1Activity 中注册一个接收器来处理结果。以下是我们可能定义的 BroadcastReceiver 子类:

private static class DigestReceiver extends BroadcastReceiver {

  private TextView view;

  @Override
  public void onReceive(Context context, Intent intent) {
    if (view != null) {
      String result = intent.getStringExtra(
                        Sha1HashService.RESULT);
      view.setText(result);
    } else {
      Log.i("Sha1HashService", " ignoring - we're detached");
    }
  }

  public void attach(TextView view) {
    this.view = view;
  }
  public void detach() {
    this.view = null;
  }
};

这个 DigestReceiver 实现相当简单——它只是从接收到的 Intent 中提取并显示结果——基本上完成了我们在上一节中使用的 Handler 的角色。

我们只想在这个 BroadcastReceiver 监听结果,当我们的 Activity 在堆栈顶部并且可见时,因此我们将在 onStart()onStop() 生命周期方法中注册和注销它。与之前使用的 Handler 一样,我们还将应用 attach/detach 模式以确保不会泄漏 View 对象:

@Override
protected void onStart() {
  super.onStart();
     ...
  mReceiver.attach((TextView) findViewById(R.id.hashResult));
  IntentFilter filter =
    new IntentFilter(Sha1HashService.DIGEST_BROADCAST);
  LocalBroadcastManager.getInstance(this).
  registerReceiver(mReceiver, filter);
}

@Override
protected void onStop() {
  ...
  LocalBroadcastManager.getInstance(this).
  unregisterReceiver(mReceiver);
  mReceiver.detach();
}

当然,如果用户移动到应用程序的另一个部分而没有注册 BroadcastReceiver,或者如果我们完全退出应用程序,他们将看不到计算的结果。

如果我们的服务能够检测到未处理的广播,我们可以修改它以通过系统通知来提醒用户。我们将在下一节中看到如何做到这一点。

检测未处理的广播

在前面的章节中,我们使用系统通知将结果发布到通知抽屉——当用户在后台工作完成之前导航离开我们的应用程序时,这是一个很好的解决方案。然而,我们不想在应用程序仍在前台并且可以直接显示结果时通过发布通知来打扰用户。

理想情况下,如果应用程序仍在前台,我们将显示结果,否则发送通知。如果我们正在广播结果,Service 需要知道是否有人处理了广播,如果没有,则发送通知。

做这件事的一种方法是通过使用 sendBroadcastSync 同步广播方法并利用我们正在广播的 Intent 对象是可变的事实(任何接收器都可以修改它)。首先,我们将在 Sha1HashService 中添加一个额外的常量:

public static final String HANDLED = "intent_handled";

接下来,修改 broadcastResult 以使用同步广播方法并返回 Intent 中布尔额外属性的值;从 Intent 中获取 HANDLED

void broadcastResult(final String text) { 
  Intent intent = new Intent(DIGEST_BROADCAST);
  intent.putExtra(RESULT, digest);              
  // Synchronous Broadcast
 LocalBroadcastManager.getInstance(Sha1HashService.this).
 sendBroadcastSync(intent);
  boolean handled = intent.getBooleanExtra(HANDLED, false);
}

因为 sendBroadcastSync 是同步的,所以所有注册的 BroadcastReceivers 都会在 sendBroadcastSync 返回之前处理广播。这意味着如果任何接收器将布尔额外属性 HANDLED 设置为 true,则 broadcastResult 将返回 true

在我们的 BroadcastReceiver 中,我们将通过添加一个布尔属性来更新 Intent 对象,以指示我们已经处理了它:

@Override
public void onReceive(Context context, Intent intent) {
  if (view != null) {
    String result = intent.getStringExtra(
                      Sha1HashBroadCastUnhService.RESULT);
    intent.putExtra(Sha1HashBroadCastUnhService.HANDLED, true);
    view.setText(result);
  } else {
    Log.i("Sha1HashService", " ignoring - we're detached");
  }
}

现在,如果 Sha1Activity 仍在运行,它的 BroadcastReceiver 已注册并接收 Intent 对象,并将额外布尔属性 HANDLED 设置为 true

然而,如果 Sha1Activity 已经完成,BroadcastReceiver 将不再注册,Sha1HashService 将从其 broadcastResult 方法返回 false

注意

最后一个复杂问题是:与始终在主线程上调用BroadcastReceiverssendBroadcast不同,sendBroadcastSync使用调用它的线程。

我们的BroadcastReceiver直接与用户界面交互,因此我们必须在主线程上调用它。为了在主线程上同步广播 intent,我们创建一个匿名 Runnable 来执行广播:

private void broadcastResult(final String text,
                             final String digest) {

  Looper mainLooper = Looper.getMainLooper();
  Handler handler = new Handler(mainLooper);
  handler.post(new Runnable() {
    @Override
    public void run() {
      Intent intent = new Intent(DIGEST_BROADCAST);
      intent.putExtra(RESULT, digest);
      LocalBroadcastManager.getInstance(Sha1HashService.this).
      sendBroadcastSync(intent);
      boolean handled = intent.getBooleanExtra(HANDLED,
                                               false);
      if (!handled) {
        notifyUser(text, digest);
      }
    }
  });
}

现在我们已经有了广播功能,我们可以在getSha1Digest中调用它,当 intent 没有被Receiver处理时,生成 Android 通知:

void getSha1Digest(final String text) {
  ...
  final String digest = SHA1(text);
  // Execute the Runnable on UI Thread
  broadcastResult(text, digest);
  ...
}

这正是我们想要的——如果我们的BroadcastReceiver处理了消息,我们不会发布通知;否则,我们将这样做,以确保用户得到他们的结果。

到目前为止,我们一直绑定在同一个进程内运行的 Service,客户端与 Service 共享内存地址空间。在下一节中,我们将详细介绍如何使用 Android IPC 特定技术来与在远程进程中运行的 Service 进行交互。

Service 的应用

通过一点工作,Services为我们提供了执行长时间运行的后台任务的方法,并使我们摆脱了Activity生命周期的束缚。与IntentService不同,直接子类化Service也使我们能够控制并发级别。

能够运行我们需要的任何数量的任务,并且可以花费必要的时间来完成这些任务,这打开了一个全新的可能性世界。

我们对如何以及何时使用Services的唯一真正限制来自于需要将结果传达给用户界面组件,如FragmentActivity,以及这所涉及到的复杂性。

Services的理想用例通常具有以下特征:

  • 长时间运行(几百毫秒以上):

  • 不特定于单个 Activity 或 Fragment 类

  • 即使用户离开了应用程序也必须完成

  • 完成不需要用户干预

  • 需要在不同调用之间保持状态的操作

  • 需要的并发级别比IntentService提供的多,或者需要控制并发级别

有许多应用程序表现出这些特征,但最突出的例子当然是处理来自网络服务的并发下载。

为了充分利用可用的下载带宽并限制网络延迟的影响,我们希望能够同时运行多个下载(但不要太多)。我们也不想因为未能完全下载文件而需要稍后重新下载,从而浪费不必要的带宽。因此,理想情况下,一旦开始下载,就应该运行到完成,即使用户离开了应用程序。

摘要

在本章中,我们探讨了非常强大的Service组件,将其用于执行带有或没有可配置并发级别的长时间运行的后台任务。

我们探讨了极其有用的IntentService——这是一个理想的构造,用于在主线程之外执行长时间运行的后台任务,其生命周期远远超出启动它的Activity,甚至在应用不再处于前台时继续执行有用的工作。

我们学习了如何使用参数化Intent将工作发送到IntentService,如何通过实现onHandleIntent在后台处理这项工作,以及如何使用PendingIntent将结果发送回原始的Activity

对于应用不再处于前台或操作特别耗时的情况,我们展示了如何向通知抽屉发送通知,包括进度更新。

我们还看到了广泛的可用于将结果发送回用户的通信机制:直接调用本地Service方法;使用BroadcastReceiver向已注册方广播结果;如果用户已经离开应用,则引发系统通知。

在下一章中,我们将向我们的武器库中添加一项新能力:通过使用AlarmManager安排闹钟,在特定时间运行后台任务——即使设备处于睡眠状态。

第六章. 使用 AlarmManager 安排工作

在整本书中,我们一直将保持前台应用程序的响应性作为我们的主要关注点,我们已经探索了许多将工作从主线程移开并在后台运行的方法。

在我们迄今为止的所有讨论中,我们都希望尽快完成工作,因此尽管我们将它移动到了后台线程,但我们仍然与正在进行的线程操作并发执行工作,例如更新用户界面和响应用户交互。

在本章中,我们将学习如何使用AlarmManager将工作推迟到未来某个时间运行,无需用户干预,甚至在必要时从空闲状态唤醒设备。同时,我们将向您介绍 Android Marshmallow 6 引入的一些节能特性,并解释如何使您的应用程序适应这一新范式。

在本章中,我们将涵盖以下主题:

  • 使用 AlarmManager 安排闹钟

  • 取消闹钟

  • 安排重复闹钟

  • 在 Doze 模式下安排闹钟

  • 设置闹钟

  • 调试 AlarmManager 闹钟

  • 使用 Activity 处理闹钟

  • 使用 BroadcastReceivers 处理闹钟

  • 使用 WakeLock 保持唤醒状态

  • 系统启动时恢复闹钟

  • AlarmManager 的应用

介绍 AlarmManager

在第二章中,使用 Looper、Handler 和 HandlerThread 执行工作,我们学习了如何使用postDelayedpostAtTimesendMessageDelayedsendMessageAtTimeHandlerThread上安排工作。这些机制在我们应用程序在前台运行时对短期调度工作来说是不错的。

然而,如果我们想要在遥远的未来某个时间点安排一个操作运行,我们可能会遇到问题。首先,我们的应用程序可能在到达那个时间之前被终止,从而消除了 Handler 运行这些计划操作的机会。其次,设备可能处于睡眠状态,并且当其 CPU 关闭时,它无法运行我们计划的任务。

注意

解决这个问题的方法是使用一种替代的调度方法,它被设计用来克服这些问题:AlarmManager

android.app.AlarmManager是一个自 Android SDK 第一版以来就存在的类,它提供了一个高级 API,可以在用户定义的特定时间或时间窗口将来触发 Intent。这些计划由 Android 系统管理,考虑到设备的电源周期和状态,以保持低能耗。

此外,AlarmManager是一个提供比 Handler 更强大调度能力的系统服务。作为一个系统服务,AlarmManager 不能被终止,并且在某些条件下,它可以从睡眠状态唤醒设备以发送计划好的闹钟。

android.app.AlarmManager的主要特性如下:

  • 从空闲状态唤醒设备的能力:用户能够控制系统应该如何处理在节能模式下设置的闹钟

  • 取消闹钟:一种基于 Intent 比较取消先前创建的闹钟的机制

  • 更新闹钟:一种更新现有预定闹钟的机制

  • 精确和不精确闹钟:一个能够控制我们调度精确度的 API

  • 由 Android 系统管理的调度:即使您的应用程序没有运行,闹钟也会触发,并且不会消耗任何应用程序资源来管理计时器

使用 AlarmManager 安排闹钟

正如我们之前所说的,所有的闹钟操作都是通过单例对象AlarmManager管理的,这是一个 Android 全局系统服务,任何可以访问Context实例的类都可以获取它。例如,在一个Activity中,我们可以通过以下代码从任何成员方法中获取AlarmManager

AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);

一旦我们有了AlarmManager的引用,我们就可以在所选时间安排一个闹钟,将PendingIntent对象传递给ServiceActivityBroadcastReceiver。最简单的方法是使用set方法:

void set(int type, long triggerAtMillis, PendingIntent operation)

当我们设置闹钟时,我们必须也指定一个type标志——set方法的第一个参数。type标志设置闹钟应该触发的条件以及我们为计划使用哪个时钟。

有两种条件和两种时钟,导致四种可能的type设置。

第一个条件指定当设备在预定闹钟时间处于睡眠状态时,设备是否会唤醒——闹钟是否为wakeup闹钟。

时钟为我们设置的计划提供了一个参考时间,定义了当我们设置triggerAtMillis的值时我们确切的意思。我们可以基于以下时间参考来制定计划:

  • 已过时间系统时钟——android.os.SystemClock——测量自设备启动以来经过的毫秒数,包括任何在深度睡眠中度过的时间。根据系统时钟,当前时间可以通过以下代码找到:

    SystemClock.elapsedRealtime()
    
  • 实时时钟(Unix 时间)- 以毫秒为单位测量自 Unix 纪元以来的时间。根据实时时钟,当前时间可以通过以下方式找到:

    System.currentTimeMillis()
    

注意

在 Java 中,System.currentTimeMillis()返回自 1970 年 1 月 1 日午夜协调世界时(UTC)以来的毫秒数——一个被称为 Unix 纪元的时间点。

UTC 是国际上公认的格林威治标准时间GMT)的继任者,并构成了表达国际时区的基础,这些时区通常定义为相对于 UTC 的正数或负数偏移。

给定这两种条件和两种时钟,这些是我们设置闹钟时可以使用的四种可能的type值:

  • android.app.AlarmManager.ELAPSED_REALTIME:这将在系统时钟相对于的相对时间安排闹钟。如果设备在预定时间处于睡眠状态,它将不会立即交付;相反,闹钟将在设备下一次唤醒时交付。

  • android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP:这将在系统时钟相对于的相对时间安排闹钟。如果设备处于睡眠状态,它将被唤醒以在预定时间交付闹钟。

  • android.app.AlarmManager.RTC:这将在 UTC 相对于 Unix 纪元安排闹钟。如果设备在预定时间处于睡眠状态,闹钟将在设备下一次唤醒时交付。

  • android.app.AlarmManager.RTC_WAKEUP:这将在 Unix 纪元相对于的相对时间安排闹钟。如果设备在预定时间处于睡眠状态,它将被唤醒,并且闹钟将在预定时间交付。

我们将开始设置一个特定时间的闹钟,在初始启动后的 24 小时后响起。我们将使用java.lang.concurrent包中的TimeUnit类来计算毫秒时间。为了设置之前的闹钟,我们需要计算 24 小时中的毫秒数,如下面的代码所示:

long delay = TimeUnit.HOURS.toMillis(24L);
am.set(AlarmManager.ELAPSED_REALTIME, delay, pending);

我们可以使用系统时间设置一个五分钟后的闹钟,通过将五分钟加到当前时间。使用系统时钟,它看起来像这样:

long delay = TimeUnit.MINUTES.toMillis(5L);
long time = System.currentTimeMillis() + delay;
am.set(AlarmManager.RTC, time, pending);

要设置一个今天晚上 9:00 的闹钟(或者如果今天已经过了 9:00,则是明天),我们可以使用Calendar类进行一些时间计算:

Calendar calendar = Calendar.getInstance();
// Tomorrow at 9 if already passed 9pm today
if (calendar.get(Calendar.HOUR_OF_DAY) >= 21) {
    calendar.add(Calendar.DATE, 1);
}
calendar.set(Calendar.HOUR_OF_DAY, 21);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
am.set(AlarmManager.RTC, calendar.getTimeInMillis(), pending);

到目前为止的任何示例都不会在闹钟时间设备处于睡眠状态时唤醒设备。为了做到这一点,我们需要使用其中一个WAKEUP闹钟条件,例如:

am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, delay, pending);
am.set(AlarmManager.RTC_WAKEUP, time, pending);

也很重要的是要理解,当闹钟时间在过去的任何时候,闹钟将在我们调用AlarmManager设置闹钟功能后立即响起。

在最近的 Android 版本中设置闹钟

如果我们的应用程序针对的 API 级别低于 19(KitKat),计划中的闹钟将在闹钟时间准时运行。对于针对 KitKat 或更高版本的应用程序,计划被认为是近似的,系统可能会重新排序或分组闹钟以最小化唤醒次数并节省电池。

从 API 级别 23 开始,Android 开发团队又前进了一步,在 Android 系统中引入了 Doze 模式,以减少设备从电源适配器断开、静止不动且用户长时间未使用时的电池消耗。

Doze 系统将尝试减少设备的唤醒频率,推迟后台任务、网络更新、同步以及我们宝贵的闹钟,直到设备退出 Doze 模式或定期维护窗口运行以执行挂起的任务、某些闹钟或与网络的同步。维护窗口完成后,如果在此期间未使用设备,设备将再次进入 Doze 模式:

在最近的 Android 版本中设置闹钟

图 6.1:Doze 模式时间线

Doze 模式可能会影响您的应用程序,并将您的警报推迟到维护窗口出现,除非您使用 setAndAllowWhileIdle()setExactAndAllowWhileIdle() 方法来允许在深度空闲状态下执行警报。

此外,在长期不活跃的情况下,Doze 模式维护窗口运行的次数将更少,因此这种新机制对我们调度的影响会增加,从而导致警报时间的不确定性增加。

在 Doze 模式期间,应用程序也不允许访问网络,WakeLocks 被忽略,并且不会执行 Wi-Fi 扫描。

如果我们需要精确调度,并且您针对的是 Marshmallow 或更高版本,我们应该使用在 API 级别 23 中引入的新的 setExactAndAllowWhileIdle() 方法:

am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);

注意

Android 系统有保护措施,防止频繁触发精确警报的滥用。AlarmManager 只唤醒设备并每分钟调度一个警报,在低功耗模式下,每 15 分钟可能只有一次。

如果您的应用程序针对的版本在 KitKat(API Level 19)和 Marshmallow(API Level 23)之间,setExact 方法就足够用于时间精度:

am.setExact(AlarmManager.RTC_WAKEUP, time, pending);

但在我们尝试调用它之前,我们需要检查这些方法是否存在;否则,我们的应用程序在早期 API 级别运行时将会崩溃。让我们绘制我们的新精确警报代码:

if (Build.VERSION.SDK_INT >= 23) {
  // Wakes up the device in Doze Mode
  am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);
} else if (Build.VERSION.SDK_INT >= 19) {
  // Wakes up the device in Idle Mode
  am.setExact(AlarmManager.RTC_WAKEUP, time, pending);
} else {
  // Old APIs
  am.set(AlarmManager.RTC_WAKEUP, time, pending);
}

这将在所有平台上在指定的时间精确发出警报。

不要忘记,您只有在真正需要时才应使用精确调度,例如,在特定时间向用户发送警报。对于大多数其他情况,允许系统稍微调整我们的计划以保护电池寿命通常是可接受的。

Android Marshmallow API Level 23 还提供了 setAndAllowWhileIdle 函数,允许我们在 Doze 模式下创建一个警报,但与 setExactAndAllowWhileIdle() 相比,精确度较低。

系统将尝试在整个系统中批量处理这类警报,以最小化设备唤醒的次数,从而减少系统的能耗。以下是创建一个在 10 小时后触发的警报的代码:

long delay = TimeUnit.HOURS.toMillis(10L);
long time = System.currentTimeMillis() + delay;

if (Build.VERSION.SDK_INT >= 23) {
     am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pending);
}

在 Doze 模式下测试您的警报

为了测试您的应用程序在 Doze 模式下的行为,Android SDK 团队向 dumpsys 工具添加了一些新命令,以便从命令行手动更改设备的电源状态。

还需要记住,Doze 模式要求您的设备从充电器上拔下。为了强制设备进入未连接充电器的状态,我们应该在具有访问 SDK 工具的命令行上运行以下命令:

# Emulate a charger unplug
adb shell dumpsys battery unplug
# Emulate a charger plug in
adb shell dumpsys battery set ac 1

然后,为了进入空闲模式,我们应该关闭屏幕并运行以下命令:

// Enable the doze mode, step required on Android Emulator
adb shell dumpsys deviceidle enable
// To goes directly go IDLE mode
adb shell dumpsys deviceidle force-idle

将设备置于空闲模式后,我们可以通过运行以下命令来启用维护窗口:

// Goes from IDLE -> IDLE_MAINTENANCE state
adb shell dumpsys deviceidle step

如果我们再次运行相同的步骤,设备将返回到空闲状态;然而,如果我们想返回到活动状态,我们应该运行下一个命令:

// Goes from IDLE,IDLE_MAINTENANCE -> ACTIVE state
adb shell dumpsys deviceidle disable

使用这些便捷的命令,我们能够验证即使在深度空闲状态下,闹钟也会响起。

设置窗口闹钟

KitKat 中增加的一个功能是setWindow(),它通过允许我们指定闹钟必须送达的时间窗口,在精确和不精确闹钟之间提供了一个折中方案。这仍然允许系统在效率方面进行一些调整,但让我们可以选择允许多少自由度。

这是如何使用sentindo()在 3 分钟窗口内安排闹钟的示例——最早从现在起 5 分钟,最晚从现在起 8 分钟——使用实时时钟:

if (Build.VERSION.SDK_INT >= 19) {
  long delay = TimeUnit.MINUTES.toMillis(5L);
  long window = TimeUnit.MINUTES.toMillis(3L);
  long time = System.currentTimeMillis() + delay; 
  am.setWindow(AlarmManager.RTC_WAKEUP, time, window, pending);
}

调试 AlarmManager 闹钟

Android 系统附带了一个便捷的诊断工具,它会向开发者输出设备上注册的闹钟列表。要获取列表,我们从命令行运行以下命令:

adb shell dumpsys alarm

在我们为 Android API Level 23 创建精确的 5 分钟闹钟后,系统将在命令输出中输出我们的注册闹钟。

...
Batch{bfce57 num=1 start=6199180 end=6199180 flgs=0x5}:
RTC_WAKEUP #0: Alarm{
 d38d44 type 0 when 1449181419460  
    com.packpublishing.asynchronousandroid}
 tag=*walarm*:my_alarm
 type=0 whenElapsed=+58s670ms when=2015-12-03 22:23:39
    window=0 repeatInterval=0 count=0 flags=0x5
    operation=PendingIntent{a58bbe0: PendingIntentRecord{
     466e99 android broadcastIntent}}

闹钟系统试图通过批量执行闹钟以节省电池,因此第一行包含我们的闹钟所属的闹钟批量的信息。

批处理输出格式的详细信息如下所示列表:

  • bfce57:批处理内部标识号。

  • num=1:此批处理中的闹钟数量。

  • start=6199180:它指的是批处理应该开始的时间,以系统启动以来的已流逝毫秒数表示。

  • end=6199180:它指的是批处理结束的时间,以系统启动以来的已流逝毫秒数表示。

在批处理内部,我们的闹钟在以下字段中得到了详细描述:

  • d38d44:系统使用的内部标识号。

  • type 0 (RTC_WAKEUP):闹钟类型。

  • when:基于时钟时间的闹钟时间(自纪元以来的毫秒数)。

  • tag=*walarm*:my_alarm:在 Intent 中指定的操作。

  • com.packpublishing.asynchronousandroid:创建闹钟的应用程序包。

  • whenElapsed=+58s670ms:指的是自系统启动以来,此闹钟将被触发的时刻。

  • when= 2015-12-03 22:23:39:此闹钟将被触发的日期/时间。

  • window= 180000:当使用setWindow()方法时,指的是窗口字段中指定的值。

  • repeatInterval=0:在重复闹钟中用于指定重复之间的间隔。

  • count=0:闹钟响起的次数。

  • operation= PendingIntent...:将被触发的挂起意图。

取消闹钟

一旦设置了闹钟,可以通过调用AlarmManger.cancel方法并使用与要取消的闹钟匹配的意图来非常容易地取消闹钟。

匹配过程使用 Intent 的 filterEquals 方法,该方法比较两个 Intent 的动作、数据、类型、类、组件、包和类别,以测试等价性。我们可能在 Intent 中设置的任何 extras 都不考虑在内。

在下面的代码中,我们将向您展示如何创建一个在 1 小时后触发的闹钟,以及取消代码,使用不同的 intent 实例来取消它:

// Function to set the Alarm
void set1HourAlarm(long time) {
  AlarmManager am= (AlarmManager) getSystemService(ALARM_SERVICE);
  long time = in1HourTime();
  am.set(AlarmManager.RTC, time, createPendingIntent(time));
}

// Cancel the alarm
void cancel1HourAlarm(long time) {
  AlarmManager am= (AlarmManager) getSystemService(ALARM_SERVICE);
  // Remove the alarms matching the Intent
  am.cancel(createPendingIntent(time));
}

// Creates the Pending Intent to set and cancel the alarm
PendingIntent createPendingIntent(long time) {
  Intent intent = new Intent("my_alarm");
  PendingIntent pending = PendingIntent.
    getBroadcast(this, ALARM_CODE, intent,
                 PendingIntent.FLAG_UPDATE_CURRENT);
  // extras don't affect matching
  intent.putExtra("exactTime", time);
  return pending;
}
// Calculate the Time
long in1HourTime() {
  long delay = TimeUnit.MINUTES.toMillis(5L);
  long time = System.currentTimeMillis() + delay;
  return time;
}

由于在我们的示例中我们使用相同的方法来构建设置和取消 PendingIntent,因此两者都将具有相同的动作和匹配,所以如果 AlarmManager.cancel 运行并找到匹配项,Android 系统将从已启用闹钟列表中删除之前设置的闹钟。

注意

要调试您的闹钟取消操作,您可以使用 adb shell dumpsys alarm 再次验证闹钟是否已从系统闹钟批次中消失。

重要的是要意识到,无论何时我们使用带有 FLAG_UPDATE_CURRENT 的 pending intent 创建闹钟,我们都会隐式更新任何现有的闹钟,使用新的 Intent 和其 extras。

设置重复闹钟

除了设置一次性闹钟外,我们还有使用 setRepeating()setInexactRepeating() 来安排重复闹钟的选项。这两种方法都接受一个额外的参数,该参数定义了重复闹钟的间隔(以毫秒为单位)。通常,建议避免使用 setRepeating(),始终使用 setInexactRepeating(),允许系统优化设备唤醒,并在运行不同 Android 版本的设备上提供更一致的行为:

   void setRepeating(
       int type, long triggerAtMillis,
       long intervalMillis, PendingIntent operation);

   void setInexactRepeating(
       int type, long triggerAtMillis,
       long intervalMillis, PendingIntent operation)

AlarmManager 提供了一些方便的常量用于典型的重复间隔:

   AlarmManager.INTERVAL_FIFTEEN_MINUTES
   AlarmManager.INTERVAL_HALF_HOUR
   AlarmManager.INTERVAL_HOUR
   AlarmManager.INTERVAL_HALF_DAY
   AlarmManager.INTERVAL_DAY

现在让我们构建一个示例,创建一个大约 2 小时后送达的重复闹钟,然后每隔大约 15 分钟重复一次,如下所示:

Intent intent = new Intent("my_alarm");
PendingIntent broadcast = PendingIntent.getBroadcast(
  this, 0, intent,PendingIntent.FLAG_UPDATE_CURRENT);
long start = System.currentTimeMillis() +
             TimeUnit.HOURS.toMillis(2L);
AlarmManager am = (AlarmManager)
                  getSystemService(ALARM_SERVICE);
am.setRepeating(
  AlarmManager.RTC_WAKEUP, start,
  AlarmManager.INTERVAL_FIFTEEN_MINUTES, broadcast);

从 API 级别 19 开始,所有重复闹钟都是不精确的——也就是说,即使我们的应用程序针对 KitKat 或更高版本,即使我们使用 setRepeating(),我们的重复闹钟也会是不精确的。为了在所有 Android 版本中实现类似的不精确行为,您应该使用 setInexactRepeating()(API 级别 3)而不是 setRepeating()

am.setInexactRepeating(
  AlarmManager.RTC_WAKEUP, start,
  AlarmManager.INTERVAL_FIFTEEN_MINUTES, broadcast);

不精确重复告诉系统,您的闹钟时间可能会调整以减少设备频繁唤醒并提高系统的整体电源效率。

如果我们真的需要精确重复闹钟,我们可以使用 setExact()/setExactAndAllowWhileIdle(),而不是使用 setExact(),并在处理当前闹钟的同时安排下一个闹钟。

之后,我们可能会增加重复闹钟的间隔,甚至可以通过调用带有匹配先前 Intent 和 FLAG_UPDATE_CURRENT 标志的 Intent 的 setRepeating() 来更改 Intent Extras,如下面的代码所示:

Intent intent = new Intent("my_alarm");
PendingIntent broadcast = PendingIntent.getBroadcast(
  this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Updates the delivery intent extras
intent.putExtra("my_int",3);
am.setRepeating(
  AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
  AlarmManager.INTERVAL_HALF_HOUR, broadcast);

设置闹钟

从 API 级别 21 开始,setAlarmClock,该函数设置一个新的闹钟并显示状态栏闹钟图标,被引入到 AlarmManager 类中:

 void setAlarmClock(AlarmClockInfo info, PendingIntent operation)

在下一个示例中,我们将创建一个闹钟,明天晚上 10:00 分响起:

Intent intent = new Intent("my_clock_alarm");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 1);
calendar.set(Calendar.HOUR_OF_DAY, 22);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);

PendingIntent broadcast = PendingIntent.getBroadcast(
                                   AlarmClockActivity.this, 0, intent, 
                                   PendingIntent.FLAG_UPDATE_CURRENT);

// Only applies to newer versions
If ( Build.VERSION.SDK_INT >= 21 ) {

  AlarmClockInfo alarmInfo = new AlarmClockInfo(          
     calendar.getTimeInMillis(),    
     // Create a Pending intent to show Alarm Details
     createShowDetailsPI());
  am.setAlarmClock(alarmInfo, broadcast);

} else {

  am.set(AlarmManager.RTC_WAKEUP,
         calendar.getTimeInMillis(), broadcast);
}
...
PendingIntent createShowDetailsPI() {
    ntent showIntent = new Intent(AlarmClockActivity.this,
                                  ShowAlarmActivity.class);
    return PendingIntent.getActivity(AlarmClockActivity.this, 0,
                                     showIntent,                                                 
                                     PendingIntent.
                                       FLAG_UPDATE_CURRENT);
}

如果你使用的是最新设备,一旦我们设置了之前的警报,我们会在系统状态栏上看到时钟图标:

设置闹钟

要取消闹钟,我们必须使用匹配的意图调用cancel方法:

Intent intent = new Intent("my_clock_alarm");
PendingIntent broadcast = PendingIntent.getBroadcast(
  this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
am.cancel(broadcast);

处理警报

到目前为止,我们已经学习了如何在AlarmManager单例服务上安排精确和不精确的警报,因此在这个时候,我们准备看看如何在任何 Android 应用程序组件中处理警报。

实际上,我们可以安排任何可以用PendingIntent启动的事情,这意味着我们可以使用警报来启动活动、服务和BroadcastReceivers。为了指定我们的警报目标,我们需要使用PendingIntent的静态工厂方法:

PendingIntent.getActivities(Context, int,Intent[],int)
PendingIntent.getActivity(Context,int, Intent, int)
PendingIntent.getService(Context,int, Intent, int)
PendingIntent.getBroadcast(Context,int, Intent, int)

所有提供的静态方法都用于创建一个挂起意图,接收一个 Context 对象、一个用于标识挂起意图的整数请求代码、一个 Intent 或 Intent 数组,这些 Intent 将被发送到组件,最后是一个整数,用于指定PendingIntent标志。

在工厂方法上使用的PendingIntent标志在 Intent 处理中起着重要作用,因此理解我们可以用来指示系统如何处理已存在的 intent、使 intent 不可变或设置只发送一次的 intent 的标志至关重要:

  • FLAG_CANCEL_CURRENT:表示系统应使现有意图无效并生成一个新的 Intent。

  • FLAG_NO_CREATE:如果PendingIntent尚不存在,则不会创建新的意图,并且工厂方法返回null

  • FLAG_ONE_SHOT:表示创建的挂起意图只能使用一次。

  • FLAG_UPDATE_CURRENT:表示如果挂起意图已经存在,则挂起意图将被此意图替换,包括所有附加信息。

  • FLAG_IMMUTABLE:表示创建的挂起意图之后不能被修改。此标志仅从 API 级别 23 开始可用。

在大多数情况下,我们希望完全用新的 intent 替换现有的 intent,因此使用FLAG_UPDATE_CURRENT是正确的标志值。

在接下来的章节中,我们将为可以使用AlarmManager的每种类型的PendingIntent构建示例。

使用活动处理警报

从警报启动Activity就像使用通过调用PendingIntent的静态getActivity方法创建的PendingIntent注册警报一样简单。

当警报送达时,Activity将被启动并带到前台,取代任何当前正在使用的应用程序。请注意,这可能会让用户感到惊讶,甚至可能让他们感到烦恼!

当启动带有警报的活动时,我们可能希望设置Intent.FLAG_ACTIVITY_CLEAR_TOP;这样,如果应用程序已经在运行,并且我们的目标Activity已经在后台栈中,新的意图将被发送到旧的Activity,并且其上所有其他活动都将被关闭:

   Intent intent = new Intent(context, HomeActivity.class);
   intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   PendingIntent pending = PendingIntent.getActivity(
       Context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

并非所有 Activity 都适合用getActivity启动。我们可能需要启动一个通常出现在应用深处的Activity,在那里按下后退不会退出到主屏幕,而是返回到后栈中的下一个Activity

让我们设想一个情况,我们想要启动一个将显示模型详细信息的Activity,并且我们想要在后台栈中有一个列出模型的Activity

这就是getActivities发挥作用的地方。通过getActivities,我们可以将多个Activity推送到应用程序的后栈,允许我们填充后栈以在用户按下“后退”时创建所需的导航流程。为此,我们通过向getActivities发送 Intent 数组来创建我们的PendingIntent

   Intent first = new Intent(context, ListActivity.class);
   Intent second = new Intent(context, DetailActivity.class);
   first.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

   PendingIntent pending = PendingIntent.getActivities(
       context, 0,
       new Intent[]{first, second},
       PendingIntent.FLAG_UPDATE_CURRENT);

Intent 数组指定了按顺序启动的Activity。当这个警报被传递时,事件发生的逻辑顺序如下:

  1. 如果应用程序已经在运行,那么在ListActivity之上的后栈中的任何 Activity 都将被完成并移除,因为我们设置了Intent.FLAG_ACTIVITY_CLEAR_TOP标志。

  2. ListActivity被(重新)启动。

  3. DetailActivity被启动并放置在ListActivity的后栈之上。DetailActivity成为前台Activity

使用Activity处理警报是一个值得了解的知识点,但不是我们经常使用的技巧,因为它非常侵入性。我们更有可能希望在后台处理警报,我们将在下一节中探讨这一点。

使用 BroadcastReceiver 处理警报

我们已经在第五章中遇到了BroadcastReceiver,在与服务交互中,我们使用它在一个Activity中接收来自Service的广播。在本节中,我们将使用BroadcastReceiver来处理在AlarmManager上设置的警报。

BroadcastReceivers可以在运行时动态注册和注销,就像我们在第五章中做的那样,与Service一起,或者使用 Android 清单文件中的<receiver>元素静态注册,并且可以接收无论如何注册的警报。

使用静态注册的接收器处理警报更为常见,因为这些对系统来说是已知的,并且可以在应用程序当前未运行时通过警报启动应用程序。

让我们实现一个静态定义的BroadcastReceiver,当警报响起时能够向一个电话号码发送短信。首先,我们将在清单文件中定义我们的BroadcastReceiver

<receiver android:name=".chapter6.SMSDispacther">
  <intent-filter>
    <action android:name="sms_dispacther"/>
  </intent-filter>
</receiver>

<intent-filter>元素给我们提供了机会,通过指定应该匹配的动作、数据和类别来说明我们想要接收哪些 Intent。

现在是时候编写代码来设置日程安排了。为此,我们将创建一个提供设置目的地号码、延迟消息发送的小时数和要发送的消息文本的表单的Activity

SMSDispatchActivity活动上,我们将为sms_dispatcher动作创建一个PendingIntent,通过 Intent 附加信息传递所需的参数:

public class SMSDispatchActivity extends Activity {
  // UI Code omitted for brevity
  …
  private OnClickListener mSubmit = new OnClickListener() {
    ...
    // Calculate the scheduled time
    // time = now + N*hours
    long delay = TimeUnit.HOURS.toMillis(hours);
    long time = System.currentTimeMillis() + delay;

    // Store the UI Form on the Intent
    intent.putExtra(SMSDispatcher.TO_KEY, phoneMumber);
    intent.putExtra(SMSDispatcher.TEXT_KEY, text);

    // Create the Broadcast Pending Intent
    PendingIntent broadcast = PendingIntent.getBroadcast(
      getBaseContext(), 0, intent,
      PendingIntent.FLAG_UPDATE_CURRENT);  

    // Set an exact Alarm
    if (Build.VERSION.SDK_INT >= 23) {
      am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time,
                                   broadcast);
    } else if (Build.VERSION.SDK_INT >= 19) {
      am.setExact(AlarmManager.RTC_WAKEUP, time, broadcast);
    } else {
      am.set(AlarmManager.RTC_WAKEUP, time, broadcast);
    }
  }
}

当这个闹钟到期时,AlarmManager即使在深度空闲状态下也会唤醒设备(如果它还没有醒来)并将 Intent 传递给BroadcastReceiveronReceive方法。闹钟管理器将保持唤醒锁,直到闹钟接收器的onReceive()运行。因此,它保证了设备至少会保持唤醒状态,直到onReceive完成,这意味着我们可以在设备被允许返回睡眠之前完成一些工作。

BroadcastReceiver一起工作

当系统向我们的BroadcastReceiver发送闹钟时,它是在主线程上进行的,因此适用通常的主线程限制;我们不能执行网络操作,也不应该执行重型处理或使用阻塞操作。

此外,静态注册的BroadcastReceiver具有非常有限的生命周期。它不能创建除了吐司或通过NotificationManager发布的通知之外的用户界面元素,onReceive方法必须在 10 秒内完成,否则其进程可能会被杀死,并且一旦onReceive完成,接收器的生命周期就结束了。

由于我们需要的工作不是密集型的,我们可以在onReceive期间简单地完成它:

public class SMSDispatcher extends BroadcastReceiver {

  public static final String TO_KEY = "to";
  public static final String TEXT_KEY = "text";

  @Override
  public void onReceive(Context context, Intent intent) {
     // Retrieve the Destination number and the
    // message from the intent extras
    String to = intent.getStringExtra(TO_KEY);
    String text = intent.getStringExtra(TEXT_KEY);

    Log.i("SMS Dispatcher", "Delivering message to " + to);
    SmsManager sms = SmsManager.getDefault();   
    sms.sendTextMessage(to, null, text, null, 0), null);
  }
}

就这样;一旦闹钟触发,BroadcastReceiver.onReceive将被调用,将短信发送到目标号码,文本由 UI 表单指定。

我们可以通过在收到来自移动网络的消息投递报告时向用户发送通知来使这更有用。

首先,我们将在AndroidManifest.xml中添加一个新的动作,由我们的BroadcastReceiver处理:

<receiver android:name=".chapter6.SMSDispatcher">
  <intent-filter>
      <action android:name="sms_dispatch"/>
  </intent-filter>
  <intent-filter>
      <action android:name="sms_delivered"/>
  </intent-filter>
</receiver>

接下来,我们将更改onReceive方法以处理两种类型的Intent

@Override
public void onReceive(Context context, Intent intent) {

    if ( intent.getAction().equals(DELIVERED_ACTION) ) {
      processDispatch(context, intent);
    } else if (intent.getAction().equals(DISPATCH_ACTION)) {
      processDelivered(context, intent);
    }
}

接下来,更新代码以调度消息以设置新的PendingIntent用于消息投递报告:

void processDispatch(Context context, Intent intent) {
  ...
  Intent deliveredIntent = new Intent("sms_delivered");
  deliveredIntent.putExtra(SMSDispatcher.TO_KEY, to);
  deliveredIntent.putExtra(SMSDispatcher.TEXT_KEY, text);
  sms.sendTextMessage(to, null, text, null,
    PendingIntent.getBroadcast(context,
      DISPATCH_ACTION.hashCode(), deliveredIntent, 0));
}

最后,我们添加代码来处理消息投递报告意图,并在通知抽屉中通知用户,如果消息已成功投递:

void processDelivered(Context context, Intent intent) {
  String to = intent.getStringExtra(TO_KEY);
  String text = intent.getStringExtra(TEXT_KEY);
  String title = null;
  switch (getResultCode()) {
  case Activity.RESULT_OK:
    title = "Message Delivered to " + to;
    break;
  default:
    title = "Message Delivery failed to " + to;
    break;
  }
  NotificationCompat.Builder builder = new
    NotificationCompat.Builder(context)
      .setContentTitle(title)
      .setContentText(text)
      .setSmallIcon(android.R.drawable.stat_notify_chat)
      .setStyle(new NotificationCompat.BigTextStyle()
         .bigText(text));
  NotificationManager nm = (NotificationManager)
                           context.getSystemService(
                             Context.NOTIFICATION_SERVICE);
  nm.notify(intent.hashCode(), builder.build());
}

虽然我们可以在BroadcastReceiver中花费最多 10 秒进行工作,但我们实际上不应该这样做——如果当闹钟触发时应用处于前台,那么如果onReceive在主线程上完成超过一百毫秒,用户将感受到明显的延迟。超过 10 秒的预算将导致系统终止应用并报告后台 ANR。

此外,如果我们尝试在后台线程中执行onReceive工作并且onReceive返回,Android 系统允许回收组件。每当没有其他 Android 组件运行时,系统可能会考虑进程为空,并积极将其杀死,立即停止我们的后台工作。

为了避免 UI 闪烁和 BroadcastReceiver 回收,在 Android API Level 11 上,BroacastReceiver.goAsync 方法被宣布将工作委托给后台线程,最多 10 秒——我们将在下一节中讨论这一点。

使用 goAsync 进行异步工作

如果我们的应用程序针对最低 API 级别 11,我们可以使用 BroadcastReceiver.goAsync 的一个功能来并行执行 onReceive 执行:

public final PendingResult goAsync()

使用 goAsync,我们可以在整个操作仍在 10 秒预算内完成的情况下,将 BroadcastReceiver 实例的生存期延长到其 onReceive 方法的完成之后。

如果我们调用 goAsync,系统在 onReceive 方法完成后不会认为 BroadcastReceiver 已经完成。相反,BroadcastReceiver 会继续存在,直到我们调用返回给我们的 PendingResult 上的 finish 方法。我们必须确保在 10 秒预算内调用 finish,否则系统将使用后台 ANR 杀死进程。

使用 goAsync,我们可以使用任何适当的并发结构(例如,AsyncTask)将工作卸载到后台线程,并且设备保证会保持唤醒状态,直到我们调用 PendingResult 上的 finish

让我们更新我们的短信调度器以异步发送消息:

public void onReceive(final Context context, final Intent intent) {
   ...
   final PendingResult result = goAsync();
   AsyncTaskCompat.executeParallel(
    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        try {
          // ... do some work here, for up to 10 seconds
          processDispatch(context, intent);
        } finally {
          result.setResultCode(Activity.RESULT_OK);
         result.finish();
        }
        return null;
      }
    });
   ...
}

注意

AsyncTaskCompat 自 Android Support Library 21.0.0 版本以来可用,允许开发者以向后兼容的方式在多个线程池上并行执行多个 AsyncTask

这很好,尽管其效用受到 10 秒预算和碎片化(它仅适用于 API 级别 11 及以上)的影响。在下一节中,我们将探讨使用服务安排长时间运行的操作。

使用服务处理闹钟

就像启动活动一样,从闹钟启动 Service 涉及安排适当的 PendingIntent 实例,这次使用静态的 getService 方法:

Intent intent = new Intent(this,SMSDispatcherIntentService.class);
intent.putExtra(SMSDispatcherIntentService.TO_KEY, phoneNumber);
intent.putExtra(SMSDispatcherIntentService.TEXT_KEY, text);
PendingIntent service = PendingIntent.getService(
   context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
am.set(AlarmManager.RTC_WAKEUP, time, service);

正如你所知道的那样,Service 应该在 AndroidManifest.xml 中全局定义,使用服务元素。鉴于我们正在使用类名显式调用它,我们只需要定义服务类:

<service android:name=".chapter6.SMSDispatcherIntentService" >
</service>

我们几乎肯定希望我们的 Service 在主线程之外执行其工作,因此通过这种方式将工作发送到 IntentService 似乎是理想的,IntentService 也会在完成工作后停止自己。如果设备是唤醒状态,这会可靠地工作。

然而,如果设备处于睡眠状态,我们可能会遇到潜在问题。AlarmManager 文档告诉我们,关于设备唤醒状态的唯一保证是它将保持唤醒状态,直到 BroadcastReceiveronReceive 方法完成。

由于直接启动 Service 不涉及 BroadcastReceiver,并且无论如何都是异步操作,因此无法保证设备返回睡眠状态之前 Service 已经启动,因此工作可能直到设备下一次唤醒才完成。

这几乎肯定不是我们想要的行为。我们想要确保 Service 启动并完成其工作,无论设备在闹钟送达时是否处于唤醒状态。为此,我们需要一个 BroadcastReceiver 和一点显式的电源管理,正如我们接下来将要看到的。

使用 WakeLock 保持唤醒

在本章的早期,我们了解到我们可以使用 BroadcastReceiver 来处理闹钟,甚至可以在后台工作长达 10 秒,尽管这仅限于运行 API 级别 11 或更高版本的设备。

在上一节中,我们看到直接使用服务处理闹钟并不是调度长时间运行工作的可靠解决方案,因为没有保证我们的 Service 在设备返回睡眠状态之前启动。

我们遇到了问题!如果我们想要在闹钟响应时执行长时间运行的工作,我们需要一种克服这些限制的解决方案。

我们真正想要的是启动一个 Service 来处理后台工作,并保持设备唤醒,直到 Service 完成其工作。幸运的是,我们可以通过结合 BroadcastReceiver 的唤醒保证来启动 Service,然后使用 PowerManagerWakeLock 进行显式的电源管理来保持设备唤醒。

如你所猜,WakeLock 是一种强制设备保持唤醒状态的方法。WakeLocks 有多种类型,允许应用程序在不同的亮度级别上保持屏幕开启,或者仅仅为了在后台工作而保持 CPU 运行。为了使用 WakeLocks,我们的应用程序必须在清单中请求额外的权限:

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

系统可能受到四种不同类型的唤醒锁(wakelock)的影响,它们对系统电源管理的影响各不相同:

  • PowerManager.PARTIAL_WAKE_LOCK:确保 CPU 开启,屏幕和键盘保持当前状态(空闲或唤醒)。

  • PowerManager.SCREEN_DIM_WAKE_LOCK:确保 CPU 开启,屏幕开启,可能处于暗淡状态,键盘可以保持关闭。

  • PowerManager.SCREEN_BRIGHT_WAKE_LOCK:确保 CPU 开启,屏幕全亮度,键盘可以保持关闭。

  • PowerManager.FULL_WAKE_LOCK:确保 CPU 开启,屏幕和键盘背光处于全亮度。

为了在我们使用 Service 进行后台工作时保持 CPU 运行,我们只需要 PARTIAL_WAKE_LOCK,它不会保持屏幕开启,并且我们可以像这样从 PowerManager 请求它:

PowerManager pm = (PowerManager)ctx.getSystemService(
                     Context.POWER_SERVICE);
WakeLock lock = pm.newWakeLock(
  PowerManager.PARTIAL_WAKE_LOCK, "my_app");

// Acquire the Power Lock
lock.acquire();

// Do your work here while CPU will stay on …

// Release the Power lock
lock.release();

我们需要在 BroadcastReceiveronReceive 方法中获取一个 WakeLock,并找到一种方法将其传递给我们的 Service,以便 Service 在完成其工作后可以释放锁。

不幸的是,WakeLock 实例是不可序列化的,所以我们不能仅仅通过 Intent 将它们发送到 Service。最简单的解决方案是将 WakeLock 实例作为一个静态属性来管理,这样 BroadcastReceiver 和目标 Service 都可以访问它。

这并不难实现,但实际上我们不需要自己实现它——我们可以使用方便的 v4 支持库类WakefulBroadcastReceiver

WakefulBroadcastReceiver公开了两个静态方法,负责获取和释放部分WakeLock。我们可以通过一个对startWakefulService的调用获取WakeLock并启动Service

ComponentName startWakefulService(Context context, Intent intent);

当我们的Service完成其工作后,它可以通过对completeWakefulIntent的相应调用释放WakeLock

boolean completeWakefulIntent(Intent intent);

现在,我们将更新我们的 SMS 计划BroadcastReceiver以获取wakelock并通过startWakefulService分发意图:

public class WakefulSMSDispatcher extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    // Forward intent to SMSDispatcherIntentService class,
    // the wakeful receiver is needed in case the
    // schedule is triggered while the device
    // is asleep otherwise the service may not have time to
    // receive the intent.
    intent.setClass(context, SMSDispatcherIntentService.class);
    WakefulBroadcastReceiver.startWakefulService(context, intent);
  }
}

我们必须确保在Service完成其工作后释放WakeLock,否则我们会在不必要地保持 CPU 供电的情况下耗尽电池。让我们实现IntentService,它从唤醒的BroadcastReceiver接收意图并在服务后台线程发送消息:

public class SMSDispatcherIntentService extends IntentService {

  @Override
  protected void onHandleIntent(Intent intent) {
    try {
      ...
      sms.sendTextMessage(to, null, text, null, null);
    } finally {
      WakefulBroadcastReceiver.completeWakefulIntent(intent);
    }
  }
}

这很棒——通过使用静态注册的BroadcastReceiver,我们确保即使在闹钟时间到来时我们的应用程序没有运行,我们也能接收到闹钟。当我们接收到闹钟时,我们获取一个WakeLock,在Service启动并执行可能长时间运行的工作期间保持设备唤醒。

一旦我们的工作完成,我们释放WakeLock以允许设备再次睡眠并节省电力。

系统重启后的闹钟重置

AlarmManager服务是一个方便的类,可以安排在 Android 应用程序上执行工作;然而,当设备关闭或重启时,由于系统在系统重启之间不会保留它们,所以所有的闹钟都会丢失。

要重置闹钟,我们应该持久化你的闹钟并创建一个BroadcastReceiver,在系统启动时设置我们的闹钟:

public class BootBroadcastReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    // Retrieve the persisted alarms
    List<SMSSchedule> persistedAlarms = getStoredSchedules();
    // Set again the alarms
    ...
  }
  List<SMSSchedule> getStoredSchedules() {...}
}

为了存储我们的闹钟,我们创建了一个POJOSMSSchedule作为我们计划的模型。

其次,在 AndroidManifest 中,我们必须注册我们的BroadcastReceiver以接收启动事件:

<receiver
    android:name=".chapter6.BootBroadcastReceiver"
    android:enabled="true" >
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

最后,我们将添加接收启动完成事件的权限:

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

现在,在系统重启后,我们重新创建我们的闹钟,即使在系统重启后它们也会触发。我们还建议在系统重启后调整使用ELAPSED_REALTIME的闹钟,因为基于那些闹钟的时钟将被重新启动。

AlarmManager 的应用

AlarmManager允许我们在没有用户干预的情况下安排工作运行。

这意味着我们可以预先安排工作,例如,准备数据,以便我们的应用程序在用户下次打开应用程序时向用户展示,或者通过通知提醒用户有关新或更新的信息。

理想的使用案例包括定期检查新电子邮件、短信调度、时间通知、定期数据处理、下载新版的期刊出版物(例如,日报和杂志),或将设备上的数据上传到云备份服务。

AlarmManager 能够有效地启动未来的工作,但应该谨慎使用 API 以保持应用程序的电池功耗处于低水平。为了实现这一点,开发者应尽量保持警报频率在一定的水平以下,并使用确切的设置函数,只有在真正必要时才强制设备唤醒。

摘要

在本章中,我们学习了如何为我们的应用程序安排在遥远的未来某个时间执行工作,无论是作为一次性操作还是定期执行。

我们学习了如何设置与系统时钟或实时相关的警报,如何唤醒设备从深度睡眠和 Doze 模式,如何在我们不再需要它们时取消警报,以及如何在最新的 Android 版本上设置精确的警报。

同时,我们向读者介绍了 Doze 模式,这是一种新的电源管理功能,通过将作业和任务推迟到维护窗口来节省电池周期。我们学习了如何测试我们的警报,考虑到由 Doze 模式引入的新电源管理状态。

我们学习了如何调试使用 AlarmManager 创建的警报,以及如何分析 dumpsys 命令打印出的信息。

我们的探索涵盖了响应警报的各种选项,包括将 Activity 带到前台或直接在 BroadcastReceiver 中执行工作,同步或异步。

最后,我们安排了一个 IntentService 使用 WakeLock 启动,以防止在完成长时间运行的后台工作期间 CPU 关闭电源,并在结束时学习了如何使用启动 BroadcastReceiver 在系统启动后重新创建警报。

AlarmManager 是一个非常有用的类,可以用于在后台安排工作,但它也有一些主要的缺点。首先,它没有考虑到设备的当前上下文,比如设备是否连接到充电器,或者设备是否连接到 Wi-Fi 网络。其次,我们只能根据时间条件来安排我们的后台工作。

为了解决这些问题,Android 团队在 Android Lollipop API 级别 5.0 中引入了 JobScheduler API;这是一个允许根据多个时间和上下文标准执行后台工作的 API。

在下一章中,我们将解释如何使用 JobScheduler API 来安排只有在适当的能源和环境设备条件满足时才会运行的任务。

第七章. 探索 JobScheduler API

到目前为止,我们一直在使用 Handler 中的时间条件来安排短期未来的后台工作,以及使用 Android Alarm Manager 来安排长期未来的工作。

那些能够在未来确切和不确切的时间执行未来任务的 API 用于触发事件、在后台刷新数据或执行无需用户干预的任务。我们在上一章中详细介绍的 AlarmManager 能够唤醒设备从深度空闲状态,并在不考虑设备电池状态的情况下执行工作。

在本章中,我们将学习如何与 JobScheduler 一起工作,在满足多个前提条件并考虑设备能耗环境的情况下在后台执行任务。

在本章中,我们将涵盖以下内容:

  • JobScheduler 简介

  • 设置 JobScheduler 运行标准

  • 使用标准控制你的任务执行

  • 如何使用 JobService 安排工作

  • 使用 JobScheduler 执行重复任务

  • 获取待处理的 JobScheduler 计划列表

  • 如何在 JobScheduler 中取消任务

JobScheduler 简介

在项目 Volta 的伞形项目下,Android 开发团队在 API 级别 21 的 Lollipop 版本中引入了一些增强功能和特性,旨在提高 Android 平台上的功耗。除了引入用于监控和跟踪 Android 平台电池使用的工具外,还正式发布了一个用于调度后台任务的新 API,以帮助开发者。当用于支持开发者应用程序的任务不需要执行时间时,它可以节省额外的电力周期,并且可以推迟到设备具有更好的电池和网络环境时再执行。

API 的创建并非为了完全替代 AlarmManager;然而,JobScheduler API 能够执行更好的电池管理并提供额外的行为。

与 Scheduler API 一起引入的主要功能如下:

  • 减少功耗:作业任务可以延迟,直到设备充电或有规律地批量运行

  • 重启后持久化任务:我们能够安装跨设备重启持久化任务的作业计划

  • 更好的网络带宽管理:作业可以延迟,直到有更高带宽的网络可用,例如当 Wi-Fi 网络连接可用时

  • 减少侵入性执行:作业可以延迟,直到用户不再与设备交互

JobScheduler 是一个单例系统服务,我们可以通过一个 Context 对象实例来检索,使用类似于以下代码的方式:

JobScheduler js = (JobScheduler)
     getSystemService(Context.JOB_SCHEDULER_SERVICE);

JobScheduler 单例服务实例对象帮助我们管理正在运行的任务,并提供成员函数来安排、取消和检索延迟作业的列表。

一旦我们有了JobScheduler服务的引用,我们可以通过将JobInfo传递给JobScheduler.schedule函数来安排一个作业:

int schedule(JobInfo job);

JobInfo是框架中使用的对象,其中我们指定了关于作业本身的所有信息,以及应满足以启动作业执行的所有条件,以及将被启动以执行所需工作的单元JobService

要构建一个JobInfo对象,一个常见的工厂模式,被称为软件工程中的Builder模式,并在静态内部类JobInfo.Builder中实现,可用于构建传递给JobSchedulerJobInfo对象。该模式为我们提供了一种在干净、逐步的基础上构建多参数JobInfo的方法,并使用Builder设置函数来定义JobInfo参数。

首先,我们将必须使用以下构造函数来构建一个JobInfo.Builder对象:

Builder(int jobId, ComponentName jobService)

jobId是一个内部数字,用于在JobScheduler服务中识别您的作业,第二个参数用于设置当系统验证所有先决条件都已满足以执行作业时将被调用的JobService派生类。

让我们编写一些代码来展示这一点:

ComponentName jobSrvc= new ComponentName(ctx, MyJobService.class);
JobInfo.Builder jobIBuilder = new JobInfo.Builder(MY_JOB_ID,
                                                  jobSrvc);

设置运行标准

使用Builder对象引用,我们可以开始设置作业参数和先决条件,使用Builder对象中可用的成员函数。

让我们考虑几个例子。在我们的第一个例子中,作业应该只在有 Wi-Fi 网络可用时启动,因此为了实现这一点,我们必须使用以下代码来设置网络可用性先决条件:

jobIBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);

无计量网络连接意味着您有一个没有每月数据使用限制的连接,并且当您超过使用限制时不会收费。当未指定网络类型时,作为条件,默认值是NETWORK_TYPE_NONE,这意味着作业将在任何网络上下文中运行,甚至在没有网络连接的情况下。除了之前的网络类型标准之外,还有NETWORK_TYPE_ANY,它确定作业可以在有网络连接可用时运行。

要指定仅在设备插入并充电时运行的作业:

jobIBuilder.setRequiresCharging(true);

当作业仅在设备处于空闲模式时运行:

jobIBuilder.setRequiresDeviceIdle(true);

空闲模式意味着作业仅在设备未被使用并且有一段时间未被使用时运行。这可能是在执行更重的计算的最佳时间,因为用户不会注意到设备资源已被分配给您的作业,因此计算不会干扰用户交互。默认情况下,任何作业都不需要空闲模式来运行。

如此持久化您的作业执行,以跨设备重启:

jobIBuilder.setPersisted(true);

例如,对于AlarmManager作业,如果您的应用程序有权接收完成的启动权限,作业计划将仅在重启后幸存。为了实现这一点,请将以下行添加到Android Manifest文件中:

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

在您想要安排周期性任务的情况下,您可以设置后续执行之间的毫秒间隔:

long interval = TimeUnit.HOURS.toMillis(5L);
jobIBuilder.setPeriodic(interval);

这是一个不精确的间隔,因为 Android 系统会尝试将任务分批处理以节省一些电池周期。

当您想定义一个最大延迟时间来运行您的任务时,我们可以指定一个时间截止点,任务必须在此时间运行,然后它将运行,无论其他准则是否满足:

long maxExecutionTime = TimeUnit.MINUTES.toMillis(5L);
jobIBuilder.setOverrideDeadline(maxExecutionTime);

在下面的代码中,我们将一个小时设置为延迟执行这个任务的最大时间,因此,如果其他先决条件未满足,一个小时后,系统将独立于其他准则运行这个任务:

jobIBuilder.setOverrideDeadline(TimeUnit.HOURS.toMillis(1L));

另一方面,我们也可以将一个最小延迟时间指定为这个任务的准则:

jobIBuilder.setMinimumLatency(TimeUnit.SECONDS.toMillis(120));

使用上述值,我们的任务将不会在接下来的120秒内运行,因为我们已经将最大延迟时间作为任务的先决条件。

注意

setMinimumLatencysetOverrideDeadline不适用于周期性任务,应避免在周期性任务计划中使用这些准则。如果这些准则中的任何一个在周期性任务中使用,当调用构建时将抛出IllegalArgumentException异常。

当任务失败时,为了指定重试策略,我们必须指定backoff初始值,该值决定了重试之间的间隔和重试增加策略。JobScheduler API 提供了两种策略,定义了重试时间在后续尝试之间增加的方式:

  • BACKOFF_POLICY_LINEAR: 重试之间的间隔时间呈线性增长—initial_backoff_millis * num_retries

  • BACKOFF_POLICY_EXPONENTIAL: 重试之间的间隔时间呈指数增长—initial_backoff_millis * 2 ^ (num_retries)设置运行标准

退避间隔将增加,直到达到五小时的退避时间(JobInfo. MAX_BACKOFF_DELAY_MILLIS),初始默认值是30秒(JobInfo .DEFAULT_INITIAL_BACKOFF_MILLIS)。

现在,在下面的示例中,我们将向您展示如何为我们的jobInfo对象创建线性退避策略和指数退避策略:

// Initial Backoff of 10 minutes that grows linearly
jobIBuilder.setBackoffCriteria(TimeUnit.MINUTES.toMillis(10L),
JobInfo.BACKOFF_POLICY_LINEAR);

// Initial Backoff of 3 minutes that grows exponentially
jobIBuilder.setBackoffCriteria(TimeUnit.MINUTES.toMillis(3),
JobInfo.BACKOFF_POLICY_EXPONENTIAL);

builder类还提供了一个方法,用于设置一些参数,以便通过PersistableBundle对象传递任务:

PersistableBundle bundle = new PersistableBundle();
bundle.putInt(MY_JOB_ARG1,2);
jobIBuilder.setExtras(bundle);

注意

PersistableBundle是一种特殊类型的包,可以保存和稍后恢复。其主要目的是将参数传递给延迟任务执行。

一旦我们有了安排我们定义的任务的所有准则,我们就能构建我们的JobInfo并使用它将任务执行纳入我们的应用程序:

JobInfo.Builder jobIBuilder = ...
// Set Criterias
JobInfo jobInfo = jobIBuilder.setRequiresCharging(true)

setRequiresDeviceIdle(true).
                                             ...
                                             build();

安排任务

已经定义了准则并且有了JobInfo对象,我们就拥有了设置我们应用程序任务所需的所有实体。所以现在让我们通过一个真实示例向您展示如何创建一个任务。

我们的示例将使用调度器 API 安排的作业同步存储在设备文件中的用户账户信息与 HTTP Web 服务。用户界面将提供一个 UI,我们可以在此处更新用户信息,一个按钮用于将信息保存到内部文件,以及一个按钮用于设置同步作业,该作业将上传账户信息到 Web 服务。

首先,让我们定义我们的作业先决条件和参数:

  • 我们的作业应该在设备充电时运行以节省电池

  • 我们的作业应该在未计费的网络可用时运行以节省移动网络带宽

  • 我们的作业应该在设备空闲时运行,因为我们不希望降低 UI 的响应速度

  • 我们的作业必须在安排后至少运行一次,在八小时内

  • 我们的作业应该在设备重启后仍然运行

JobInfo对象需要一个ID来在所有JobSchedule方法中标识作业,因此为了确保一致性,使用一个public static int来标识它是一个好主意:

  static final int ACC_BACKUP_JOB_ID ="AccountBackJobService"
  hashCode();

后续调用取消或列出作业创建必须使用在此处定义的相同jobId

由于我们在设备内部使用文件存储账户信息,因此用于检索账户信息的文件名需要作为参数传递给作业。同样的原则也适用于远程 Web 服务端点。

为了转发所需的参数,我们必须构建PersistableBundle,将文件名和端点路径作为包参数传递:

private static final String SYNC_FILE = "account.json";
private static final String SYNC_PATH = "account_sync";
private static final String SYNC_PATH_KEY = "path";
...
PersistableBundle bundle = new PersistableBundle();
// Forward filename where the account information is stored
bundle.putString(SyncTask.SYNC_FILE_KEY,SYNC_FILE);
// Forward the HTTP Path used to upload the account information
bundle.putString(SyncTask.SYNC_PATH_KEY,SYNC_PATH);

一旦声明了标准并且我们有了服务的标识符和类名,我们就能够使用Builder内部类创建我们的JobInfo,如下面的代码所示:

ComponentName serviceName = new ComponentName(this,
  AccountBackupJobService.class);

// Setup the Job Information and criterias over a builder
JobInfo jobInfo = new JobInfo.
   Builder(ACC_BACKUP_JOB_ID, serviceName)
    .setRequiresCharging(true)
    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
    .setRequiresDeviceIdle(true)
    .setPersisted(true)
    .setOverrideDeadline(TimeUnit.HOURS.toMillis(8L))
    .setExtras(bundle)
    .build();

现在我们已经准备好使用JobScheduler服务来安排作业:

// Get a Reference to the Service
JobScheduler jobScheduler = (JobScheduler) 
   getSystemService(JOB_SCHEDULER_SERVICE);

int result = jobScheduler.schedule(jobInfo);

if ( result == JobScheduler.RESULT_FAILURE ) {
  // Failed to setup the job 
  Toast.makeText(AccountInfoActivity.this, 
                 "Failed to setup a sharedpref backup job", 
                 Toast.LENGTH_SHORT).show();
} else {
  // Schedule Success 
  Toast.makeText(SharedPrefActivity.this,
                 "SharedPrefBack job successfully scheduled",
                 Toast.LENGTH_SHORT).show();
}

如果失败,JobScheduler 的schedule方法将返回RESULT_FAILURE,在成功的情况下将返回我们在JobInfo.Builder构造函数中定义的作业标识符。

现在,既然我们已经安排了我们的作业,是时候在JobService子类中编写备份行为。在下一节中,我们将详细介绍如何实现一个与JobScheduler框架良好协作的JobService

实现JobService

我们的JobService子类是即将执行艰苦工作并接收回调的实体,一旦满足JobInfo中指定的所有标准。为了实现我们自己的服务,我们必须从JobService扩展并重写启动和停止回调,如下面的代码所示:

public class AccountBackupJobService extends JobService {
    @Override
    public boolean onStartJob(JobParameters params) {
      // Start your job here
        return false;
    } 
    @Override
    public boolean onStopJob(JobParameters params) {
        // Stop your job here
        return false;
    }
}

onStartJob回调函数在主线程上运行,如果作业需要进行异步处理,则应返回true以指示它仍在后台线程上执行工作。callback函数还接收JobInfo包中指定的额外参数。

onStopJob在系统需要取消作业执行,因为jobInfo中指定的标准不再满足时自动调用。

例如,我们的任务需要在设备处于空闲状态时运行,因此,如果设备因为用户开始与设备交互而离开空闲模式,onStopJob将被调用以暂时放弃执行。

在这个函数中,我们应该释放分配给我们的任务的任何资源,并停止任何正在进行的后台处理。这个返回boolean类型的函数将指示你是否希望根据在任务创建时指定的相同标准重试此任务,或者放弃任务执行。你应该使用true来根据在任务创建期间指定的重试标准重新安排此任务。

在我们添加服务业务逻辑之前,我们必须将我们的Service类添加到AndroidManifest.xml中,并且我们必须使用android.permission.BIND_JOB_SERVICE权限保护我们的服务:

<service 
   android:name=".chapter7.AccountBackupJobService"
   android:exported="true"
   android:permission="android.permission.BIND_JOB_SERVICE" />

关于你的JobService实现有两个重要的事情需要记住:

  1. 一、onStartJobonStopJob回调将在主线程上运行,你有责任将你的服务的长时间运行执行转移到单独的线程上,以防止由于主线程中的阻塞操作而在你的应用程序中出现任何 ANR 对话框。

  2. 第二,当你的JobService回调正在运行或直到你明确调用jobFinished方法(在这种情况下,你在onStartJob函数中返回true)时,Android 系统将为你获取并保持一个WakeLock。如果你不这样做,告诉系统你的任务执行已完成。WakeLock将保持你的设备唤醒,并浪费你的设备电池。这可能会让用户生气,并成为用户卸载应用程序的原因,因为你的应用程序会浪费资源、电池,并影响用户体验。

现在我们已经学习了关于JobService的理论,让我们编写代码,在后台处理线程上执行与远程服务器的账户同步操作,而不是在主线程上。

考虑到到目前为止学习的 Android 结构,我们将使用在第三章中学习的AsyncTask结构,探索 AsyncTask,因为它简单,并创建一个用于上传账户信息的AsyncTask子类:

public class Result<T> {
    public T result;
    public Exception exc;
}

public class SyncTask extends 
  AsyncTask<JobParameters, Void, Result<JobParameters>> {

  // Parameter Keys for parameter arguments
  public static final String SYNC_FILE_KEY = "file";
  public static final String SYNC_ENDPOINT_KEY = "http_endpoint";

   // Variable used to store a reference to the service
  private final JobService jobService;

  // Constructor
  public SyncTask(JobService jobService) {
        this.jobService = jobService;
  } 
  ...
}

作为起点,我们指定了通用的AsyncTask类参数类型,将JobParameters设置为doInBackground的参数,将Result设置为从doInBackground返回的类型,并将其传递给onPostExecute函数。

然后,我们创建用于在 bundle 中传递信息的最终常量键。

Result类型也会从之前的会话中恢复,以便在后台执行过程中发生错误时返回错误。

不深入细节,我们将实现负责将数据上传到远程 Web 服务的doInBackground代码:

@Override
protected Result<JobParameters> doInBackground(
  JobParameters... params) {

  Result<JobParameters> result = new Result<JobParameters>();
  HttpURLConnection urlConn = null;
  try {
    URL url;
    ...
         // Retrieve the file to upload from the parameters
         // passed in 
    String file = params[0].getExtras().
                                  getString(SYNC_FILE_KEY);
         // Remote WebService Path
    String endpoint = params[0].getExtras().
                             getString(SYNC_ENDPOINT_KEY);
    url = new URL ("http://<webs_host>:<webs_port>/" 
                        + endpoint);
    ...
    // Load the account information stored internally
    String body = Util.loadJSONFromFile(jobService, file);
    // Write the information to the remote service
            uploadJsonToServer(urlConn, body);
    // Process Server Response
         ...
  } catch (Exception e) {
    result.exc = e;
  } finally {
     if ( urlConn != null) {
      urlConn.disconnect();
     }
  }
  return result;
}

为了简洁,这里省略了一些实现细节,但我们已经实现了doInBackground函数来读取存储在设备文件上的内部 JSON 数据,并通过HttpURLConnection上传它。显示表单的 Android Activity会将按钮保存并同步到最终用户。当按下保存按钮时,会将账户信息存储在account.json本地文件中。

当点击同步按钮时,将安排与我们的远程 HTTP 服务器同步数据的任务。当我们定义的作业标准得到满足时,doInBackground会被调用以在后台执行同步过程。

现在我们有了上传数据到我们服务器的代码,让我们通过处理响应和服务器错误来完成它:

try {
  ...
  int resultCode = urlConn.getResponseCode();
  if ( resultCode != HttpURLConnection.HTTP_OK ) {
    throw new Exception("Failed to sync with server :" + 
        resultCode);
  }
  result.result = params[0];
...

当发生异常时,例如服务器宕机或发生服务器内部错误,异常会通过我们的Result对象传播到onPostExecute以进行进一步处理。

注意,我们正在小心处理错误情况,因此,为了将后台工作的结果通知用户,我们将编写一个在主线程上运行的onPostExecute函数,该函数将发布一个系统通知,告知用户任务是否成功完成或彻底失败:

@Override
protected void onPostExecute(Result<JobParameters> result) {

  NotificationCompat.Builder builder =
    new NotificationCompat.Builder(jobService);
  ...
  if ( result.exc != null ) {
    // Error handling 
    jobService.jobFinished(result.result, true);
    builder.setContentTitle("Failed to sync account")
    .setContentText("Failed to sync account " + result.exc);
  } else {
    // Success handling
    builder.setContentTitle("Account Updated")
      .setContentText("Updated Account Sucessfully at " + 
             new Date().toString());
    jobService.jobFinished(result.result, false);
  }
  nm.notify(NOTIFICACTION_ID, builder.build());
}

当任务完成时,我们调用jobFinished(JobParameters params, boolean needsRescheduled)来让系统知道我们已完成该任务;然而,当发生异常时,我们通过将第二个jobFinished参数设置为true来通知系统我们未能成功完成任务。

当一个完成的作业失败并需要重新安排时,我们将第二个jobService.jobFinished参数传递为false,调度器 API 将使用JobInfo对象中指定的回退时间重新安排我们的作业。然而,由于我们的作业仅在空闲模式下执行,失败的作业将被添加到调度器队列,并在未来的空闲维护窗口中重新执行,而不使用JobInfo中指定的回退时间。

总是调用jobFinished来释放分配给作业的WakeLock,并通知系统它可以处理额外的作业。

如果一切顺利,通知抽屉中应该会出现一个通知,显示成功消息和上次成功同步的时间。

最后,我们可以更新SyncJobService代码以启动和停止SyncTask的执行:

public class SyncJobService extends JobService {

    private static final String TAG = "SyncJobService";
    SyncTask mJob = null;
    @Override
    public boolean onStartJob(JobParameters params) {
        Log.i(TAG, "on start job: " + params.getJobId());
        if ( mJob != null ){
            mJob = new SyncTask(this);
            mJob.execute(params);
            return true;
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.i(TAG, "on stop job: " + params.getJobId());
        if ( mJob != null ){
            mJob.cancel(true);
            mJob = null;
        }
        return true;
    }    
}

列出挂起的作业

AlarmManager API 不同,调度器 API 提供了列出您应用程序所有挂起计划的能力。这个实用的功能可以帮助我们识别将要执行的作业,并相应地处理该列表。检索到的列表可以帮助我们定位我们想要取消的挂起作业。

JobScheduler服务类有一个具有以下签名的instance方法:

public List<JobInfo> getAllPendingJobs();

此方法将返回一个JobInfo对象列表,我们可以使用它来观察在构建工作期间的工作参数集:

  • 每个工作的工作标准:

    • getNetworkType()

    • isRequireDeviceIdle()

    • isRequireCharging()

    • getMinLatencyMillis()

    • isPeriodic()

    • getIntervalMillis()

    • isPersisted()

    • getMaxExecutionDelayMillis()

  • 将由JobScheduler回调以执行作业的JobService子类——getService()

  • 作业参数:getExtras()

  • 重试策略:getInitialBackoffMillis()getBackoffPolicy()

好的,现在我们准备好创建一个 Activity 来打印我们应用程序的待处理工作列表:

public class JobListActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
   ...
JobScheduler jobScheduler = (JobScheduler)
       getSystemService(JOB_SCHEDULER_SERVICE);

    // Get the list of scheduled jobs
    List<JobInfo> jobList = jobScheduler.getAllPendingJobs();
    // Initialize the adapter job list
    JobListRecyclerAdapter adapter = 
      new JobListRecyclerAdapter(this, jobList);

    rv.setAdapter(adapter);
    // Set the Job Counter
    TextView jobCountTv = (TextView)findViewById(R.id.jobCount);
    jobCountTv.setText(Integer.toString(jobList.size()));
  }
}

为了在 UI 中列出待处理的工作,我们使用了支持库的RecyclerView类,它是ListView的一个更高级版本,简化了创建大量Views的过程。

首先,我们将构建我们的ViewHolder来保存将显示jobIdService端点的行视图的引用:

public class JobListRecyclerAdapter extends
  RecyclerView.Adapter<JobListRecyclerAdapter.JobViewHolder> {
       …
  public static class JobViewHolder extends
    RecyclerView.ViewHolder {
    // References to the Views
    CardView cv;
    TextView jobId;
    TextView serviceName;

    JobViewHolder(View itemView) {
      super(itemView);
      cv = (CardView)itemView.findViewById(R.id.cv);
      jobId = (TextView)itemView.findViewById(R.id.jobIdTv);
      serviceName = (TextView) 
        itemView.findViewById(R.id.className);
   }
 }
}

要将jobInfo参数绑定到当前的ViewHolder,我们将编写RecyclerView.onBindViewHolder来根据当前的JobInfo设置信息:

@Override
public void onBindViewHolder(
  JobListRecyclerAdapter.JobViewHolder holder, int position) {
  // Retrieve the job for the current list row
  final JobInfo ji = mJobList.get(position);
  // Update the UI Views with the Job Info
  holder.jobId.setText(Integer.toString(ji.getId()));
  holder.serviceName.setText(ji.getService().getClassName());
}

是的,多亏了getAllPendingJobs,我们有了我们工作列表,而且我们还可以通过编程方式分析它,以创建围绕当前应用程序情况的行为。

故意省略了一些代码;然而,完整的源代码可以在 Packt Publishing 网站上找到。查看完整的源代码,可以欣赏到如何使用 recycler view 和 card view 构建工作列表 UI。

要完全随意地操作作业,我们只需要在本章中覆盖一个 CRUD(创建、读取、更新、删除)操作——即删除操作。删除作业操作由cancel函数提供,将在下一节中详细介绍。

取消作业

有一些情况下,我们希望为用户提供取消作业的能力,因为环境情况已经改变或者执行作业不再有意义——例如,用户更改了作业依赖的信息,作业就不再适用。JobScheduler服务通过以下cancelcancelAll方法为我们提供了作业取消的支持:

void cancel(int jobId);

void cancelAll();

第一个方法cancel(jobId)允许我们使用从schedule(JobInfo job)函数返回的作业标识符或JobInfo对象上的jobId取消一个特定的作业。

cancelAll()方法允许我们取消由当前应用程序通过JobScheduler注册的已安排的任务。

使用前一个示例中的JobInfo,我们可以通过传递作业标识符来取消一个特定的作业:

  final JobInfo ji = ...;
  JobScheduler jobScheduler = (JobScheduler)
        mContext.getSystemService(mContext.JOB_SCHEDULER_SERVICE);
  // Cancel a Specific Job based on the JobInfo->jobId
  jobScheduler.cancel(ji.getId());

每当我们取消一个安排时,该任务将从JobScheduler未来执行队列中删除,并且将不再由SyncJobService或任何其他JobService执行。

安排周期性工作

到目前为止,我们已经安排了一次性作业,但你是否知道还有一个选项可以安排在周期性间隔内执行作业。这类作业可能是执行重复备份或重复网络操作(如应用程序用户数据备份)的完美结构。

让我们更新我们的 AccountInfoActivity 以安排定期执行账户同步作业。

我们将首先为我们的周期性作业定义一个新的作业标识符:

static final int SYNC_PER_JOB_ID = "SyncJobPerService".hashCode();

我们可以像这样安排一个周期性作业,大约每 12 小时执行一次:

JobInfo.Builder builder = new JobInfo.Builder(SYNC_PER_JOB_ID,
   serviceName);
builder.setRequiresDeviceIdle(true)  
  // Persist the schedule across the device reboots
 .setPersisted(true)
 .setPeriodic(TimeUnit.HOURS.toMillis(12L))
  .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
  .setRequiresDeviceIdle(true)
  .setExtras(bundle);

// Schedule the repeating job
JobScheduler jobScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(builder.build());

现在,我们能够安排在设备空闲且 Wi-Fi 网络可用的情况下定期在后台运行同步作业。作业调度将由系统持久化,并在设备启动后重新启用,直到我们明确取消作业或通过 cancelAll() 取消所有作业。

JobScheduler 的应用

JobScheduler API 允许我们在满足某些条件下,在未来无需用户干预的情况下异步调度工作。此 API 还能够通过推迟作业执行直到设备充电或连接到非计费网络(如 Wi-Fi 网络)来减少能耗。

理想情况包括诸如可以推迟且不需要精确时间执行的数据库备份、定期上传用户数据到网络以及配置参数的下载等。因此,通常不需要立即运行且数据不需要为用户消费准备好的作业。在不影响用户体验的情况下减少应用程序的能耗将增加设备电池寿命,从而提高用户体验。

JobScheduler 可以覆盖大多数 AlarmManager 的用例,尽管它提供了优化设备资源获取的高级功能。作为附加功能,此 API 提供了一种创建在设备关闭和重启后仍然有效的调度方式。

唯一的缺点是 JobScheduler 仅在 Android 5.0(Lollipop)中引入。因此,您需要将应用程序的目标设置为 API 版本 21 或更高版本,以与该 API 交互。

在 2016 年 7 月撰写本文时,45% 的 Android 设备运行支持 JobScheduler 的 Android 版本。要获取关于按版本划分的 Android 市场份额的最新信息,请查看 Android 开发者仪表板。

摘要

在本章中,我们探讨了 JobSheduler API,并展示了如何使用它来安排在满足我们定义的一组条件时启动的后台工作。

我们详细学习了如何设置 API 支持的不同标准,以及如何根据 JobInfo 对象进行调度,该对象在设备充电且未使用时启动作业。

同时,我们学习了如何实现一个异步的JobService,它能够在后台执行行中运行,并通过释放所有获取的资源(WakeLock...)来正确完成作业执行。

之后,我们使用getAllPendingJobs创建了一个示例代码,用于列出我们应用程序中所有挂起的调度器 API 作业。从示例中,我们学习了如何在我们的应用程序中取消特定作业或所有挂起的作业。

最后,我们使用JobSheduler API 构建了一个持久和周期性的调度,它每隔 12 小时唤醒设备并执行我们的作业。

在下一章中,我们将学习如何使用有效的异步库和协议从网络传输数据,而不会耗尽电池。

第八章。与网络交互

到目前为止,我们一直在使用 HttpURLConnection HTTP 客户端来在网络之间传输数据,例如从 HTTP 服务器下载图像以及与远程 HTTP 服务器同步信息。我们一直在盲目地使用这个 Android HTTP 客户端,而没有深入了解其内部结构和提供的功能,这个框架为我们透明地处理 HTTP 协议。

在本章中,我们将深入了解 HttpURLConnection 的高级功能以及使用 HTTP 协议异步和安全地与远程服务器通信的新技术。

同时,我们将学习如何使用定制的 HTTP 客户端通过安全通道进行通信,调整 HTTP 客户端以处理网络延迟,以及如何与 Web API 交互。

在本章中,我们将涵盖以下内容:

  • 介绍 Android HTTP 客户端

  • 异步执行 HTTP 请求

  • 与 JSON Web API 交互

  • 与 XML Web API 交互

  • 优化 HTTP 超时

  • 通过 SSL 会话安全通信

  • HTTP 开源库

介绍 Android HTTP 客户端

近年来,从远程服务器发送和接收数据的能力已成为所有应用程序都应该强制执行的基本功能,以创建动态和令人印象深刻的体验。今天,几乎每个应用程序都使用网络来检索数据信息,执行远程业务逻辑操作,以及下载或上传资源。

应用程序与远程服务器之间的网络交互通常定义为一系列请求/响应消息,这些消息通过网络协议在网络中传输。

通常情况下,HTTP 协议常用于在各个对等体之间传输消息,Android SDK 提供了两个高级 HTTP 客户端,可以直接发送和接收数据:AndroidHttpClientHttpURLConnection

HTTP 通信协议是一个无状态的、基于文本的应用协议,由 互联网工程任务组IETF)和 万维网联盟W3C)维护,并在互联网上广泛用于在客户端(通常称为用户代理)和服务器之间交换数据。

该协议随着时间的推移进行了一些改进,但大多数服务器和客户端基于 HTTP 1.1 的实现,这是原始 HTTP 1.0 的修订版,它引入了连接重用功能和分块传输编码到原始协议中。

在典型的 HTTP 流程中,客户端,即启动操作的实体,通过连接向服务器发送请求并等待服务器的响应。在另一端,服务器从通信通道读取请求,处理请求,并将响应发送回客户端。在下一图中,我们可以可视化两个对等体之间交换的请求和响应示例:

介绍 Android HTTP 客户端

2015 年 5 月发布的新的 HTTP 修订版 2.0 尚未得到广泛采用,并且在 Android SDK 中没有官方支持。

在初步介绍 HTTP 协议之后,我们将尝试比较 Android SDK 上可用的 HTTP 客户端。

AndroidHttpClient

基于 Apache HTTP 客户端的AndroidHttpClient客户端库自 API 级别 9(姜饼)以来已被弃用,但它提供了一个大而灵活的 API,用于支持 cookie 管理、超时自定义、基本身份验证方案和安全通信通道的 HTTP 服务器访问。

在 API 级别 8(蜂巢)和 API 级别 7(闪电)上,此客户端比HttpURLConnection更稳定。

在 API 级别 23(棉花糖)中,由于缺乏透明的响应压缩和响应缓存,已移除对此客户端的支持,转而使用HttpURLConnection

HttpURLConnection

此客户端框架支持安全通信会话(HTTPS)、透明的响应压缩、响应缓存、网络超时自定义、网络连接轮询、IPv6 支持、代理服务器和流式传输。

根据 Google 的说法,在 API 级别 8(蜂巢)之前,此客户端存在一些重要问题,可能会破坏 HTTP 连接的重用。

注意

自 Android 4.4(奇巧)以来,此实现引擎基于开源的 OkHttp Square 库。

由于HttpURLConnection是 Google 为 API 级别 9 以上的 Android 版本推荐的 HTTP 客户端,因此我们将基于此 HTTP 客户端编写代码示例。然而,通过使用基于 API 级别的不同 Android HTTP 客户端,我们可以超越这种碎片化问题:

if (Build.VERSION.SDK_INT >= 9) {
    // After Gingerbread, use the google recommended
    // client, HttpUrlConnection
    ...
}  else {
    // Prior to Gingerbread, use the Apache based
    // client, AndroidHttpClient
    ...
}

在下一节中,我们将开始编写基于HttpURLConnection的 HTTP 异步工具包,并探索客户端提供的先进功能,以与远程对等方进行通信。

异步执行 HTTP 请求

到目前为止,我们一直在使用HttpURLConnection客户端和AsyncTask在我们的代码示例中异步检索远程数据。

虽然此解决方案在大多数情况下可以工作,但我们可能会在我们的应用程序中产生大量的重复代码。

在本节中,我们将创建一个整洁的高级异步 HTTP 客户端,以在主线程之外执行远程请求,并使用回调处理程序将请求的结果转发到应用程序的主线程。这种方法与应用程序 UI 模型很好地配合,因为执行在主线程上的回调处理程序能够使用从服务器检索的数据更新 UI。

首先,让我们定义我们的异步客户端应该遵守的基本接口,以在后台执行远程请求:

public interface AsyncHTTPClient {
    void execute(HTTPRequest request, ResponseHandler handler);
}

HTTPRequest类是一个 Java 模型,用于定义构建 HTTP 请求所需的所有参数。我们将省略一些实现细节,但借助Builder类,我们将能够定义 HTTP 请求动词、请求 URL、HTTP 头、HTTP 查询参数和 HTTP 正文:

public class HTTPRequest {

  final Verb mVerb;
  final String mUrl;
  final List<Header> mHeaders;
  final Map<String, String> mParameters;
  final Body mBody;

  private HTTPRequest(Builder builder) {...}
  }

ResponseHandler 是一个类,它定义了当服务器发送成功或失败响应,或者在操作执行过程中发生异常时将被调用的回调。因此,我们将定义一个抽象的 ResponseHandler 类供子类实现:

public abstract class ResponseHandler {

    // Method invoked when a success response is returned
    // 200 Response Code
    abstract public void onSuccess(HTTPResponse response) ;

    // Method invoked when a failure response is returned
    // 4XX, 50X 	
    abstract public void onFailure(HTTPResponse response) ;

    // Method Invoked when an error happens
    abstract public void onError(Throwable error);
}

当响应或错误准备好被发送到处理器时,所有回调方法都会自动转发到主线程。所有网络和输入/输出操作以及内存分配都必须在后台线程上完成,以避免任何 UI 不希望的暂停。

当服务器返回 HTTP 响应时,根据响应消息返回的代码,将调用以下方法之一,onSuccessOnFailure。因此,当任何回调被调用时,将传递一个 HTTPResponse 对象以进行进一步处理。

目前,HTTPResponse 类携带有关请求代码、响应头和响应体的信息:

public class HTTPResponse {

    final int mResponseCode;
    final List<Header> mHeaders;
    final Body mBody;

}

已经定义了基类和接口后,让我们在 HttpURLConnection 类的帮助下实现我们的异步高级客户端。

由于我们已经知道如何基于 AsyncTask 类构建后台处理管道,为了简单起见,我们将基于这个结构实现我们的实现。将来,你可以用 AsyncTaskLoader 替换 AsyncTask 以支持配置更改:

 public class HTTPAsyncTask extends
  AsyncTask<HTTPRequest, Void, Result<HTTPResponse>> {

  // Response Handler to be invoked In onPostExecute
  // on the UI Thread
  final ResponseHandler mHandler;

  // Handler is passed on the constructor
  public  HTTPAsyncTask(ResponseHandler handler) {
    this.mHandler = handler;
  }
  ...
}

如前述代码所示,我们的 AsyncTask 的输入参数类型是 HTTPRequest,因此将一个类型为 Result<HTTPResponse> 的对象发送到 UI 线程。结果是一个泛型类,如前几章所述,它能够携带错误或 HTTPResponse 对象。

已经定义了 HTTPAsyncTask 的泛型参数后,现在是我们重写 doInBackground 以在后台发送 HTTP 请求并处理响应的时候了:

@Override
protected Result<HTTPResponse> doInBackground(HTTPRequest... params) {

  HTTPRequest request = params[0];
  Body body = null;
  HttpURLConnection conn = null;
  Result<HTTPResponse> response = new Result<HTTPResponse>();
  try {

    // Retrieve the request URL from the request object
    URL url = new URL(request.mUrl);

    // Opens up the connection to the remote peer
    conn = (HttpURLConnection) url.openConnection();

    // Set the HTTP Request verb
    conn.setRequestMethod(request.mVerb);

    // set The HTTP Request Headers
    setRequestHeaders(conn,request);

    // Allows Receiving data on the response
    conn.setDoInput(true);

    // Retrieve the response code
    int responseCode = conn.getResponseCode();

    // Build the HTTP Response Object
    HTTPResponse.Builder builder = new HTTPResponse.Builder()
    .setResponseCode(responseCode);

    // Fill the HTTP Response Headers
    fillHeaders(conn.getHeaderFields(), builder);

    // Read the Body from the Connection Input Stream
    // using a factory
    body = BodyFactory.read(conn.getContentType(),
                              conn.getInputStream());
    // Set the HTTPResponse body
    builder.setBody(body);
    // Build the HTTP Response Object
    response.result = builder.build();
  } catch (Exception e) {
    response.error = e;
  } finally {
    if ( conn != null ) {
      conn.disconnect();
  }
  }
  return response;
  }

// Write any header to the Request Ex: Accept: text/xml
void setRequestHeaders(HttpURLConnection con, HTTPRequest request ) {
  for (Header header : request.mHeaders) {
    con.addRequestProperty(header.getName(), header.getValue());
  }
}

openConnection 将与 URL 对象中指定的资源建立 TCP 连接。一旦建立连接,并且我们尝试检索状态行响应代码时,我们的 HTTP 请求头和体被发送到网络。

一旦读取到状态行,我们就处理接收到的 HTTP 响应头,并将它们存储在我们的响应对象中以供进一步处理。你可能已经知道,HTTP 响应可能包含与请求 URL 上指定的资源相关的 HTTP 消息体数据。

为了进一步处理,数据将从连接的 InputStream 中消费,以构建一个 Body 对象。为了检测在 HTTP 响应中接收到的数据内容类型,客户端应该查看头 Content-Type 的内容。为了简化这种识别,HttpURLConnection 类提供了一个成员方法 getContentType(),它直接从头中检索内容。

体的消费和构建是在以下代码中显示的 BodyFactory 类上完成的:

public class BodyFactory {

  public static Body read(String mimeType,
                          InputStream is) throws IOException {
    Body result = null;
    if ( mimeType.startsWith("text") ) {
      result = new TextPlainBody(mimeType);
      result.consume(is);
    }    
    return result;
  }
}

由于我们知道内容类型,我们准备好消费体并存储在 Body 对象上接收的字节,以便由我们的 ResponseHandler 进行进一步处理。

Body 是一个抽象类,它本身无法读取任何类型的内容,尽管我们可以直接扩展 Body 类来从接收到的数据构建文本体。

我们的 Body 子类,称为 TextPlainBody,将实现抽象的 consume 函数,以便从 InputStream 构建体。

abstract void consume(InputStream is)
  throws IOException;

为了简洁起见,省略了 TextPlainBody 的消费代码,尽管完整的源代码可以从 Packt Publishing 网站下载。查看 TextPlainBody 的源代码,以了解我们如何使用 InputStream 构建字符串。

目前,我们只支持 text/* 类型;然而,在接下来的章节中,我们将扩展 BodyFactory 类以支持其他有趣的 MIME 类型,例如 JSON 文档。

一旦完全读取响应体,与远程服务器的连接将被关闭,并且释放连接持有的资源。

注意

连接不会立即被销毁,而是推送到连接池以供将来使用。在池中空闲一定时间(idleTimeout)后,连接将被销毁。

doInBackground 执行期间,可能会出现网络或输入/输出异常,为了避免应用程序崩溃,我们必须捕获并将它们通过 result.error 传递给 postExecute 函数,然后传递给在执行开始时指定的 ResponseHandler

从 AsyncTask 的方法中传播的未检查异常将使我们的应用程序崩溃,因此我们必须仔细测试并在必要时处理这些异常。

为了使我们的 AsyncTask 子类有用,我们必须编写 onPostExecute 函数,将响应或错误转发到 ResponseHandler 对象:

protected void onPostExecute(Result<HTTPResponse> result) {

  if ( result.error != null ) {
    mHandler.onError(result.error);
  } else if ( result.obj.mResponseCode ==
              HttpURLConnection.HTTP_OK ) {
    mHandler.onSuccess(result.obj);
  } else {
    mHandler.onFailure(result.obj);
  }
}

注意

如前所述,在 第三章 中,探索 AsyncTaskonPostExecute 回调将在主线程上执行,因此你应该避免在以下回调中进行任何耗时操作:onErroronSucessonFailure

剩下的只是调用我们的 AsyncTask,来自我们的 AsyncHTTPClient 子类—PacktAsyncHTTPClient

public class PacktAsyncHTTPClient implements AsyncHTTPClient {

  @Override
  public void execute( HTTPRequest request,
                       ResponseHandler handler) {
    // Execute the HTTP Request on the default AsyncTask Executor
    new HTTPAsyncTask(handler).execute(request);
  }
  ...
 }

太好了!现在我们有一个支持文本 MIME 类型的核心异步 HTTP 客户端实现。在下一节中,我们将使用我们的高级客户端从远程服务器检索文本消息。

获取文本响应

使用支持文本响应的异步 HTTP 客户端,我们能够利用它来获取动态文本,因此让我们创建一个活动来显示远程 URL 资源上可用的文本。

首先,我们必须使用 HTTPRequest.Builder 类构建我们的 HTTPRequest

protected void onCreate(Bundle savedInstanceState) {
   ...
  HTTPRequest.Builder builder = new HTTPRequest.Builder();
  // Set the HTTP Verb to GET
  builder.setVerb(HTTPRequest.Verb.GET);
  // Sets location of the remote resource
  builder.setUrl("http://<hostname>:<port>/greetings");
  // Build the request object
  HTTPRequest request =  builder.build();
  ...
}

我们希望引起您的注意,您应该将 <hostname><port> 替换为使其适用于您的 HTTP 服务器。

如前所述,为了在我们的异步客户端AsyncHTTPClient上执行请求,我们必须向execute方法提供一个ResponseHandler对象。此外,我们还想定义一个对象,该对象使用获取到的文本更新 UI。

首先,我们将扩展我们的ResponseHandler抽象类,创建一个类来处理体内容,并将响应转发给接收文本消息作为输入的回调。

public abstract class TextResponseHandler 
  extends ResponseHandler {
   // Response Callback receiving the string body
    abstract void onSuccess(String response);

    @Override
    public void onSuccess(HTTPResponse response) {
        TextPlainBody body = (TextPlainBody)response.mBody;
        // Invoke the callback that receives a string
        onSuccess(body.getContent());
    }
}

接下来,我们需要完成我们的Activity代码以发送 HTTP 请求,同时我们还将显示一个进度对话框,让用户意识到后台正在进行某些操作:

HTTPRequest request =  builder.build();
// Create a client Instance object
PacktAsyncHTTPClient client = new PacktAsyncHTTPClient();
// Enable a progress bar
ProgressBar pb =(ProgressBar) findViewById(R.id.loading);
pb.setVisibility(View.VISIBLE);
// Retrieve the response on the background
client.execute(request,textResponseHandler);
...

现在,我们只需要定义我们的textResponseHandler,这是一个实现了TextReponseHandler的匿名内部类,它更新View以显示接收到的字符串,并关闭已启用的不确定进度对话框:

TextResponseHandler greetingsHandler = new TextResponseHandler() {

  // Invoked when request was processed with success by the server
  @Override
  void onSuccess(String response) {

    // Update the View with the String received on the 
    // HTTP body
    EditText et = (EditText) findViewById(R.id.inputText);
    et.setText(response);
    dismissProgress();
  }

  // Invoked when the served returned an error
  @Override
  public void onFailure(HTTPResponse response) {
    Log.e("GreetingsActivity", "Server returned an error: " + 
          response.mResponseCode + " " + 
          response.mResponseMessage);
    dismissProgress();
  }

  // Invoked when an error happened
  @Override
  public void onError(Throwable error) {
    Log.e("GreetingsActivity", "Exception happened: " + 
           error.getMessage(), error);
    dismissProgress();
  }  
};

注意,所有回调函数都将在主线程上执行,因此在这些函数中,你应该遵循黄金法则,不要阻塞线程,以避免 UI 延迟。

当服务器返回错误或在执行过程中发生异常时,分别调用以下回调方法,onFailureOnError

从远程服务器读取文本资源支持是一个良好的起点,尽管在大多数情况下,我们打算与远程服务器通信以交换结构化文档格式,例如 JSON、XML,甚至是二进制数据。

与 JSON Web API 交互

我们之前的TextResponseHandler能够处理填充有文本响应的通用响应HTTPResponse,并将来自 JSON Web API 的字符串转发给TextResponseHandler onReceive(String)回调。

现在,我们想更进一步,将doInBackground返回的HTTPResponse转换为表示我们业务逻辑中模型的普通 Java 对象POJO)。为了实现这一点,我们必须将 HTTP 体上返回的 JSON 结构化内容转换为之前定义的 Java 对象。

为了将处理转发到上面定义的回调,我们将创建JSONResponseHandler,它是ResponseHandler的抽象子类,实现了onSuccess(HTTPResponse)onFailure(HTTPResponse),并将对象分别转换为<ResponseType><ErrorType>

public abstract class JsonResponseHandler<ResponseType, ErrorType> extends ResponseHandler {

  abstract public void onSuccess(ResponseType response);
  abstract public void onFailure(ErrorType response);
  ...
}

为了支持不同体内容的处理,我们的BodyFactory需要更新。因此,在我们继续到JSONResponseHandler实现之前,我们将更新我们的BodyFactory函数以支持application/json媒体类型:

public class BodyFactory {

  public static Body read(String mimeType,
                         InputStream is)
    throws IOException {

    ...
    } else if ( mimeType.startsWith("application/json")){
      result = new RawBody(mimeType);
      result.consume(is);
    }  
    return result;
  }
}

注意,我们从网络读取内容,并将数据存储在一个名为RawBody的新Body类中,该类简单地存储来自 HTTP 响应体的内容,在一个内部字节数据缓冲区中。

注意

存储整个体可能适用于我们的简单客户端用例。尽管如此,如果我们愿意处理具有兆字节数据的体,我们必须使用另一种策略来读取和消费体,分块消费体,或将体保存到本地文件系统中。

在设备内存中已经存储了带有 JSON Web API 的体之后,我们准备好使用我们的JSONResponseHandler来处理它。

将 Java 对象转换为 JSON

为了将 JSON 文档格式转换为普通对象,我们将使用一个在 Android 社区中非常著名的开源库——Google GSON 库:github.com/google/gson

GSON 库,这是一个由 Google 开发和维护的库,能够将 Java 对象转换为 JSON 对象,反之亦然。

因此,在你继续之前,请确保将库添加到你的 Gradle 或 Eclipse 项目中:

compile 'com.google.code.gson:gson:2.5'

为了将协议编码的内容体转换为 POJO,我们将定义BodyDecoder接口:

public interface BodyDecoder <T> {
    T decode(Body body) throws Exception ;
}

为了将 POJO 转换为Body对象,我们将定义BodyEncoder接口:

public interface BodyEncoder<T> {
    Body encode(T obj,String mimeType) throws Exception;
}

要将 JSON 文档解码为 POJO,我们必须继承这个泛型接口,并编写代码来使用 GSON 库处理 JSON 解码:

public class JSONConverter<POJO> 
  implements BodyEncoder<POJO>, BodyDecoder<POJO> {
  // Store the Generic Type information
  private final Type pojoType;

  JSONConverter(Type pojoType) {
    this.pojoType = pojoType;
  }

  @Override
  public POJO decode(Body body) throws Exception {
    Gson gson = new Gson();
    RawBody rawBody = (RawBody) body;
    InputStream is = null;
    POJO obj = null;
    try {
      is = new ByteArrayInputStream(rawBody.getContent());
      BufferedReader bfReader = 
        new BufferedReader(new InputStreamReader(is));
      obj = gson.fromJson(bfReader, pojoType);
    } finally {
      if (is != null) is.close();
    }
    return obj;
  }
}

最后,我们准备好实现我们的泛型JSONResponseHandler,它将onSuccessonFailure方法返回的转换后的 JSON 结构文档转发到泛型类型ResponseError

下面是JsonResponseHandler代码的示例:

public abstract class JsonResponseHandler<ResponseType, ErrorType>
  extends ResponseHandler {
   // Store the Response Type class information
    private final Type responseType;
   // Store the Error Type class information
    private final Type errorType;

    JsonResponseHandler(Type responseType, Type errorType) {
        this.responseType = responseType;
        this.errorType = errorType;
    }

  // Callback invoked on the main Thread that converts
  // a body to a POJO object
  @Override
  public void onSuccess(HTTPResponse response) {
    RawBody body = (RawBody)response.mBody;

    if ( body != null ) {
      Response obj = null;
      try {
        obj = new JSONConverter<Response>(responseType).
                decode(body);
        onSuccess(obj); 
      } catch (Exception e) {
        onError(e);
      }

    } else {
      onSuccess((Response)null);
    }
  }
  @Override
  public void onFailure(HTTPResponse response) {...}
}

这个泛型类能够消费从onSuccessonFailure传递过来的RawBody对象,并暴露两个泛型类型参数。第一个Response是一个泛型类型,它指定了成功响应(onSuccess)的 POJO 类类型,而Error泛型类型指定了错误函数(onFailure)的 POJO 类类型。

当任何接收HTTPResponse的回调函数被调用时,借助 GSON 库,我们将存储在RawBody上的 JSON 文档转换为响应/错误对象。

onFailure函数被省略了,因为它的代码与onSuccess方法非常相似;然而,你可以在 Packt Publishing 网站上查看完整的源代码以了解差异。

最后,我们准备好通过从 HTTP JSON 体中读取一个模型来测试我们的异步客户端:

  Content-Type: application/json

   [
     {
      "userId": 1,
      "id": 1,
      "title": "..",
      "body": "..."
     },...
   ]

为了测试我们的客户端,我们将利用JSONPlaceHolderjsonplaceholder.typicode.com/),一个用于测试和原型设计的假在线 REST API。

在我们尝试访问它之前,让我们定义我们将用于示例的User POJO 类:

public class User {
  public int id;
  public String name;
  public String username;
  public String email;
  public String phone;
  public String website;
}

然后,我们将为failure函数定义一个 POJO 类:

public class Error {
  public int resultCode;
  public String resultMessage;
}

为了显示从模拟 API 返回的模型,我们将创建一个新的UserListActivity,该活动将实现JSONResponseHandler并显示所有用户的姓名和电子邮件,这些信息由从jsonplaceholder.typicode.com/users返回的 JSON 文档提供。

让我们先定义一个JsonResponseHandler<List<User>,Error>匿名类,该类接收一个用户列表作为成功回调的参数List<User>,以及一个Error对象作为失败回调的参数。

onSuccess(List<User>)将更新一个显示姓名后跟电子邮件的ListView

JsonResponseHandler<List<User>, Error> jsonResponseHandler =
  new JsonResponseHandler<List<User>, Error>(
    new TypeToken<ArrayList<User>>() {} .getType(),
    new TypeToken<Error>() {} .getType()) {
      // On Success Callback
      @Override
      public void onSuccess(List<User> users) {
        // Update the List View with the a List Adapter that displays
        // the user name and email per user
        ListView listView = (ListView) findViewById(R.id.usersList);
        ListAdapter adapter = new UserListAdapter(
          UserListActivity.this, users);
        listView.setAdapter(adapter);
      }
      // Prints the Error object
      @Override
      public void onFailure(Error response) {
        Log.e("UserListActivity",
              "Error happened retrieving greetings " +
              response.toString());
      }
      @Override
      public void onError(Throwable error) {
      // Do Something with the exception
      }
    };

剩下的就是编写 Activity 的onCreate函数,该函数构建 HTTP 请求并将请求 GET /users 分派给JSONPlaceHolder API:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.user_list_layout);

  HTTPRequest.Builder builder = new HTTPRequest.Builder();
  // Set the HTTP Verb to GET
  builder.setVerb(HTTPRequest.Verb.GET);
  // Sets location of the remote resource
  builder.setUrl("http://jsonplaceholder.typicode.com/users");

  // Notify the server that the client is able to receive json
  // documents
  builder.addHeader(new Header("Accept","application/json"));
  HTTPRequest request = builder.build();

  PacktAsyncHTTPClient client = new PacktAsyncHTTPClient();
  client.execute(request, jsonResponseHandler);
}

当服务器向我们请求发送响应时,JSONResponseHandler对象将解码包含 10 个用户的 JSON 文档,并将文档转换为List<User>对象。

JSON 数据交换格式因其简单性、可读性、紧凑性和缺乏数据结构刚性,在互联网上部署的大多数最近 API 和 Web 服务中得到了广泛使用。

然而,基于 XML 的远程 Web 服务,主要基于 SOA 数据交换协议,仍然存在,以覆盖我们想要有更严格的数据模型模式验证和建模、内置命名空间支持和高级信息提取工具的高级用例。

尽管基于 XML 的 API 仍然很常见,但基于 XML 的 API 在行业中已经失去了一些吸引力,因此在下一段中,我们将扩展我们的工具集以支持在 HTTP 请求上对 XML 内容的编码和解码。

与 XML Web API 交互

在过去的 10 年里,基于 XML 的消息 API 已经成功用于在应用程序和远程服务器之间交换数据(SOAP)和运行远程过程(XML-RPC)。此外,如今,一些 REST API 使用 JSON 和 XML 支持构建,因此开发者需要决定他们是否想使用 XML 或 JSON 文档与 API 交互。

在这个想法的指导下,我们将扩展我们的工具集以交换 XML 消息,以便与远程服务器通信。

在前面的章节中,我们实现了一个ResponseHandler来解码 HTTP 主体上发送的 JSON 文档,但现在我们想要更进一步,添加对在 HTTP 请求中发送 XML 文档的支持。

如果我们回顾一下HTTPAsyncTask的细节,支持请求体传输所需的代码缺失;因此,这是重写代码以携带 HTTP 请求实体体的完美时机:

@Override
protected Result<HTTPResponse> doInBackground(HTTPRequest... params) {
  ...
  if ( request.mBody != null ) {
    // Allows Sending data on the request
    conn.setDoOutput(true);
    // Specifies The Content-Type based on the Body Mime Type
    conn.setRequestProperty(
      "Content-type", request.mBody.getMimeType());
    // Retrieves the connection stream to write content
    OutputStream os = conn.getOutputStream();
    request.mBody.write(os);
  }
  // Retrieve the response code
  int responseCode = conn.getResponseCode();
  ...
}

在前面的代码中,当HTTPRequest对象上有可用体时,在我们尝试读取响应状态行的responseCode之前,我们从HttpURLConnection检索输出流,并将存储在Body对象上的内容写入。

除了写入的数据外,Context-Type 标头根据正文的多媒体类型设置在 HTTP 请求头部分。

现在,我们已经准备好在 HTTP 请求和响应中发送和接收实体体,因此现在我们可以开始编写我们的 XML 实现示例。

在接下来的段落中,并遵循 JSON 部分,我们将编写将 API 请求序列化为 XML 文档的代码,以及将接收到的 XML 文档反序列化为 API 响应对象的代码。

将 Java 对象转换为 XML

要从 Java 对象反序列化和序列化到 XML 以及反之亦然,我们将使用开源库 SimpleXML (simple.sourceforge.net/);因此,为了使用它,请将此库添加到您的应用程序外部依赖项列表中。

如果您使用 Android Studio,以下是在您的 build.gradle 依赖项列表中需要添加的内容:

dependencies {
  ...
  compile('org.simpleframework:simple-xml:2.7.+'){
        exclude module: 'stax'
        exclude module: 'stax-api'
        exclude module: 'xpp3'
  }
}

在定义了 User 模型之后,我们将构建一个名为 GetUserInfoAPIRequest,它将获取请求中提交的用户标识符的用户详细信息(GetUserInfoResponse)。

为了进一步描述,让我们定义 API 请求 POJO 对象:

@Root(name = "GetUserInfo")
@Namespace(prefix="p", 
  reference="https://www.packtpub.com/asynchronous_android")
public class GetUserInfo {

    @Namespace(reference=
      "https://www.packtpub.com/asynchronous_android")
    @Element(name="UserId")
    public String userId;
         // … Setters and Getters
}

让我们定义 API 响应 POJO 对象:

@Root
@Namespace(prefix = "p",
  reference = "https://www.packtpub.com/asynchronous_android")
public class GetUserInfoResponse {

  @Element(name = "User")
  @Namespace(prefix = "p", 
    reference = "https://www.packtpub.com/asynchronous_android" )
  private User user = new User();
  // … Setters and Getters
}

对于其他类,如 UserAddressCompany 的注解与上面类似,所有元素都引用了 https://www.packtpub.com/asynchronous_android 命名空间,因此为了简洁,我们将省略 POJO 的更改。

在完成 Java 对象与 XML 请求和响应消息之间的映射后,让我们开始编写将 GetUserInfo 对象转换为 XML 文档的代码。

要将 Java 对象编码为 XML 文档,我们将通过 SimpleXML 的帮助,将这个通用接口子类化,以将 POJO 通用类型编码到 XML 文档中:

public class XMLConverter<POJO>
  implements Encoder<POJO>{

  private final Class<POJO> clazz;
  XMLConverter(Class<POJO> clazz){ this.clazz = clazz; }

  @Override
  public Body encode(POJO obj, String mimeType)
    throws Exception {

    // Creates SimpleXML Serializer Instance
    Serializer serializer = new Persister();
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    // Converts from obj -> xml document
    serializer.write(obj, output);

    // Build a RawBody body
    RawBody body = new RawBody(mimeType);
    output.close();

    // Stores the result on the body
    body.setContent(output.toByteArray());
    return body;
  }
}

将 XML 转换为 Java 对象

要将 XML 文档解码为 Java POJO 对象,我们将向 XMLConverter 类定义接口中添加 BodyDecoder<POJO>,并编写使用 SimpleXML 库将 XML 文档转换为 POJO 通用类的代码:

public class XMLConverter<POJO>
  implements BodyEncoder<POJO>, BodyDecoder<POJO> {

  ...
  @Override
  public POJO decode(Body body) throws Exception {
    // Instantiate a SimpleXML Serializer
    Serializer serializer = new Persister();
    InputStream is = null;
    RawBody rawBody = (RawBody)body;
    POJO obj = null;
    try {
      is = new ByteArrayInputStream(rawBody.getContent());
      BufferedReader bfReader =
        new BufferedReader(new InputStreamReader(is));
      obj = (POJO) serializer.read(clazz, bfReader);
    } finally {
      if ( is != null ) is.close();
    }
    return obj;
  }
  ...
}

在 XML 序列化和反序列化已经就绪的情况下,让我们开始编写活动,该活动将使用上面定义的 GetUserInfoGetUserInfoResponse 从远程服务器检索用户信息,并在 UI 上显示用户信息。

为了测试我们的客户端,我们将使用可模拟的 www.mockable.io/ 网络应用程序创建一个假 XML WebService。这个网络应用程序允许我们创建可配置的 REST API,它返回静态 JSON 或 XML 文档作为对客户端的响应,因此这个工具将非常有助于在受控环境中测试我们的客户端。

当向以下 URI 发送任何 HTTP 请求时,我们的假 HTTP API 将返回以下 XML 文档,demo1472539.mockable.io/GetUserInfo

<?xml version="1.0"  ?>
<p:GetUserInfoResponse
   >
 <p:User>
   <p:Id>12</p:Id>
   <p:Name>John</p:Name>
   <p:Username>John</p:Username>
   ...
   <p:Company>
        <p:Name>Packt</p:Name>
        ...
    </p:Company>
  </p:User>
  </p:GetUserInfoResponse>

为了将onSuccess(HTTPRequest)onFailure(HTTPResponse)回调接收到的HTTPResponse对象转换为领域模型,我们必须子类化ResponseHandler抽象类并创建反序列化通用 POJO 类的代码:

public abstract class XMLResponseHandler
   <Response, Error> extends ResponseHandler {

  // Class used to by Simple to convert to the ResponseTYpe
  private final Class<Response> responseClass;

  private final Class<Error> errorClass;

  XMLResponseHandler(Class<Response> responseClass,
                     Class<Error> errorClass) {
    this.responseClass = responseClass;
    this.errorClass = errorClass;
  }

  // Callback invoked with the converted Response  object instance
  abstract public void onSuccess(Response response);

  // Callback invoked with the converted Error object instance
  abstract public void onFailure(Error response);

  // Convert the body to a POJO object using the our converter
  @Override
  public void onSuccess(HTTPResponse response) {
    RawBody body = (RawBody)response.mBody;

    if ( body != null ) {
      Response obj = null;
      try {
        obj = new XMLConverter<Response>(responseClass)
        decode(body);
        onSuccess(obj);
      } catch (Exception e) {
        onError(e);
      }
    } else { onSuccess((Response)null); }
  }
 //.. Failure elided for brevity
}

现在,我们已经准备好收集从我们的模拟服务器发出的GetUserInfoResponse,因此让我们在我们的Activity中实现一个匿名内部XMLResponseHandler子类,该子类将使用用户详细信息更新 UI:

XMLResponseHandler<GetUserInfoResponse, Error> xmlResponseHandler
  = new XMLResponseHandler<GetUserInfoResponse,Error>(
      GetUserInfoResponse.class, Error.class) {

  // Updates the UI with the user details
  @Override
  public void onSuccess(GetUserInfoResponse getUserInfoResponse) {
    TextView nameTv = (TextView)findViewById(R.id.nameValue);
    TextView emailTv = (TextView)findViewById(R.id.emailValue);

    nameTv.setText(getUserInfoResponse.getUser().name);
    emailTv.setText(getUserInfoResponse.getUser().email);

  }
  @Override
  public void onFailure(Error response) {
    // Do Something with the Error 
  }
  @Override
  public void onError(Throwable error) {
    // Do Something with the Throwable
  }
};

为了完成我们的Activity,我们需要构建请求,并使用HTTPAsyncTask类帮助我们的异步客户端在后台执行我们的需求:

HTTPRequest.Builder builder = new HTTPRequest.Builder();
// Set the HTTP POST Verb
builder.setVerb(HTTPRequest.Verb.POST);

// Set location of the remote resource
builder.setUrl("http://demo1472539.mockable.io/GetUserInfo");

// Tell the Server that you are able to consume 
// application/xml contents on the response
builder.addHeader(new Header("Accept", "application/xml"));

// Build the Request Body object
GetUserInfo query = new GetUserInfo();
query.setUserId("123");
try {
  // Encode the POJO into a XML Document
  Body body = new XMLConverter<GetUserInfo>(GetUserInfo.class)
                .encode(query, "application/xml");
  builder.setBody(body);
} catch (Exception e) {
  // Catch and display and error to the user
  ...
}
PacktAsyncHTTPClient client = new PacktAsyncHTTPClient();
client.execute(builder.build(), xmlResponseHandler);

一旦从模拟服务器返回响应,UI 将根据模拟端点指定的 XML 更新,以显示用户名、电子邮件和其他属性。

关于错误流程没有提及;然而,我将挑战你创建一个返回错误对象的模拟端点,以练习onError回调。带有错误流程的完整源代码可以从 Packt Publishing 网站下载。看看它,以欣赏错误处理是如何实现的。错误 XML 文档可能类似于以下内容:

<?xml version="1.0"?>
<p:Error   
   >
 <p:ResultCode>1000</p:ResultCode>
 <p:ResultMessage>The LDAP Server is down</p:ResultMessage>
</p:Error>

到目前为止,我们已经涵盖了在 HTTP 客户端和服务器之间交换数据时在行业中使用的最常见格式(文本、XML 和 JSON)。每种格式都有其自身的优势;然而,由于其紧凑性和简单性(易于解析、语法等),JSON 格式已被 API 设计者、电子产品制造商和软件即服务(SaaS)供应商广泛采用。

在此过程中,我们将构建一个核心框架,它可以很容易地扩展以支持不同的数据交换格式,如 YAML 或二进制协议。因此,你可能需要编写自己的BodyEncoderBodyDecoderResponseHandler以适应标准数据格式,甚至是你自己的数据格式。

在本节中,我们将向读者介绍在用于处理请求执行中网络延迟的HttpUrlConnection上可用的超时设置。

自定义 HTTP 超时

HttpUrlConnection通过低带宽网络(2G、3G 等)连接、读取或写入内容时,不可避免地会暴露于不可预测的通信延迟。此外,除了移动网络延迟外,当 HTTP 服务器在高流量情况下,可能会引入显著的服务延迟(服务器延迟)。

虽然HttpUrlConnection使用的默认超时值已经足够长,可以应对这些延迟,但在某些特殊情况下,你可能需要根据你的需求自定义默认值。例如,当前往应用服务器时,HTTP 请求会经过一些代理。

HttpUrlConnection提供了两个成员方法,可以用来更改默认的超时时间:

void setConnectTimeout(int timeoutMillis)
void setReadTimeout(int timeoutMillis)

setConnectTimeout(int)能够重新定义客户端被允许等待的最大时间(以毫秒为单位),直到与远程主机的 TCP 连接建立(服务器关闭)。每当连接失败时,例如,如果服务器关闭或由于资源不足无法及时响应,将抛出ConnectTimeoutException,因此你应该小心处理这个异常,并在你的代码中干净地处理它。

当主机名解析为多个地址时,客户端将依次尝试连接,超时将适用多次。如果超时设置为0,连接将被阻塞,直到 TCP 超时到期,在 Android 上这通常是几分钟。

setReadTimeout(int)定义了客户端被允许在读取操作上阻塞的最大时间,直到允许读取任何可用数据。默认值0将无限期地阻塞读取操作,直到数据变得可用、远程对等方断开连接或套接字上发生错误。当超时时间大于0且超时到期时没有数据可用,将抛出SocketTimeoutException

现在我们已经理解了每个超时时间的含义,让我们更新我们的HTTPRequest.Builder,添加一些新的设置方法:

public class HTTPRequest {
  ...
  public static class Builder {

    private int mReadTimeout = 0; // Default Value
    private int mConnectTimeout = 0; // Default Value

    public void setConnectTimeout(int connectTimeoutMs) {
      this.mConnectTimeout = connectTimeoutMs;
    }
    public void setReadTimeout(int readTimeoutMs) {
      this.mReadTimeout = readTimeoutMs;
    }
  }
}

最后,我们必须更新HTTPAsyncTask,以设置在HTTPRequest上指定的超时,在用于连接远程服务器的HttpURLConnection对象上:

public class HTTPAsyncTask extends 
  AsyncTask<HTTPRequest, Void, Result<HTTPResponse>> {
   @Override
   protected Result<HTTPResponse> doInBackground(
    HTTPRequest... params) {    
      conn = (HttpURLConnection) url.openConnection();
      conn.setReadTimeout(request.mReadTimeout);
      conn.setConnectTimeout(request.mConnectTimeout);
      ...
  }
}

强烈建议你在几个网络延迟级别下测试你自定义的超时设置,并考虑到当服务器负载时,它可能延迟响应几秒钟的时间。

典型的网络延迟可能在 LTE 移动网络上从 80-120 毫秒到 GPRS 移动网络上的 150-550 毫秒不等。

当你在互联网上运行应用程序并需要处理敏感数据,例如个人信息、支付信息或商业文件时,使用一层安全层来保护你的通信渠道,以隐藏交换的数据免受外部攻击者或避免任何欺骗攻击,这一点非常重要。在下一节中,我们将扩展我们的高级客户端以支持到远程服务器的安全 SSL 连接通道。

在 SSL 会话中安全通信

到目前为止,我们一直使用普通连接与远程 HTTP 服务器进行通信。尽管这些类型的连接在交换的数据不敏感时可能符合你的应用程序需求,但有些情况下我们必须使用安全通道来发送或接收数据,以防止第三方读取或更改网络上交换的数据。

为了与远程服务器设置 SSL 会话,我们的客户端在加密工具的帮助下将创建一个加密通信通道,其中所有数据都使用在安全连接握手期间交换的秘密密钥进行加密的对称加密。除此之外,使用先前交换的秘密密钥接收和加密的内容将与其他对等公钥进行验证,以证明数据是从正确的来源发送和签名的。

在建立连接的过程中,作为 SSL 握手的一部分,服务器必须证明它持有受信任证书的私钥。受信任的证书是指可以从我们的受信任证书颁发机构CAs)列表中获取的证书,或者它是由您的受信任 CA 之一签发的。

注意

Android 平台自带一组知名受信任的 CA 列表,帮助我们确保大多数互联网上服务器的身份。

因此,当我们联系由知名 CA 签发的证书的服务器时,之前用于创建 HTTPS 连接的代码不需要任何更改:

URL url = new URL("https://packtpub.com");
con = (HttpsURLConnection) url.openConnection();

此代码示例将创建一个使用 Android 平台默认加密加密套件的 SSL 连接,并验证服务器是否与平台 CA 相匹配。

尽管这在大多数情况下都能工作,但可能需要使用特定的 CA、一组受信任的证书,或者仅使用最安全的加密套件(TLSv1)。在开发者必须构建自己的SSLContext并指定自己的TrustManagersKeyManagers以成功连接到远程服务器的情况下。

在接下来的段落中,我们将扩展我们的客户端以使用存储在 Java 密钥库中的私钥和 CA,以便验证和与由我们管理的证书颁发机构CA)管理的服务器进行通信。

首先,我们将创建一个SSLOptions类,在该类中我们定义了在 SSL 会话中想要使用的加密套件:

public class SSLOptions {

  enum CipherSuite {
    DEFAULT("Default"), // Supported on Android API Level > 9
    SSL("SSL"), // Supported on Android API Level > 9
    SSLv3("SSLv3"), // Supported on Android API Level > 9
    TLS("TLS"), // Supported on Android API Level > 1
    TLSv1("TLSv1"), // Supported on Android API Level > 1+
    TLSv1_1("TLSv1.1"), // Supported on Android API Level > 16+
    TLSv1_2("TLSv1.2"); // Supported on Android API Level > 16+
 ...
  }
  // Cipher Suite used on the connection
  final CipherSuite cipherSuite; 
  final SSLContext sslContext;

  public SSLOptions(Context ctx, Builder builder)
    throws Exception {
    ...
    // Build up our own SSLContext
    sslContext = SSLContext.
    getInstance(cipherSuite.toString());
 // Will use the Android default KeyManager and TrustManager
    sslContext.init(null,null,new SecureRandom());
  }  

  public static class Builder {   
    private CipherSuite cipherSuite = CipherSuite.DEFAULT;
    ...         
    SSLOptions build(Context ctx) throws Exception {
      return new SSLOptions(ctx, this);
    }
  }
}

通过前面的代码,我们能够控制SSLContext使用的加密套件;然而,为了使用我们自己的私钥、证书和受信任的 CA,我们必须使用自己的TrustManagerKeyManager初始化SSLContext

void init(KeyManager[] km, TrustManager[] tm, SecureRandom sr)

为了简单起见,我们将从存储在应用程序资源目录中的密钥库文件中加载我们的KeyManagersTrustManager。Java 密钥库可在 Packt Publishing 网站上找到。因此,在我们继续之前,请从 Packt 网站下载asynchronous_client.ks文件,其中包含一个可用的私钥、一个证书以及作为受信任权威机构签发证书的自定义 CA 证书。

注意

注意,您可以构建自己的TrustManagerKeyManager自定义子类,可以从不同的来源加载私钥和证书,但为了简单起见,我们将从文件中加载它们。

让我们借助 keytool 应用程序查看我们的 asynchronous_client.ks java 密钥库文件。

在命令行中,请运行以下命令以列出密钥库的内容:

keytool -list -v -keystore asynchronous_client.ks -storetype BKS provider org.bouncycastle.jce.provider.BouncyCastleProvider providerpath bcprov-jdk15on-146.jar

Alias name: asynchronous_client
Entry type: PrivateKeyEntry
Certificate[1]:
Owner: C=UK,ST=Birmingham,L=Birmingham,O=Packt Publishing,OU=Packt Publishing,CN=asynchronous_client
Issuer: C=UK,…,CN=packt
Certificate[2]:
Owner: C=UK,…,CN=packt

Alias name: ca
Entry type: trustedCertEntry
Owner: C=UK,…,CN=packt
Issuer: C=UK,…,CN=packt

我们的自定义密钥库文件,它将作为受信任的存储文件和密钥库文件,包含一个名为 asynchronous_client 的公钥和私钥以及一个名为 ca 的受信任 CA。keytool 需要 Bouncy Castle 提供者 JAR 来读取文件内容,因此,请在此之前从 Packt 网站下载该文件。

注意,由于我们有 C=UKST=BirminghamL=BirminghamO=Packt PublishingCN=packt 作为受信任 CA,我们将允许我们的 HTTP 连接到由该机构签署的 SSL 端点或所有中间证书都值得信赖的证书(一个受信任的证书链)。

注意

每个 Android 设备都预装了一个受信任证书列表,这些证书可以用来验证一个安全的远程对等实体。

既然我们已经了解了密钥库和受信任存储库的详细信息,让我们更新我们的 SSL 选项以从 asynchronous_client.ks 加载 KeyManagerTrustManager

public class SSLOptions {
  final CipherSuite cipherSuite; 
  final SSLContext sslContext;
  private final  String keyStore;
  private final String keyStorePassword;
  private final String trustStore;
  private final String trustStorePassword;
  ...

  public SSLOptions(Context ctx, Builder builder)
    throws Exception {
    ...
    sslContext = initSSLContext(ctx);
  }
  // Initialize the SSLContect with loaded 
  private SSLContext initSSLContext(Context ctx)
    throws Exception {

    KeyManagerFactory kmf = getKeyManagerFactory(ctx);
    TrustManagerFactory tmf = getTrustManagerFactory(ctx);
    // Use the cipher suite defined SSL, SSLv3 , TLS, TLSv1,
    SSLContext result = SSLContext.getInstance(
                          cipherSuite.toString());

    result.init( kmf != null ? kmf.getKeyManagers() : null ,
                 tmf != null ? tmf.getTrustManagers() : null,
                 new SecureRandom());
    return result;
  }
}

注意,我们使用 KeyManagerFactoryTrustManagerFactory 返回的 Keymanager 列表和 TrustManager 列表初始化 SSLContext。因此,下一步是编写代码成员方法来获取我们的工厂。所以,让我们从 getKeyManagerFactory 开始:

KeyManagerFactory getKeyManagerFactory(Context ctx)
  throws Exception {
  KeyManagerFactory kmf = null;
  // Initialize Key store
  if ( keyStore != null ) {
    // Load the file keystore from the assets directory
    InputStream keyStoreIs = ctx.getResources().
                              getAssets().open(keyStore);
    String algorithm = KeyManagerFactory.getDefaultAlgorithm();
    kmf = KeyManagerFactory.getInstance(algorithm);

    // Create BouncyCastle Key Store 
    KeyStore ks = KeyStore.getInstance("BKS");

    // Load the Keymanagers available on the file using
    // a password
    ks.load(keyStoreIs, keyStorePassword.toCharArray());
    kmf.init(ks, keyStorePassword.toCharArray());
  }
  return kmf;
}

之前的代码将从我们之前准备的 BCS 密钥库中加载一个公钥和私钥。因此,剩下的就是 getTrustManagerFactory 函数,用于加载我们的受信任 CA:

TrustManagerFactory getTrustManagerFactory(Context ctx)
  throws Exception {

  TrustManagerFactory tmf = null;

  if ( trustStore != null) {
    InputStream keyStoreIs = ctx.getResources().
                               getAssets().open(trustStore);
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    tmf = TrustManagerFactory.getInstance(algorithm);
    KeyStore ts = KeyStore.getInstance("BKS");
    ts.load(keyStoreIs, trustStorePassword.toCharArray());
    tmf.init(ts);
  }
  return tmf;
}

现在我们有了 SSLOptions 类来初始化我们的 SSLContext,让我们继续到 HTTPRequest 并更新我们的 Builder 以存储 SSLOptions

public class HTTPRequest {

  final SSLOptions mSSLOptions;

  private HTTPRequest(Builder builder) {
    this.mSSLOptions = builder.mSSLOptions;
  }

  public static class Builder {
    ...
    private SSLOptions mSSLOptions = null;

    public Builder setSSLOptions(SSLOptions options) {
      this.mSSLOptions = options;
      return this;
    }
  }
  ...
}

最后,我们准备更新我们的 HTTPAsyncTask 以使用我们的 SSLOptions 对象来自定义我们的 SSL 客户端端点。因此,我们将能够验证由我们自己的 CA 签署的证书的服务器身份(C=UKST=BirminghamL=BirminghamO=Packt PublishingCN=packt)以及反之亦然:

@Override
protected Result<HTTPResponse>
doInBackground(HTTPRequest... params) {
  // Retrieve the request URL from the request object
  URL url = new URL(request.mUrl);
  // Opens up the connection to the remote pper
  conn = initConnection(request, url);
  ...
}

HttpURLConnection 
initConnection( HTTPRequest request, URL url) throws IOException {

  HttpURLConnection genCon = (HttpURLConnection) url.
                             openConnection();

  if ( url.getProtocol().equals("https") ) {
    HttpsURLConnection con = (HttpsURLConnection) genCon;
    // Apply our SSL Options to the connection
    if ( request.mSSLOptions != null ) {
      applySSLContext(request, con);
    }
  }
  return result;
}
void 
applySSLContext(HTTPRequest request, HttpsURLConnection con) {
 // Initialize the SSL Session with your own
 // keystore and truststore
  if ( request.mSSLOptions != null ) {
    SSLContext ctx = request.mSSLOptions.sslContext;
    con.setSSLSocketFactory(ctx.getSocketFactory());
    con.setHostnameVerifier(new AcceptAllHostNameVerifier());
  }
}

注意,我们的实现不执行主机名验证,因为服务器 CN 可能不会与用于联系 HTTP 服务器的服务器主机名匹配。

然而,如果您想对此更加严格,请将 setHostnameVerifier 行更改为使用默认行为,实现您自己的 HostnameVerifier,或使用 Android SDK 中可用的主机名验证器,例如 Apache 的 X509HostnameVerifier,该验证器检查提供的主机名是否与提供的证书 CN 或 Subject-Alts 中的任何一个匹配。

最后,让我们说明如何使用我们的客户端连接到一个具有以下证书链的服务器:

// Server Certificate
Certificate[1]:
Owner: CN=asynchronous_server, OU=Packt Publishing, O=Packt Publishing, L=Birmingham, ST=Birmingham, C=UK
Issuer: CN=packt, ..., C=UK

// CA Certificate
Certificate[2]:
Owner: CN=packt, OU=Packt Publishing, O=Packt Publishing, L=Birmingham, ST=Birmingham, C=UK
Issuer: CN=packt, …, C=UK

以下代码展示了如何与一个协议为 https 的 URL 建立一个 SSL 会话:

// Set the HTTP Verb to GET
builder.setVerb(HTTPRequest.Verb.GET);
// Sets location of the remote resource
builder.setUrl("https://<server_hostname>:<port>/hello_ssl");
builder.addHeader(new Header("Accept", "text/plain"));
SSLOptions.Builder sslBuilder = new SSLOptions.Builder();

// TLS Cipher Suite using the  asynchronous_client.ks 
// as the truststore file and keystore file 
sslBuilder.setKeyStore("asynchronous_client.ks", "123qwe")
          .setTrustStore("asynchronous_client.ks", "123qwe")
          .setCipherSuite(SSLOptions.CipherSuite.TLS);

// The Application context is required to load
// the keystore from the assets
builder.setSSLOptions(sslBuilder.build(getApplication()));

HTTPRequest request = builder.build();

PacktAsyncHTTPClient client = new PacktAsyncHTTPClient();
client.execute(request, textResponseHandler);

如果对等之间的 SSL 握手以成功结束,服务器将验证我们的身份,而我们的客户端将验证服务器的身份。因此,在两个实体之间打开了一个加密通道,使数据对第三方入侵者隐藏。

摘要

在本章中,我们详细探讨了HttpUrlConnection Android HTTP 客户端,并构建了一个基本且可扩展的异步客户端以与 HTTP Web API 交互。

在第一部分,我们揭示了HttpUrlConnection客户端与在 Marshmallow SDK 之前可用的已弃用的 Apache HTTP 客户端之间的主要区别。

接下来,我们编写了异步客户端的核心类和回调接口,并扩展了我们的高级客户端以与 JSON 和 Web API 交互。此外,我们还构建了将我们的 Java 模型转换为 JSON 或 XML 文档的代码。

之后,我们学习了如何配置 HTTP 超时并设置能够使用我们自己的签名证书、密钥和 CA 的安全通信。在我们的示例中,我们创建并准备了一个 SSL 上下文,用于基于准备好的 Java 安全密钥库建立安全通道。

在下一章中,我们将介绍并探讨JNI(Java Native Interface)以在本地代码(C/C++)中创建异步任务。此接口能够与直接在设备 CPU 上运行的编译代码进行交互。

第九章。在本地层上的异步工作

在前面的章节中,我们一直在使用 Android SDK 提供的 Java 线程 API 和并发原语来构建我们的异步结构。Java 线程是我们应用程序中独立执行线路,它自动附加到 Android 虚拟机,并绑定到系统上的一个本地线程。在前面的章节示例中,我们在 JVM 上执行了 Java 编译的字节码,并使用 Java 同步和并发原语来解决正确性和活跃性问题。

在本章中,我们将利用 Java 本地接口(JNI)来执行用 C/C++ 编写的代码并将其编译成本地代码。本地代码直接在硬件上运行并使用本地 CPU 应用程序二进制接口ABI),由于编译器进行的优化或开发人员使用特定 ABI 技术引入的优化,通常比字节码运行得更快。因此,当我们对设备进行密集计算操作时,这可能是提高应用程序性能和降低功耗的方法。

考虑到这一点,我们将学习如何使用 JNI 接口在本地代码(C/C++)上执行并发任务,从本地层与 Java 代码交互,并从本地代码更新 UI。

之后,我们将学习如何创建本地线程并使用同步原语如 mutexcondition 来避免在设备并行执行多行代码且它们共享相同的内存段时可能出现的任何内存一致性问题时。

最后,我们将启动一组线程在本地层上运行后台工作,并将结果分派到 UI。

在本章中,我们将涵盖:

  • JNI 简介

  • Android NDK

  • 从 Java 代码调用 C 函数

  • 从 Java 代码调用 C++ 成员/静态函数

  • 从本地代码访问 Java 对象

  • 在 Java 线程上执行本地后台工作

  • 在本地线程上执行异步工作

  • 从本地代码与 Java 监视器交互

  • 在本地层上处理 Java 异常

JNI 简介

JNI 是一个接口,允许从 Java 虚拟机JVM)执行用 C、C++ 或汇编编写的本地代码。该接口严格定义了任何 JNI 实现应该如何行动以管理和控制 Java 代码与机器代码之间的交互。此外,机器代码能够与 JVM 交互并创建对象、执行成员函数、修改成员变量和处理 Java 异常。

允许您在 Java 代码中执行机器代码的 JNI 通常用于:

  • 加速应用程序的一些关键部分。由于代码直接在硬件上运行,它可以利用特定的指令集来提高执行效率:

    • 示例:使用 SIMD 指令来加速音频或视频浮点运算。
  • 将现有的 C/C++库集成到您的 Android 应用程序中。您可以将任何针对 Android 平台编写的遗留代码或库移植到您的 Android 应用程序中并使用它:

    • 示例:将开源库如opencvlibgdxbox2d集成到您的应用程序运行时。
  • 要使用 Java API 无法访问的平台相关特性:

    • 示例:如 poll 和信号量等低级 OS 特性或如 OpenGL、OpenSL ES 或 OpenMAX AL 等本地 API。

注意

注意,将 C/C++和 JNI 添加到您的项目中并非免费,通常会增加项目的复杂性,使得调试、构建和测试变得更加困难。因此,在决定在您的应用程序中使用它之前,您必须评估其成本效益。

Android NDK(本地开发工具包)

为了帮助构建需要 Java 层和本地层之间动态协作的 Android 应用程序,Android 开发者网站上提供了一个名为 Android NDK 的开发工具包(developer.android.com/ndk/index.html)。

Android NDK 是一个 Android 工具集,允许您将用 C/C++编写的代码编译成 Android 支持的多个 ABIs,它还能够将用 C 或 C++编写的现有库编译到 Android 平台。

在我们更详细地继续之前,您应该在您的开发平台上安装 NDK 包,按照developer.android.com/ndk/downloads上定义的说明进行操作。

在撰写本文时,NDK 的最新版本是 10e,因此我们将以此版本为基础编写代码和示例,本章的其余部分也将以此为基础。

您将在应用程序中编写的 Java 源代码,由 Android SDK 编译,生成 Android 字节码,该字节码将在任何 Android 设备上的 Android JVM 上被解释。

使用用 C 或 C++编写的源代码,NDK 编译器将其转换为具有不同指令集、硬件特性、调用约定和存储约定的 CPU 代码。每种 CPU 架构都有自己的 ABI,它定义了机器代码应该如何排列以与 CPU 硬件交互。

NDK 工具集包含工具,可以抽象这些硬件特性,并为以下 ABIs 生成机器代码:armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips 和 mips64。

大多数情况下,您希望支持尽可能多的设备,因此默认情况下,NDK 将为所有支持的 CPU 架构和指令集生成代码。

从 Java 代码调用 C 函数

如前所述,强大的 JNI 接口能够管理双向交互,从 Java 到 C 和从 C 到 Java。

声明方法的 Java 类使用关键字 native 声明该方法的行为是在本地代码中实现的。与常规 Java 方法一样,JNI native 方法能够接收 Java 对象或原始数据类型作为参数,并返回原始数据类型和对象。

让我们看看一个 native 方法定义在 Java 类中会是什么样子:

public class MyNativeActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    cTv.setText(isPrime(2) ? "true" : "false");
  }
  …
  private native boolean isPrime(int number );
}

前面的活动将调用本地代码来检查一个数字是否为质数,并在 UI 上打印结果。

注意,我们的函数接收一个原始数据作为参数,并返回一个原始 boolean 类型的结果,并且没有函数体,就像一个抽象函数一样。

当成员函数声明为 native 时,我们通知编译器该函数将在 C/C++ 中实现,在一个在运行时动态加载的本地库中。

现在成员函数已经在 Java 侧声明为 native,让我们使用 javah 在本地代码中声明和实现我们的本地方法。

javah 能够帮助开发者生成使用 JNI 接口命名的约定来生成本地方法原型,SDK 成为一个方便的工具,可以为您所有的类生成本地方法的头文件。

要使用它,请前往您的项目目录,创建一个 jni 目录,并运行下一个 javah 以生成您本地函数的头文件。在 Android Studio IDE 中,打开一个终端窗口并转到 src/main 目录:

javah  -d jni -classpath <sdk_direcory>/android.jar:../../build/intermediates/classes/debug/  com.packpublishing.asynchronousandroid.chapter9.MyNativeActivity

如果一切按预期进行,文件 com_..._chapter9_MyNativeActivity.h 将会生成我们的 native 方法声明:

JNIEXPORT jboolean JNICALL Java_com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity_isPrime
  (JNIEnv *, jobject, jint);

native 方法将接收指向 JVM 环境的 JNIEnv* 指针,一个指向实际调用该方法的 Java 对象实例的 jobject 引用,以及一个整型参数。

在以下 JNI 规范中声明的先前方法,应在您的代码中声明和实现,并在运行时通过共享库加载。

现在我们已经声明了方法,让我们在 jni 目录下创建一个 source 文件,名为 c_functions.c,其中包含 isPrime 函数的 native 方法实现:

#include "com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity.h"

#ifdef __cplusplus
extern "C" {
#endif

jboolean  Java_com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity_isPrime( JNIEnv *env, jobject obj, jint number) {
    int c;
    for (c = 2; c < number ; c++) {
        if (number % c == 0)
            return JNI_FALSE;
    }
    return JNI_TRUE;
}
#ifdef __cplusplus
}
#endif

当调用 MyNativeActivity.isPrime 时,JNI 接口将处理过程透明地转发到本地代码函数,传递一个本地整型原始数据 (jint)。Android JNI 实现自动将 Java 类型 int 的值转换为本地类型 (jint),执行本地函数,并在最后返回由 JNI 接口自动转换为 Java 原始数据 booleanjboolean

以下表格显示了 Java 类型如何映射到本地类型:

Java 类型 本地类型 描述
boolean Jboolean 无符号 8 位
byte Jbyte 有符号 8 位
char jchar 无符号 16 位
short jshort 有符号 16 位
int jint 有符号 32 位
long long jlong 有符号 64 位
float jfloat 32 位浮点数
double jdouble 64 位浮点数
Object jobject 任何 Java 对象
Class class 类对象
String jstring 字符串对象
void void

虽然我们在源文件中声明并实现了本地函数,但 JVM 不会找到该方法,直到我们加载包含我们的本地函数的共享库。首先,我们将在项目的根目录 local.properties 文件中定义 ndk 文件夹:

ndk.dir=<Path to downloaded NDK package>/android-ndk-r10e

接下来,在我们的 build.gradle 模块配置中,我们将在 ndk 配置部分下定义共享库的名称:

apply plugin: 'com.android.library'

android {
    defaultConfig {
        minSdkVersion 9
     ...
        ndk { moduleName "mylib" } 
    } 
}

最后,Android Studio 能够编译 c_functions.c 并为构建目录中支持的构建目标生成名为 mylib 的共享库:

├── lib
│   ├── arm64-v8a
│   │   └── libmylib.so
│   ├── armeabi
│   │   └── libmylib.so
│   ├── armeabi-v7a
│   │   └── libmylib.so
│   ├── mips
│   │   └── libmylib.so
│   ├── mips64
│   │   └── libmylib.so
│   ├── x86
│   │   └── libmylib.so
│   └── x86_64
│       └── libmylib.so

这些库将被打包在一个通用的 apk 文件中,以便我们的 Android 应用程序加载。

剩下的工作就是在我们的代码中使用它之前,在我们的运行时加载库。要在 JVM 运行时加载共享库,java.lang.System 类提供了一个 static 方法来加载共享库及其依赖项,因此在我们使用它之前,我们将在我们的 Activity 类中添加一个静态部分,以便在类加载器加载我们的类时立即加载库:

public class MyNativeActivity extends Activity {
   …
   static {
      System.loadLibrary("mylib");
   }
}

当需要库时,System 类会自动检测设备正在运行的 ABI 并加载所需的平台相关库。因此,如果您正在 x86 设备上运行,将加载 x86/libmylib.so

从本地代码调用 C++ 函数

到目前为止,我们已经调用了一个在 c_functions.c 源文件中实现的 C 函数,因此,在下一节中,我们将向您展示如何调用 C++ 成员类。

首先,我们将向 MyNativeActivty 添加一个名为 isPrimeCPlusPlus 的本地方法,该方法返回 String 类型的结果。让我们看看本地函数声明将如何看起来:

public class MyNativeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      ...
      TextView cPlusTv = (TextView)  
          findViewById(R.id.helloFromCPlusPlus);
      cPlusTv.setText(isPrimeCPlusPlus(4));
    }

    public native String isPrimeCPlusPlus(int number);
}

对新的 MyNativeActivity 类定义运行 javah 工具将生成一个具有以下签名的新的函数声明:

JNIEXPORT jstring JNICALL Java_com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity_isPrimeCPlusPlus(JNIEnv *, jobject, jint);

接下来,我们将在名为 mylib.cpp 的 C++ 源文件中实现素数函数,作为一个 class static 函数:

#include "com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity.h"

class Math {
  public:
  static int isPrime(int number) { ...// Elided for brevity}
};

#ifdef __cplusplus
extern "C" {
#endif

jstring  Java_com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity_isPrimeCPlusPlus
(JNIEnv * env, jobject obj, jint number) {

  return (env)->NewStringUTF(
     Math::isPrime(number) ? "Is Prime" : "Is not Prime");
}

#ifdef __cplusplus
}
#endif

如果您在 Android Studio 中构建项目,mylib.cpp 源文件将被检测到,并将新的函数和类添加到 libmylib.so 共享库文件中。

一旦我们运行应用程序,C++ 系统默认库将加载一个最小的系统 C++ 运行时。默认的 C++ 运行时不提供 C++ 标准库、异常支持和 运行时类型信息RTTI)。因此,如果您想使用 C++ 标准库的字符串类、容器、流和通用算法,您必须在加载库之前显式加载所需的 C++ 运行时。有关 Android 上可用的 C++ 运行时的完整和最新比较,请访问 Android 开发者网站上的 C++ Runtimes。

如果我们想使用不同于系统运行时的 C++ 运行时,我们必须在模块的 build.gradle 文件中显式设置运行时:

  ndk {
    moduleName "mylib"
    stl "c++_shared"
  }

此外,在我们加载我们的库或任何依赖于它的库之前,我们必须先加载非默认的 C++ 运行时库:

public class MyNativeActivity extends Activity {
  static {
    System.loadLibrary("c++_shared");
    System.loadLibrary("mylib");
  }
}

由于 c++_shared 提供了完整的 STL 库实现,从现在起我们将使用此运行时作为代码示例的基础 C++ 运行时。

太好了!到目前为止,我们已经学习了如何使用 JNI 接口与原生方法交互,所以我们的下一步是学习如何从原生代码访问 Java 对象。

从原生代码访问 Java 对象

当我们调用原生函数时,C 或 C++ 函数接收一个指向 JNI 方法表的 JNIEnv 指针,这些 JNI 方法用于与 JVM 运行时交互。JNIEnv 指针为我们提供了一组原始数据,可用于查找 Java 类定义、设置或获取 Java 对象字段值、调用静态或成员 Java 对象函数、创建 Java 对象、与 Java 监视器交互或处理异常。

我们的下一个示例将在原生函数中计算 UI 小部件 EditText 上的单词数量,并使用原生代码中的计数结果更新 TextView 文本。因此,我们将学习如何使用 JNIEnv 访问成员 Java 对象字段,以及如何使用 JNIEnv 接口调用 Java 对象方法(TextView.setText)。

让我们首先定义我们的原生函数,并在 EditField 内容更改时调用它:

public class MyNativeActivity extends Activity {

  protected EditText inputTextEt = null;
  protected TextView charCountTv = null;

  @Override
  protected void onCreate(Bundle savedInstanceState) {

     // Reference stored as member fields for native access	
    inputTextEt = (EditText) findViewById(R.id.inputText);
    charCountTv = (TextView) findViewById(R.id.charCount);

    // Called every time the code changes
    inputTextEt.addTextChangedListener(new TextWatcher() {
      @Override
      public void onTextChanged(CharSequence s, int start, 
                                int before, int count) {
        updateWordCounter(s.toString());
      }
      ...
    });
  }
  // Native function that calculates the number of words 
  // in a string
  private native void updateWordCounter(String s);
}

注意,已向 Activity 添加了新函数,请确保您运行 javah 以生成新的原生函数声明。

接下来,我们将定义一个 JNI 原生函数,用于计算字符串中的单词数量:

class Util {
  public:
  static int countWords(const std::string &strString) { 
    ... 
  };
}

Void Java_com_packpublishing_asynchronousandroid_chapter9_MyNativeActivity_updateWordCounter(JNIEnv *env, jobject obj, jstring text) {

    std::string content(env->GetStringUTFChars(text, 0));

    size_t word_cnt= Util::countWords(content);

    // Update the TexView with word_cnt integer

}

我们留下了原生实现,因为我们将会逐步实现它。作为第一步,我们将从 charCountTv 对象字段获取用于在 UI 文本输入中显示单词数量的 TextView 对象实例。

要访问 Java 对象字段或方法,始终需要一个 jmethodIDjfieldID

jmethodID   GetMethodID(JNIEnv* env, jclass clazz, 
                        const char*name, 
                        const char* methodSignature);

jfieldID    GetFieldID(JNIEnv*, jclass clazz, 
                       const char*name, 
                       const char* fieldTypeCode);

为了构建 methodSignaturefieldTypeCode(TC),我们必须使用以下表格将 Java 类型映射到类型代码:

Java 类型 类型代码 (TC)
boolean Z
Byte B
Char C
double D
float F
Int I
Long J
short S
Object L<package>;
Void V

在转换数组时,始终在类型代码前加上 [ 字符。

要创建一个 jfieldID,我们需要一个单独的类型代码。然而,要构建方法签名,我们使用以下格式:

 (<Argument 1 TC ><Argument N TC>) <Return TC>

让我们看看如何使用以下说明中的指令在原生代码中获取 charCountTv TextView 对象:

// 1\. Obtain a reference to the MyNativeActivity class definiton
jclass activityClass = env->GetObjectClass(obj);

// 2\. Get the fieldId for the charCountTv TextView 
   jfieldID charCountFId = env->GetFieldID(activityClass, "charCountTv", "Landroid/widget/TextView;");

// 3\. Retrieve the object using the object and the jfieldID
jobject tvObj = env->GetObjectField(obj,charCountFId); 

一旦我们有了 TextView 引用,我们就可以调用 setText(CharSequence) 实例方法来发布找到的单词数量。要调用 Java 方法,我们将使用 JNI 函数 CallVoidMethod,该函数由方法签名创建的 jmethodId

void CallVoidMethod(JNIEnv *env, jclass clazz,
                    jmethodID methodID, ...);

让我们看看更新TextView charCountTv以显示单词数的原生代码将如何看起来:

// 1\. Get the TextView class definition 
jclass textViewClass = env->GetObjectClass(tvObj);

// 2\. Get the methodId for the TextView.setText function
jmethodID setTextMId = env->GetMethodID(
       textViewClass, "setText", "(Ljava/lang/CharSequence;)V");

// 3\. Invoke the SetText instance function
   env->CallVoidMethod(
     tvObj,setTextMId,env->NewStringUTF(wordCountStr);

要调用static方法和具有不同结果类型的方法,JNI 接口为我们提供了一组具有以下签名的函数:

// To invoke a Class static method that returns a Java Type
<NativeType> CallStatic<Type>Method(
   JNIEnv *env, jobject obj, jmethodID methodID, ...);

// To invoke a method that returns a Java <Type>  
<NativeType> Call<Type>Method(
   JNIEnv *env, jobject obj, jmethodID methodID, ...);

// To invoke a method that returns a Java <Type[]>  
<NativeArrayType> Call<Type>MethodA(
  JNIEnv *env, jobject obj, jmethodID methodID, ...);

现在我们已经了解了如何使用 JNI 接口调用原生函数的基本知识,我们就可以开始使用 JNI 在原生代码上执行异步工作了。

在 Java 线程上执行原生后台工作

在前面的章节中,我们使用了 JNI 接口在主线程上执行原生函数。由于它们在主线程上运行,函数能够更新 UI,访问Activity实例字段,或者直接更新任何 UI 小部件。

然而,正如我们之前讨论的那样,对于长时间计算或密集型任务,我们必须在后台线程上执行它们。

在前面的章节中,我们学习了如何使用AsyncTaskLoaderHandler和远程服务在后台线程上执行工作,这些线程不会降低 UI 响应性或干扰 UI 渲染。

在这些任何 Android 特定构造中,后台线程已经附加到 JVM 上。因此,后台线程已经拥有访问一个准备好使用的 JNI 环境的权限。

在我们的下一个示例中,我们将使用Loader构造函数并构建AsyncTaskLoader,在后台加载图像,在原生代码中将图像转换为灰度,并在 UI 屏幕上发布结果。

首先,在我们深入探讨原生函数的细节之前,我们将详细说明Loader Java 类定义将如何看起来:

public class GrayImageLoader extends AsyncTaskLoader<Result<Bitmap>> {

  final String fileName;
  Bitmap grayImage;

  public ToGrayImageLoader(Context ctx, String fileName) {
    super(ctx);
    this.fileName =  fileName;
  }

  @Override
  public Result<Bitmap> loadInBackground() {
    Result<Bitmap> result = new Result<Bitmap>();
    try {
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inPreferredConfig = Bitmap.Config.ARGB_8888;

      // Build a RGBA 8888 Bitmap to represent the image 
      Bitmap b = BitmapFactory.decodeFile(this.fileName, options);

      // Convert the Image to Gray scale on Native code
      Bitmap originalImage = BitmapFactory.decodeStream(
        getContext().getAssets().open(fileName));

 	  // Fill the result with the Gray Image
      result.obj = convertImageToGray(originalImage);
    } catch (Exception e) {
      result.error = e;
    }
    return result;
  }

  private native Bitmap convertImageToGray(Bitmap original);
  …
}

注意,我们的Loader将加载一个被通用Result类包裹的Bitmap图像,就像我们在前面的章节中所做的那样。当 Java 或原生代码发生任何异常时,Result.error将被填充,使得Loader消费者能够检测到错误并相应地做出反应。

我们的加载器将接收一个参数,即从资源加载的图像文件名,并将图像解码为ARGB_8888格式的Bitmap对象,并返回一个灰度图像

当原生函数在后台线程上无错误执行时,Result<Bitmap>对象将被传递到 UI 线程上的Loader消费者,以便更新到设备屏幕。

对我们的新AsynTaskLoader类执行javah应该生成具有以下函数签名的com_packpublishing_asynchronousandroid_chapter9_ToGrayImageLoader.h头文件:

JNIEXPORT jobject JNICALL Java_com_packpublishing_asynchronousandroid_chapter9_ToGrayImageLoader_convertImageToGray(JNIEnv * env, jobject loader, jobject bitmap);

要在原生层处理Bitmap对象,需要jnigraphics共享库。因此,让我们更新我们的gradle构建配置,将我们的库与jnigraphics共享库链接:

ndk {
    moduleName "mylib"
    stl "stlport_shared"
    ldLibs "jnigraphics", "log"
}

将我们的库mylibjnigraphics链接将强制动态加载器在每次通过System.loadLibrary加载我们的库时加载jnigraphics库。除此之外,gradle系统将为应用程序包文件APK)中所需的几个 ABIs 打包jnigraphics共享库。

现在我们已经在头文件中定义了方法,是时候实现将原始Bitmap转换为灰度Bitmap的原生函数了。

首先,让我们创建包含jni方法定义和所需的jnigraphics头文件的源文件image.cpp

#include "com_packpublishing_asynchronousandroid_chapter9_ToGrayImageLoader.h"
#include <android/bitmap.h>

接下来,我们将实现将原始像素转换为灰度像素的功能:

jobject Java_com_packpublishing_asynchronousandroid_chapter9_ToGrayImageLoader_convertImageToGray(JNIEnv * env, jobject obj, jobject bitmap) {

  AndroidBitmapInfo info; // Image Information
  void * pixels; // Pixel Matrix
  int ret; // Jni Graphics operation result code

   // Reads the Image width, height, format,...
  if ( (ret = AndroidBitmap_getInfo(env, bitmap, & info)) < 0) {
    jclass clazz = env->FindClass("java/lang/RuntimeException");
    env->ThrowNew(clazz, "Failed to get bitmap info");
    return 0;
  }
  // Loads the bitmap pixel matrix on pixels pointer
  if ((ret = AndroidBitmap_lockPixels(env,bitmap,(void **)& pixels)) < 0) {
    // Exception Generation Elided for brevity
  }
  // Convert each pixel to gray
  ...
  AndroidBitmap_unlockPixels(env, bitmap);
  return bitmap;
}

jnigraphics库的帮助下,我们可以使用AndroidBitmap_getInfo读取图像信息,如果一切顺利,图像信息将被存储在局部变量info中以供进一步使用。

然而,如果AndroidBitmap_getInfo失败,我们将在 JVM 中抛出异常并立即从原生函数返回,因为我们调用了return。在正常情况下,如果我们使用ThrowNew在 JVM 中抛出异常,原生函数不会停止并将控制权传递给异常处理器。因此,如果在原生代码调用期间抛出异常,当函数返回时,JNI 接口将检测到它并将执行权传递给异常处理器。

在我们的例子中,我们使用从Findclass JNI 函数获得的jclass生成RuntimeException

当我们完成位图处理时,我们通过AndroidBitmap_unlockPixels解锁像素,并将Bitmap jobject返回给最初从后台线程调用原生方法的loadInBackground函数。

如您所知,处理过的位图将由AsyncTaskLoader在 UI 线程中交付,并可用于更新屏幕上显示图像的ImageView或其他类型的 UI 小部件。让我们看看LoaderCallback.onLoadFinished回调可能的样子:

@Override
public void onLoadFinished(Loader<Result<Bitmap>> loader, Result<Bitmap> data) {
  if ( data.obj != null ) {
    ImageView iv = (ImageView)findViewById(R.id.grayImage);
    iv.setImageBitmap(data.obj);
  } else {
    Log.e("<TAG>", data.error.getMessage(), data.error);
  }
}

在这个简单的例子中,我们能够在AsyncTaskLoader的帮助下以机器码的形式执行异步工作,尽管类似的程序也可以通过AsyncTask子类、普通线程或甚至HandlerThread来完成。这类 Android 构造使用由 Android JVM 管理的 Java 后台线程,因此,不需要显式地将这些线程附加到 JVM,因为它们是 JVM 系统的一部分,并且有自己的JNIEnv

在下一章中,我们将学习如何创建纯原生线程,并使用它们以一致和可靠的方式为我们的 Android 应用程序执行后台工作。

在原生线程上执行异步工作

Android NDK 包含 POSIX 线程 C API,它提供了一个创建和销毁本地线程、本地互斥同步原语(如命名互斥锁)和条件变量的 API,类似于 Java 监视器,允许线程等待资源状态的变化。除了这个全局 API 之外,开发者还可以访问在 clanggnu_stl C++ 运行时上可用的更高层次的 C++11 线程 API。

由于这两个框架都提供了相同类型的并发功能,我们将使用 C++11 线程框架,因为它简单且与 Java Thread API 相似。

首先,让我们更新我们的 ndk build.gradle 以使用支持我们将在后续代码示例中使用的线程 API 的 clang C++ 运行时:

ndk {
  moduleName "mylib"
  stl "c++_shared"
  cppFlags.add("-frtti")
  cppFlags.add("-exceptions)
}

将本地线程附加到 JVM 和从 JVM 中分离

为了与我们的 JVM 交互并为我们执行并发后台工作,本地线程应该附加到当前虚拟机并构建自己的 JNIEnv

线程 JNIEnv 与特定的本地线程绑定,不能与其他线程共享,因为它管理自己的引用和本地线程环境。

为了更实际地向您展示,在接下来的几段中,我们将构建一个代码示例,该示例创建附加到 JVM 的本地线程,在后台执行工作并与 UI 线程交互,使用众所周知的 Android 处理器结构发布 keep-alive 消息。

要将任何线程附加到 JVM,我们需要访问全局虚拟机结构,JavaVM

jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args) 

一个获取 JVM 结构的好方法是从 JNI_OnLoad 函数中检索,这是一个当我们的库被 JavaVM 加载时自动调用的函数。当 JNI 接口回调被调用时,我们将保存 JavaVM 引用以供将来使用:

// Java VM Global Pointer
static JavaVM* gVm = NULL;
jint JNI_OnLoad (JavaVM* vm, void* reserved) {
    gVm = vm;
    return JNI_VERSION_1_4;
}

随着 JVM 全局指针准备就绪并可供使用,我们能够将任何本地线程附加到应用程序 JVM 并开始与 JNIEnv 交互。

作为起点,我们将创建一个高级 C++ 类,该类会自动附加到 JVM,并在实例被销毁时从 JVM 中分离。这个类将作为我们线程示例的基类,提供一个用于原生线程创建的通用抽象接口。

让我们看看 JavaThread 类定义将如何看起来:

#include <thread> // including the C++11 Thread Header
#include <jni.h>  // JNI Header
class JavaThread: public std::thread {
public:	 
  JavaThread();
  Void join(); // Wait for the thread to finish
  void entryPoint();
  void start(); 
  void stop();
protected:
   // Method that subclass should implement to define
   // the unit of work 
   virtual void run() = 0;    	 
   virtual void onDetach() {};JNIEnv* threadEnv = NULL; // Thread specific JNI Environment
   std::thread thread_; // C++11 Thread 
   // is Thread attached to JVM
bool isStarted = false;
   std::condition_variable startCond;
   std::mutex startMutex;
   volatile bool shouldStop = false;
   std::condition_variable stopCond;
   std::mutex stopMutex;
};

在这个类头文件中,我们从原始的 C++ 线程类中继承 JavaThread 并定义抽象方法 run。任何工作线程都可以继承 JavaThread,提供自己的 run 方法实现。此外,特定于 JNI 接口环境的受保护线程存储在 threadEnv 中,供线程子类将来使用。

此外,我们将向您介绍从线程头文件中可用的 C++同步原语。std:mutex是一个互斥原语,它一次只允许一个线程进入受保护的临界区。如果一个线程正在执行临界代码,另一个尝试进入临界区的线程将阻塞执行,直到执行临界代码的线程释放锁。以下是一个简单的示例:

 // Thread waits for his turn
 mutex.lock();
 ...// Only one thread enters on this section
 mutex.unlock();

在 Java 中,可以通过在 Java 块或同步块中使用synchronized关键字实现相同的行为。

类似于 Java 监视器的条件并发原语,可以用来阻塞一个线程或一组线程的执行,直到另一个线程修改共享信息并发送信号通知等待的线程。

现在我们知道了这些 C++并发原语用于什么,让我们实现自动附加和从 JVM 中分离的JavaThread

首先,我们将通过传递entryPoint函数作为线程的运行时函数,在原生的start()方法中启动背景原生线程:

void JavaThread::start() {
    thread_ = std::thread(&JavaThread::entryPoint, this);
    std::unique_lock<std::mutex> lck(startMutex);
    // wait until the Thread is attached to JVM
    while (!isStarted) startCond.wait(lck);
}

std::thread在系统中创建线程时,它将调用我们的对象中的entrypoint函数来初始化 JNI 环境。

同时,我们将阻塞调用线程,直到新线程附加到 JVM 并发送信号到startCond条件变量。接下来,当std::thread在操作系统中初始化新线程时,它将执行控制权更改为构造函数中指定的成员函数,即JavaThread::entryPoint。在这个函数中,我们将原生线程附加到 JVM,并将执行调度到子类run()方法。

让我们看看我们如何实现entryPoint函数:

void JavaThread::entryPoint() {

  // Attach current thread to Java virtual machine
  // and obrain JNIEnv interface pointer
  { 
    // Acquires the start Mutex to access the conditional variable
    std::unique_lock<std::mutex> lck(startMutex);
    // Ataches the current thread to the JVM 
    // and caches the JNIEnv 
    if ( gVm->AttachCurrentThread(&threadEnv, NULL) != 0) {
      ..// Handle the error 
    }
    isStarted = true;  // Changes the shared variable
    startCond.notify_all(); // Notify the thread constructor
  }
  onAttach();
  try {
    // Run the subclass method
    run();
  } catch (...) {
    // Detach current thread when an exception happens
    onDetach();
    gVm->DetachCurrentThread();
    throw;
  }
  // Detach current thread from Java virtual machine
  onDetach(); 
  gVm->DetachCurrentThread();
}

注意,即使在run执行期间抛出运行时异常,线程也会从 JVM 中分离出来。当线程从 JVM 中分离出来时,所有线程监视器都会释放,并且所有等待此线程的 Java 线程都会收到通知。

对于停止机制,我们将使用一个boolean变量和一个condition变量来通知shouldStop条件已更改。稍后,我们的JavaThread子类将利用此机制来停止run()执行。

让我们看看stop方法将如何看起来:

void JavaThread::stop() {
    // Acquire the stop mutex 
    std::unique_lock<std::mutex> lck(stopMutex);    
    // Change the should stop condition 
this->shouldStop =true;    
    // Notify any thread waiting for this signal that shouldstop
    // condition has changed.
    stopCond.notify_all();
}

在完全定义了原生线程基类之后,我们现在可以创建我们的派生类,该类在run方法中实现所需的行为。

如前所述,我们将使用Handler构造来提交从后台线程到 UI 线程的消息。由于它们在同一个进程中运行并共享相同的内存,我们可以安全地将Handler对象的引用传递给原生后台线程。

首先,在我们开始实现我们的JavaThread子类之前,我们将编写NativeThreadsActivity并实现一个Handler匿名子类来接收原生线程的消息:

public class NativeThreadsActivity extends Activity {  
  public static final int HEALTHCHECK = 0; // Handler Message Code
  ...
  // Process the Message sent by the native threads
  Handler myHandler = new Handler() {
    public void handleMessage(Message msg) {
      switch (msg.what) {
      case HEALTHCHECK:
        TextView tv = (TextView)findViewById(R.id.console);
        tv.setText((String) msg.obj + tv.getText());
        break;
      }
    }
  };
  // Start the Native Threads when the start button is clicked
  public native void startNativeThreads(Handler handler);
  // Stop The Native Threads when the stop button is clicked
  public native void stopNativeThreads();
}
Once our handler receives a message with the code HEALTHCHECK it will prepend the String received on the msg.obg to a TextView on the Activity UI screen. 

此类还将负责在每次点击启动停止按钮时启动和停止本地线程。

代码示例中省略了启动和停止按钮的设置。然而,启动按钮将调用 startNativeThreads 的本地函数,并将 myHandler 作为 Handler 参数传递,而停止按钮将调用 stopNativeThreads 来停止本地线程的执行。此外,我们还可以在 Activity.onStop 中调用 stopNativeThreads 来在活动被销毁时停止线程。

现在我们需要实现一个将在后台运行的 JavaThread,并通过 handler 对象向 UI 线程提交一个 healthcheck 消息。由于 handler 来自不同的 jniEnv,首先需要从原始 handler 创建一个 JNI 全局引用。让我们先从实现构造函数开始,该构造函数创建一个全局的 Handler 对象引用并将引用存储在一个 member 变量中:

class HealthCheckThread: public JavaThread {

     jobject handlerObj; // Cache the Global Reference  
public:
    HealthCheckThread(JNIEnv *env_,jobject handlerObj_):
      JavaThread(), 
      // Use the main threadJNIEnv to create a global ref
      handlerObj(env_->NewGlobalRef(handlerObj_)) {} 
     }

在构造函数中,我们从主线程接收了对象引用,调用了我们的 JavaThread 默认构造函数,并创建了一个全局引用来存储原始引用。

JNI 引用解释

理解 JNI 引用如何被 JVM 管理非常重要,因为如果我们不正确使用它们,我们可能会使应用程序崩溃或引入内存泄漏。内存泄漏会影响应用程序性能,增加电池消耗,并在长期内导致应用程序因 java.lang.OutOfMemoryError 异常而崩溃。如您所知,JVM 垃圾收集器 (GC) 管理应用程序内存使用,在对象不再使用时清理对象。当一个对象在内存中没有引用时,该对象被认为是垃圾收集的候选对象,因此,当 GC 发现一个未引用的对象时,它将释放该对象从内存中。

JNI 支持三种类型的引用:

  • 局部引用 – 附属于线程、JNIEnv 的引用,其生命周期有效于本地方法的持续时间。该引用传递给本地方法,并在方法返回时销毁。用户也可以在本地方法中创建和删除局部引用,以防止任何对象被垃圾回收。请注意,局部引用在其创建的 JNIEnv 中有效。以下 JNI 函数可用于管理局部引用:

    Jobject NewLocalRef(jobject);
    void DeleteLocalRef (jobject);
    
    • JVM 提供了一个函数来在当前 JNI 帧中分配空间以存储局部引用。默认情况下,它具有 16 个引用的容量:

      jint EnsureLocalCapacity(jint);
      
  • 全局引用 – 用于保持全局对象无限期存活期的引用。这类引用可以在线程JNIEnv对象之间共享。在不再需要时,显式地从 JVM 删除引用是至关重要的。当你不释放系统中的引用时,你正在应用程序中创建内存泄漏。请注意,当你从系统中释放引用时,该引用就不再有效,因此如果你尝试使用它,JNI 接口将抛出异常。以下 JNI 函数用于管理局部引用:

    jobject NewGlobalRef (jobject);
    void DeleteGlobalRef(jobject);
    
  • 弱全局引用 – 与全局引用类似,但它不会阻止对象在它是对象唯一活动引用时被垃圾回收。JNI 中的弱全局引用是 Java 弱引用的简化版本:

    jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
    void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
    

从本地线程与 UI 交互

由于我们希望缓存一个Handler对象的引用,该对象在startNativeThreads执行期间仍然存活,因此在将其保存到成员变量之前创建一个全局引用是有意义的。该引用将在我们的后台线程中用于向 UI 提交消息。

由于我们在HealthCheckThread类中创建了一个全局引用,为了在 JVM 中释放引用并避免任何内存损失,我们将在线程停止期间调用的HealthCheckThread.onDetach()函数中删除全局引用:

class HealthCheckThread: public JavaThread {
    ...
    virtual void onDetach(){
        jniEnv()->DeleteGlobalRef(handlerObj);
    }
}

接下来,我们将更新HealthCheckThread并实现一个run方法,该方法将提交健康检查消息到 UI 线程关联的Handler对象:

virtual void run(){

  while (!shouldStop) {
    std::unique_lock<std::mutex> lck(stopMutex);
    // Do Work
    // ...
    sendHealthMessage();
    // Wait until a stop signal is sent
    stopCond.wait_for(lck, std::chrono::seconds(1));
  }
    }

run函数将持续执行,直到shouldStoptrue。此外,在每次循环之间,线程将发送一条消息并阻塞一秒钟,除非发送停止信号通知线程停止。在这种情况下,使用本地条件变量唤醒线程从一秒钟的睡眠中醒来,当设置停止条件时。

关于HealthCheckThread类剩下的工作就是实现sendHealthMessage

void sendHealthMessage() {

  // Get the Handler class from the JVM
  jclass handlerClass = jniEnv()->FindClass("android/os/Handler");

  // Get the Handler.obtainMessage methodId 
  jmethodID obtainMId = jniEnv()->GetMethodID(handlerClass,  
 "obtainMessage","(ILjava/lang/Object;)Landroid/os/Message;");
  ...
  // Build up the alive message
  std::ostringstream oss;
  oss << "Thread[" << std::this_thread::get_id() 
      << "] is alive at " << ctime( & tt) << std::endl;;

  // Obtain a message object 
  jobject messagObj = jniEnv()->CallObjectMethod(handlerObj, 
                      obtainMId,
                      HEALTHCHECK_MESSAGE,
                      jniEnv()->NewStringUTF(oss.str().c_str()));

  // Get the Handler.senMessage methodId
  jmethodID sendMsgMId = jniEnv()->GetMethodID(handlerClass, 
                         "sendMessage","(Landroid/os/Message;)Z");

  // Enqueues a new message on the main thread looper
  jniEnv()->CallBooleanMethod(handlerObj,sendMsgMId, messagObj);
  // Deletes the local references
  jniEnv()->DeleteLocalRef(handlerClass);
  jniEnv()->DeleteLocalRef(messagObj);
}

在开始使用处理器对象之前,我们使用obtainMessage实例方法从Handler全局消息队列中检索一个Message。为了构建传递给Message对象的字符串消息,我们使用ostringstream、线程 ID 和当前datetime格式化消息。

然后,我们将构建的Message推送到处理器对象,以便在我们的Activity中传递。最后,我们从本地JNIEnv删除创建的局部引用。

启动本地线程

为了完成我们的示例,我们将编写startNativeThreadsstopNativeThreads本地方法。每次我们点击开始或停止按钮时,这些方法将创建和销毁本地线程。为了简洁起见,省略了 UI 代码。让我们首先看看startNativeThreads

static const int num_threads = 10;
static JavaThread* threads[num_threads];

void  Java_com_packpublishing_asynchronousandroid_chapter9_NativeThreadsActivity_startNativeThreads
        (JNIEnv *jEnv, jobject activity, jobject handler){

    LOGI("Starting  %d Native Threads",num_threads);
    // Launch a group of threads
    for (int i = 0; i < num_threads; ++i) {
        threads[i] = new HealthCheckThread(jEnv,handler);
        threads[i]->start();
     }
}

startNativeThreads 中,我们创建了 num_thread 个线程,并将主线程的 JNIEnv 和处理程序引用传递给 HealthCheckThread 构造函数。从构造函数返回的 HealthCheckThread 指针被缓存在一个静态数组中,以供将来使用。

停止本地线程

由于我们使用 C++ 操作符 new 在动态内存中分配 HealthCheckThread 对象,在 stopNativeThreads 中,除了停止线程执行外,还需要释放动态内存,以避免在本地代码中出现内存泄漏。因此,剩下的就是实现 stopNativeThreads

void Java_com_packpublishing_asynchronousandroid_chapter9_NativeThreadsActivity_stopNativeThreads(JNIEnv *env, jobject activity){

    LOGI("Stopping %d Native Threads", num_threads);

    for (int i = 0; i < num_threads; ++i) {
        // Notify the thread to stop
     threads[i]->stop();        
        // This blocks the execution of the current thread until
        // HealthCheckThread native thread finishes
        threads[i]->join();        
  // De-allocates memory previously allocated 
        delete threads[i];
    }
}

stopNativeThreads 函数将使用 JavaThread::stop 成员函数停止创建的线程。如前所述,stop 成员函数将使用条件原语来通知运行循环它应该完成执行。在我们通知后台线程后,我们等待它完成,并销毁存储在数组指针中的对象。

太好了!在本节中,我们能够启动本地线程,将它们附加到 JVM,并使用 Handler 对象与主线程交互。在这个过程中,我们学习了 C++ 的 conditionmutex 并发原语,以同步对共享资源的访问。尽管我们一直在使用 C++11 并发原语来创建和同步线程,但我们也可以使用 POSIX pthread 库提供的并发原语来编写我们的示例。

POSIX 库 libpthread 也提供了管理本地线程、互斥并发原语(互斥锁)和条件变量的方法。

在本地层处理 Java 异常

当在 Java 中,如果在方法执行期间抛出异常,JVM 会停止正常的方法执行,并尝试在运行时找到一个异常处理程序来控制执行,但当你从 JNI 代码执行 Java 方法时,情况并不相同。

JNI 要求开发者在 Java 方法调用发生异常后显式实现异常处理。

此外,当异常处理挂起时,只有少数 JNI 函数是安全的可以调用:DeleteGlobalRefDeleteLocalRefDeleteWeakGlobalRefExceptionCheckExceptionClearExceptionDescribeExceptionOccurredMonitorExitPopLocalFramePushLocalFrameRelease<PrimitiveType>ArrayElementsReleasePrimitiveArrayCriticalReleaseStringCharsReleaseStringCriticalReleaseStringUTFChars

在本地函数中处理 Java 异常有三种方法。

第一种方法是使用 ExceptionClear 清除挂起的异常,并继续执行本地代码。这种方法很少是安全的,你需要审查所有错误流程,以验证你是否正确处理了异常。

第二种方式是,一旦检测到挂起的异常,释放 JNI 分配的资源,停止本地代码执行,并将控制权返回给 Java 代码。在这种情况下,JNI 将尝试在调用本地方法的 Java 帧中找到一个 Java 异常处理器。

第三种方式是释放挂起的异常,生成一个新的具有不同类类型的异常,并使用新的异常从本地方法返回,该异常将在 Java 代码中待处理。

在我们的下一个示例中,我们将遵循第三种方法,因为我们将使用 JNI 中大多数可用的函数来处理异常。首先,我们将向您展示如何使用 JNI 异常处理函数清除挂起的 Java 异常。除此之外,我们将停止本地方法执行,释放所有本地资源,并抛出一个不同的异常,以便在 Java 中由 RuntimeException 处理器处理。

首先,我们将编写一个 Activity,调用本地方法将在 JVM 中产生一个挂起的异常:

public class ExceptionActivity extends Activity {

 ...
 OnClickListener onClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      try {
        // Allocate a ByteBuffer with a size of 8 bytes
        ByteBuffer byteBuffer = ByteBuffer.allocate(8);

        // Call a native function that will try to access 
        // an out of bounds buffer position
        genException(byteBuffer);
	    // Catches a Runtime Exception
      } catch (RuntimeException e) {
        // Prints the Exception Stack Trace to the TextView
        TextView console = (TextView)findViewById(R.id.console);
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        console.setText(sw.toString());
      }
    }
  };
  ...
  // Native Function that will generate and Exception
  private native void genException(ByteBuffer buffer);
}

每次点击 genException 按钮时,调用一个会抛出运行时异常(java.lang.IndexOutOfBoundsException)的本地方法。

onClick (View v) 方法只能处理 java.lang.RuntimeException,因此我们必须在本地函数中处理 IndexOutOfBoundsException 并将其转换为 RuntimeException

void Java_com_packpublishing_asynchronousandroid_chapter9_ExceptionActivity_genException(JNIEnv * jniEnv, jobject activityObj, jobject byteBuffer){

  // Get the ByteBuffer class 
  jclass byteBufC= jniEnv->GetObjectClass(byteBuffer);

  jmethodID getMid = jniEnv>GetMethodID(byteBufC,"get","(I)B");

// Trying to access a buffer position out of the buffer capacity
  jbyte byte = jniEnv->CallByteMethod(byteBuffer,getMid,32);

  if (jniEnv->ExceptionCheck()) {
    // Prints the exception  on the standard Error
    jniEnv->ExceptionDescribe();    
    // Clears the exception on the JVM
    jniEnv->ExceptionClear();
    jclass excC = jniEnv>FindClass("java/lang/RuntimeException");
    jniEnv->ThrowNew(excC,"Failed to get byte from buffer");
    // Release the Allocated Resources 
    jniEnv->DeleteLocalRef(excC);
    jniEnv->DeleteLocalRef(byteBufC);        
    // Return with Pending RuntimeException
    return;
  }
  ...
}

有两个函数用于检测 JNI 本地函数中的异常:

ExceptionOccurred 函数如果存在尚未处理的挂起异常,则返回一个 jthrowable 对象引用,如果没有异常准备就绪,则返回 null。

当 JVM 中存在未处理的未处理异常时,ExceptionCheck 函数返回 jboolean,该函数将返回 JNI_TRUE 作为结果。

假设我们不想使用 ExceptionOccurred 返回的 jthrowable,我们将使用 ExceptionCheck 来检测异常并进入异常处理代码分支。

然后,使用 ExceptionDescribe 函数,我们将打印当前挂起的 throwable 栈跟踪到错误输出,而使用 ExceptionClear,我们将从 JVM 中清除挂起的 IndexOutOfBoundsException

由于我们只能在 Activity 函数中处理 RuntimeException,我们将向 JVM 附带一个 RuntimeException,以便在本地代码返回时立即处理。

总结来说,由于我们打算停止本地函数执行,我们必须在从本地函数返回之前释放任何资源或 JNI 引用。

是的,借助这些 JNI 异常,您应该能够检测和处理由 Java 方法调用引起的任何已解决的未处理异常。如前所述,在尝试安全调用其他 JNI 方法之前,手动处理任何挂起的异常至关重要,这些方法调用 memberstatic 函数,getset 对象上的字段,甚至创建新对象。

从本地代码与 Java 监视器交互

到目前为止,我们一直在 Java 线程中使用synchronized语句或synchronized方法同步对共享资源的访问:

synchronized (obj) { ... // synchronized block }
synchronized void incrementCount() { ... // synchronized methods }

当我们执行本地方法并希望访问多个 Java 代码和本地代码之间共享的资源或变量时,JNI 为我们提供了MonitorEnterMonitorExit方法来控制对由 Java synchronized块管理的互斥区的访问:

jint MonitorEnter(JNIEnv *env, jobject obj);
jint MonitorExit(JNIEnv *env, jobject obj);

MonitorEnter是负责获取 Java 监视器范围的访问的函数,当另一个本地线程或 Java 线程是监视器的所有者时可能会阻塞。当任何线程获得对块的访问时,JVM 将确保除了当前线程外,没有其他线程进入临界区。

MonitorExit是负责释放之前使用MonitorEnter获得的监视器的函数,给其他线程进入互斥区的机会。

注意

为了防止死锁条件,任何MonitorEnter调用都必须跟随一个MonitorExit调用。

在我们的下一个代码示例中,我们将演示这种技术来同步 Java 代码和本地代码使用的共享对象的访问。

我们将创建一个本地线程,该线程将不断从我们在Activity中管理的共享队列列表中轮询命令请求。

StatsActivity将有一个按钮用于将命令推送到共享队列列表,并将由本地线程发送的请求响应显示在TextView中。而 UI 将在主线程中将命令推送到请求队列列表,本地代码将尝试在后台线程中从本地代码中拉取命令,两者都需要对共享队列列表进行同步访问。

我们的命令将要求本地层发送有关程序运行所需主内存数量的信息。一旦收到响应,它将在 UI 的TextView中打印出来。

首先,让我们从 UI 的角度定义推送新命令的代码:

public class StatsActivity extends Activity {
     // Memory RSS(Resident Set Size) SIZE Retrieve size Request
  public static final int MEM_RSS_REQUEST = 0;

  // Shared Resource between Java and Native
  Queue<Integer> requests = new LinkedList<Integer>();
  Object queueLock = new Object();

  OnClickListener onRSSReqListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      synchronized (queueLock) {
        requests.add(MEM_RSS_REQUEST);
      }
    }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    RSSButton.setOnClickListener(onRSSReqListener);
  }
}

注意,在onRSSReqListener将新请求命令推送到队列列表之前,它获取了对由queueLock对象控制的同步部分的访问。

由于queueLock对象将作为访问共享资源的保护对象,我们必须将其传递给本地代码。

由于我们已编写了命令请求消费者,现在我们将关注命令请求消费者,名为CPUStatThread的本地 C++ JavaThread子类,它将处理请求并发送命令响应。

如前所述,CPUStatThread将实现run方法,并通过ActivityHandler发送响应,因此我们首先在新的源代码文件stats.cpp中实现run方法以从Activity检索请求:

#include "thread.h" // Header where JavaThread is defined

static const int RSS_REQUEST= 0;

class CPUStatThread: public JavaThread {

  // Reference to the Activity and received on the constructor
  jobject activityObj; 
  ...
  virtual void run() {
    while ( !shouldStop ) {
      std::unique_lock<std::mutex> lck(stopMutex);
      processMessage();
      // Wait until a stop signal is sent
         stopCond.wait_for(lck,std::chrono::milliseconds(200));
    }
  }
  void processMessage(){

    jclass activityClass = jniEnv()->GetObjectClass(activityObj);

    // Retrieve the QueueLock(lockObj) and the Handler 
    // Fields(handlerObj) objects from Activity and 
    // getNextRequest methodId
	 ... 
    // Acquire the queue monitor	 
    jniEnv()->MonitorEnter(lockObj);

    // Retrieve the next command request to be processed
    int requestCode = jniEnv()->CallIntMethod(activityObj, 
                                              getNextRequestMid);
    switch (requestCode){
      case RSS_REQUEST:
        LOGI("Received a RSS Request");
        sendRSSMessage(handlerObj);
        break;
    }
    // Release the queue monitor	
      jniEnv()->MonitorExit(lockObj);
	// Release local References to avoid leaks
   ...
  }
};

我们的运行方法将从StatsActivity中检索queueLock字段,并在获取到由queueLock控制的同步块访问权限后,使用StatsActivitygetNextRequest方法从队列中拉取一个新的请求:

public class StatsActivity extends Activity {

  int getNextRequest(){
    return requests.size()> 0 ? requests.remove():-1;
  }
}

getNextRequest将在没有可处理的内容时返回-1,因此我们的线程将根据我们在运行方法中定义的,休眠 100 毫秒。

当接收到RSS_REQUEST时,我们的原生后台将在sendRSSMessage方法中处理它,并通过系统进程消耗的内存发送一个响应。

包装原生数据对象

到目前为止,为了从原生代码向 Java 代码发送任何类型的结构化数据,我们一直在构建和分发常规 Java 对象。然而,为了减少从原生类型转换为 Java 类型以及相反所需的开销,发送原生包装结构或对象指针到 Java 运行时可能是有意义的,而不是在原生代码中创建纯 Java 对象。

最可靠的技术是将原生地址存储在包装对象的 64 位和 32 位指针兼容的长成员变量中:

public class MyObject {
  // Transports a pointer to original 
  // native object or structure
  long nativePtr; 
}

如你所知,JVM 垃圾回收器会持续维护堆内存并清理未引用的对象,以便为应用程序下一次所需的分配释放更多内存。然而,这并不适用于使用new运算符或malloc函数在动态内存中分配的原生对象。

当我们在原生堆中创建一个对象时,我们总是必须显式地使用delete运算符或free函数来释放它,因此为了强制所有包装原生对象的内存清理,我们将定义一个接口,该接口定义了释放底层原生对象所需的功能:

public interface Disposable {
  // Releases the native objects wrapped on the object
  void dispose();
}

注意

尽管我们可以在对象被垃圾回收时使用finalize方法来释放任何原生资源,但无法保证垃圾回收器会在未来的任何特定时间调用finalize方法。

为了在我们的示例中演示这种技术,我们将使用Handler发送回一个包裹在 Java 对象中的原生CPUStat``struct

让我们先定义在发送时携带与进程内存消耗相关的信息的原生CPUStat

struct CPUStat{
    double vm; // Virtual Memory Size
    double rss; // Process Resident Memory    
    // constructor 
    CPUStat( double &vm_,
             double &rss_):vm(vm_),rss(rss_){}
};

And its Java counterpart:

public class JCPUStat implements Disposable {    
    // Reference to the native struct stored on a long
    long nativePtr;

    public JCPUStat(long nativePtr){
        this.nativePtr = nativePtr;
    }
    native long getRSSMemory();

    @Override
    public native void dispose();
}

注意,我们的JCPUStat实现了之前解释的可丢弃对象,因此剩下的只是编写 JCPUStat 类的原生方法:

   // Generic function to convert a nativePtr member to a T pointer 
template <typename T>
T * getNativePtr(JNIEnv * env, jobject obj) {
  jclass c = env->GetObjectClass(obj);
  jfieldID nativePtrFID =  env->GetFieldID(c, "nativePtr", "J");
  jlong nativePtr = env->GetLongField(obj, nativePtrFID);
  return reinterpret_cast<T * >(nativePtr);
}

void unsetNativePtr(JNIEnv * env, jobject obj) {
  jclass c = env->GetObjectClass(obj);
  jfieldID nativePtrFID =  env->GetFieldID(c, "nativePtr", "J");
  env->SetLongField(obj, nativePtrFID, 0);
}

void Java_com_packpublishing_asynchronousandroid_chapter9_JCPUStat_dispose(JNIEnv *env, jobject obj){

   // Retrieves the pointer to the original structure
   CPUStat *stat = getNativePtr<CPUStat>(env,obj);
   if ( stat != 0 ) {
     delete stat;  // Releases the memory allocated to stat
     unsetNativePtr(ev,obj); 
   }
   }

为了简化原生引用的处理,我们创建了两个通用函数来操作包装对象中的nativePtr字段。第一个函数getNativePtr将从对象中获取指针字段,并借助reinterpret_cast将存储在CPUStat指针中的原始长值转换。

在我们获取到原始指针后,我们可以调用delete运算符来释放系统中的内存,并将 nativePtr 设置为 0。将指针设置为 0 可以防止在错误地调用dispose方法两次时发生双重释放。

接下来,在定义了包装类之后,我们将处理原始请求,并构建一个JCPUStat响应对象,使用活动的Handler将其发送回Activity

// Function to retrieve the Memory Usage 
void CPUStatThread::processMemUsage(
  double& vm_usage, double& resident_set){...}

void CPUStatThread::sendRSSMessage(jobject & handlerObj) {

  double vm, rss;
  // Read the mempory usage
  processMemUsage(vm, rss);
  jclass jCpuStatClass = jniEnv()->FindClass(
    "com.packpublishing.asynchronousandroid.chapter9.JCPUStat");

  // Find the JCPUStat Constructor
  jmethodID  jCpuConstructorMid = jniEnv()->GetMethodID(
    jCpuStatClass, "<init>", "(J)V");
  // Create a native CPUStat object 
  CPUStat * cpuStat = new CPUStat(vm, rss);

  // Wrap the native object on a JCPUStat object  
  jlong nativePtr = reinterpret_cast<jlong>(cpuStat);
  jobject jCpuStat = jniEnv()->NewObject(
    jCpuStatClass, jCpuConstructorMid, nativePtr);

  // Get the Handler Reference and send Message
  ...    
  // Build up the Response Message with the 
  jobject messagObj = jniEnv()->CallObjectMethod(
     handlerObj, obtainMId, RSS_RESPONSE, jCpuStat);

  // Push the message to the main Thread Handler                   
  jniEnv()->CallBooleanMethod(handlerObj, sendMsgMId, messagObj);
  // Clean up the local references
  ...
  }

我们的sendRSSMessage函数将使用系统功能计算进程消耗的内存,并构建一个包装本地 C++结构的JCPUStat对象。之后,使用sendRSSMessage函数传入的活动处理程序成员对象将JCPUStat调度到主线程。最后,我们清理了在局部作用域中创建的所有局部引用。

完整的源代码可在 Packt Publishing 网站上找到。查看完整的源代码,以了解我们是如何确定当前进程消耗的内存的。

为了完成示例,我们将更新StatsActivity以在Handler上处理 RSS 命令响应:

public class StatsActivity extends Activity {
  public static final int MEM_RSS_REQUEST = 0;
  public static final int MEM_RSS_RESPONSE = 1;
  public Handler myHandler = new Handler() {
    public void handleMessage(Message msg) {
      switch (msg.what){
        case MEM_RSS_RESPONSE:
          TextView tv = (TextView) findViewById(R.id.console);
          JCPUStat stat = (JCPUStat) msg.obj;
          tv.setText("Memory Consumed is "+stat.getRSSMemory());
          // Releases the native object and frees the memory
          stat.dispose();
          break;
      }
    }
  };
  ...
  public native void startCPUStatThread();
  public native void stopCPUStatThread()
}

一旦我们从Message对象中获取到JCPUStat,我们就使用其本地方法getRSSMemory读取 RSS 内存,然后在控制台 UI 小部件上打印结果。

正如我们之前解释的,JCPUStat.dispose方法在 Java 运行时被显式调用,以销毁由后台线程发送给我们的本地对象。JVM GC 不会清理本地对象,因此我们必须调用dispose以释放附加到Disposable对象的本地资源。

getRSSMemory方法类似于dispose方法,将利用nativePtr字段从本地对象检索存储的 RSS 值。让我们看看它的样子:

jlong Java_com_packpublishing_asynchronousandroid_chapter9_JCPUStat_getRSSMemory(JNIEnv *env, jobject obj) {
    CPUStat *stat = getNativePtr<CPUStat>(env,obj);
    return (jlong)stat->rss;
}

为了简洁,省略了startCPUStatThreadstopCPUStatThread,因为它们与之前示例中启动本地线程所使用的代码非常相似——请参阅可下载的示例以获取完整代码。

太好了!我们学习了如何将本地对象包装在 Java 对象中,我们定义了一个接口,在本地对象不再需要时从 Java 对象中清除本地内存,我们还学习了如何通过调用对象构造函数从本地 Java 对象创建对象。

摘要

在本章中,我们向您介绍了 JNI,这是一个在 Java 上可用的标准 API,用于与用汇编、C 或 C++编写的本地代码交互,它对安装了 Android NDK 套件的任何 Android 开发者都可用。

在第一部分,我们解释了如何在 Android Studio 上设置带有 JNI 代码的项目,以及如何从您的应用程序中的任何 Java 类调用 C 函数和 C++成员函数。

之后,我们使用 JNI 接口在本地函数上执行一个Loader异步后台工作。本地函数能够在由AsyncTaskLoader创建的 Java 后台线程中将彩色图像转换为灰度图像。

接下来,我们将发现如何将使用 C++标准库创建的纯本地线程附加和分离到 JVM。附加的线程作为一个正常的 Java 线程工作,并管理自己的 JNI 环境、资源和引用。

同时,我们还发现了 JNI 全局引用和局部引用之间的差异,以及如何从本地代码作用域访问 Java 对象字段。

我们还学习了一种将原生对象包装在 Java 对象中的技术,并定义了一个具体接口来处理附加到 Java 对象上的 JNI 资源。

在本章结束时,我们学习了如何检测和处理在 JVM 上由 Java 函数抛出的挂起异常。

本章中解释的所有技术,您应该能够将任何用 C/C++ 编写的代码集成到您的异步后台执行中。除此之外,您还可以利用原生代码来优化应用程序中的关键功能或集成一些原生实用库。

在下一章中,我们将学习如何使用 Google GCM 从您的服务器高效地推送和拉取实时消息,以及如何使用 Google Play Services 框架安排工作。

第十章:使用 GCM 的网络交互

在前面的章节中,为了更新我们示例所需的任何类型的动态数据,我们明确地启动了一个与远程服务器的连接,唤醒了网络射频和其他执行网络操作所需的资源。应用程序可能会获取新鲜数据,或者如果自上次获取以来没有变化,则获取完全相同的数据。

虽然这种通信-获取模型可能适用于大多数用例,但当数据不经常更改时,它可能会无谓地消耗电池资源和互联网带宽。

这种通常称为数据轮询的技术,当大量客户端尝试获取或验证数据是否已更改时,也可能增加服务器的负载。

轮询技术的替代方法是数据推送。在这种技术中,服务器告诉应用程序何时有新数据可用或数据已更改。当数据消费者(应用程序)收到通知时,它将启动与服务器的新交互以检索新鲜数据。

由于所需的同步较少,这将导致更少的网络交互,进而导致消耗更少的电池资源。

在本章中,我们将向您介绍Google Cloud MessagingGCM),这是由 Google Play 服务提供的一项服务,可以帮助您构建需要数据推送或拉取消息服务的应用程序。GCM 提供了一种框架,以节能的方式向多个设备或设备组发送推送消息。

在本章中,我们将涵盖以下主题:

  • 轮询与推送消息对比

  • 如何为您的应用程序设置和配置GCM

  • 使用 GCM 接收来自服务器的下行消息

  • 从 GCM 主题流接收下行消息

  • 使用 GCM 向您的服务器发送上行消息

  • 使用GcmNetworkManager注册一次性网络任务和周期性网络任务

GCM 简介

由于每次与您的服务器进行网络交互都可能唤醒无线射频接口,在能源资源有限的设备上,最小化您的应用程序尝试连接网络以同步数据的次数至关重要。

对于需要定期更新和最新数据的应用程序,如消息应用,通过每 x 分钟设置一个闹钟在后台轮询,然后唤醒射频并下载数据,可能会在几小时内耗尽电池。

GCM 简介

图 1 - 从远程服务器轮询数据

GCM 为我们提供了一个平台,以小于 4096 字节的方式高效地发送通知,当有新数据要消费或同步时。这种交互模型减少了网络交互,无需不断轮询服务器以发现数据变化。

GCM 简介

除了从您的服务器(使用 HTTP 或 XMPP 协议消息)向您的 Android 应用程序发送下游消息的能力之外,GCM 框架还提供了一个省电的通信通道,用于从您的应用程序向由您管理的 XMPP 服务器发送上游消息。

在 Android 设备上运行的 GCM 客户端提供了一个可靠且省电的连接,连接您的 GCM 服务器和设备。保持的连接高度优化,以最小化带宽和电池消耗。因此,对于需要高频网络数据更新的应用程序,如实时消息,使用 GCM 极为推荐。

此外,当设备离线且无法联系 GCM 服务时,平台能够保留队列中的消息,直到达到最多 20 条队列消息,并确保设备再次上线后立即投递消息。

为您的应用程序设置和配置 GCM。

要在您的应用程序上设置 Google 云消息,您需要在 GCM 上注册并在您的 Google 开发者控制台(developers.google.com/mobile/add)上设置一个 Google API 项目:

  1. 首先选择 Android 应用程序平台

  2. 指定您的应用程序名称。

    示例:Asynchronous Android

  3. 提供您的应用程序包名。

    示例:com.packpublishing.asynchronousandroid

  4. 选择 云消息服务启用 Google 云消息

  5. 生成配置文件并将 JSON 配置文件 google-services.json 下载到您的计算机上。

  6. 将您的凭证(服务器 API 密钥、发送者 ID)保存下来,以便在 GCM 平台上进行身份验证。

一旦您已在 GCM 上注册了您的应用程序,获取 google-services.json 配置文件并将其复制到您的 Android Studio 项目的 app/mobile/ 目录中。

接下来,将 Google Play 服务 SDK 添加到您的项目级别和应用程序级别的 <PROJECT_DIRECTORY>/build.gradle 文件中,并重新构建您的 Android Studio 项目:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.google.gms:google-services:1.5.0-beta2'
    }
}
..

更新应用程序模块构建 <PROJECT_DIRECTORY>/app/build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "21.1.1"
    defaultConfig {
        applicationId "com.packpublishing.asynchronousandroid"
        minSdkVersion 9
        targetSdkVersion 23
    }    
}
dependencies {
  ...
  compile 'com.google.android.gms:play-services-gcm:8.3.0'
}
apply plugin: 'com.google.gms.google-services'

要在您的 Android 应用程序上使用 GCM,您需要一个安装了 Android API 8 或更高版本且已安装 Google Play 商店的设备,或者如果您想使用 Google Play 服务提供的 GCM 新功能,则需要一个 API 级别为 9 的设备。

在我们的构建文件中声明了 Google 服务库依赖项后,我们就可以开始在应用程序上启动 GCM 基础设施。

要在您的应用程序中使用 GCM,您必须在您的 AndroidManifest.xml 文件中注册以下权限:

<uses-permission android:name="android.permission.INTERNET" />
<!-- Required to wakeup the device and deliver messages --> 
<uses-permission android:name="android.permission.WAKE_LOCK" />

<permission android:name="<Package>.permission.C2D_MESSAGE"
        android:protectionLevel="signature"/>
<uses-permission android:name="<Package>.permission.C2D_MESSAGE"/>
   ...
</manifest>

注意

注意,您应将 <Package> 替换为您独特的应用程序包名,例如 com.packpublishing.asynchronousandroid

注册 GCM 接收器

为了从 GCM 平台接收广播意图,我们将向 AndroidManifest.xml 应用程序元素添加 GCM GcmReceiver,这是 GCM 库提供的 WakefulBroadcastReceiver 子类:

<receiver
  android:name="com.google.android.gms.gcm.GcmReceiver"
  android:exported="true"
  android:permission="com.google.android.c2dm.permission.SEND" >
  <intent-filter>
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
      <category android:name="<Package>" />
  </intent-filter>
</receiver>

BroadcastReceiver 在从 GCM 服务器接收到新的下行消息时接收一个意图,因此需要订阅具有操作 com.google.android.c2dm.intent.RECEIVE 的意图。

设置注册服务

为了从 GCM 平台接收下行消息,Android 应用程序需要一个注册令牌。注册令牌是由 GCM 服务器签发的秘密 ID,必须获取以在服务中标识设备。

为了获取注册令牌,我们将定义一个 IntentService,它将使用实例 ID API 获取注册令牌。让我们首先在 AndroidManisfest.xml 中定义它:

  <service android:name=".chapter10.RegistrationIntentService"
           android:exported="false">
  </service>

我们的 IntentService 子类将在后台使用从 GCM 注册返回的 SenderId 获取新的注册令牌。一旦接收到新的注册令牌,它将被发送到我们的服务器以安全存储。该令牌是我们的通行证,用于访问 GCM 服务,因此,为了提交通知,服务器必须提供此令牌。在设备上,注册将由 GCM 框架隐式安全存储。

public class RegistrationIntentService extends IntentService {

  @Override
  protected void onHandleIntent(Intent intent) {

    SharedPreferences sharedPreferences = PreferenceManager.
      getDefaultSharedPreferences(this);

    try {
      // Get the InstanceID Singleton
      InstanceID instanceID = InstanceID.getInstance(this);

      Log.i(TAG, "\n-----------------------------------------\n" +
                 " GCM App instance UUID: " + instanceID.getId() +
                 "\n-----------------------------------------\n"
            );

      // Retrieve the Sender Id from GCM Registration
      String senderId = getString(R.string.gcm_defaultSenderId);

      // Retrieve a token with a sender ID
      String token = instanceID.getToken(senderId, 
       GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);

      // Save the Registration to the server
      sendRegistrationToServer(token);

   sharedPreferences.edit().
     putBoolean(MyChatActivity.SENT_TOKEN_TO_SERVER, true).
       apply();

    } catch (Exception e) {
      Log.d(TAG, "Failed to get registration token", e);
   sharedPreferences.edit().
     putBoolean(MyChatActivity.SENT_TOKEN_TO_SERVER, false).
       apply();    
    }
  }
}

一旦成功接收到注册令牌,我们更新默认应用程序共享首选项文件,将 SENT_TOKEN_TO_SERVER 设置为 true。此属性指示生成的令牌是否已发送到您的服务器。如果属性为 false,我们将发送令牌到您的服务器。否则,您的服务器应该已经收到了令牌。

如果在调用 sendRegistrationToServer 期间获取新令牌或更新我们服务器上的注册令牌时发生异常,我们将设置 SENT_TOKEN_TO_SERVERfalse,确保稍后执行新的尝试。

尽管您可能希望将注册信息持久化到您的后端服务器,但到目前为止,我们将打印注册令牌到日志输出。您可以使用 logcat 选择该值以供将来在我们的示例中使用。

  private void sendRegistrationToServer(String token) {       
    Log.i(TAG, " GCM Registration Token: " + token );
  }

实例 ID 监听器

第一次通过 InstanceID.getInstance 获取 InstanceID 时,会生成一个 UUID 应用标识符以在 GCM 平台上标识应用程序。

如果以下情况发生,实例 ID 可能会变得无效:

  • 应用程序明确删除实例 ID (Instance.deleteToken)

  • 设备进行了出厂重置

  • 应用程序被卸载

  • 用户清除应用程序数据

为了接收通知,每次注册令牌需要刷新时,我们将创建一个扩展 InstanceIDListenerService 的服务,注册到 com.google.android.gms.iid.InstanceID 意图,并将其包含在 AndroidManifest.xml 中:

<service
  android:name=".chapter10.MyInstanceIDListenerService"
  android:exported="false">
  <intent-filter>
   <action android:name="com.google.android.gms.iid.InstanceID" />
  </intent-filter>
</service>

public class MyInstanceIDListenerService 
   extends InstanceIDListenerService {
   @Override
   public void onTokenRefresh() { 
     // Starts the Registration Service to obtain a new token
     Intent intent = new Intent(this, 
                                   RegistrationIntentService.class);
     startService(intent);     
     sharedPreferences.edit().
     putBoolean(MessagingActivity.SENT_TOKEN_TO_SERVER, false).
     apply();    
   }
}

当注册令牌需要刷新时,将调用 onTokenRefresh 回调。这可能发生在前一个令牌的安全受到损害的情况下,例如令牌被可疑使用。此过程通常由 instanceID 提供商启动。

Instance ID API 用于管理授权您的应用程序或您的服务器与 GCM 服务交互的安全令牌。

InstanceID 监听器

除了创建新的令牌外,InstanceID 单例实例还能够删除令牌或甚至使 InstanceID 失效。

void  deleteInstanceID()
void  deleteToken(String authorizedEntity, String scope)

接收下行消息

在设置 GCM 客户端所需的基本块已经就绪的情况下,在我们的第一个 GCM 示例中,我们将通过 GCM 平台发送一条简单的下行消息,并将其作为通知打印到 Android 通知抽屉中。

为了处理 GCM 消息,我们需要实现一个继承自 GcmListenerService 的服务,并重写 onMessageReceived(String,Bundle) 方法。由于 GcmReceiver 继承自 WakefulBroadcastReceiver,可以保证 CPU 将会保持唤醒状态直到服务完成消息的投递。

我们的 GcmListenerService 子类将在接收到消息后立即创建一个 Android 通知。

public class NotificationGCMHandler extends GcmListenerService {

  public static final int NOTIFICATION_ID ="GCMNotification".
                                            hashCode();

  @Override
  public void onMessageReceived(String from, Bundle data) {

    String msgType = data.getString("type");

    // Notification Message received from GCM.
    if ( msgType.startsWith("my_notifications") ) {
      createNotification(data.getString("title"),
                         data.getString("body"));
    }
  }
  private void createNotification(String title, String body) {   
   // Elided for brevity...
 }  
  }

我们还需要在 AndroidManifest.xml 中注册我们的 GcmListenerService 服务类,将服务注册为接收 com.google.android.c2dm.intent.RECEIVE 动作:

  <service android:name=".chapter10.NotificationGCMHandler"
           android:exported="false" >
     <intent-filter>
        <action 
        android:name="com.google.android.c2dm.intent.RECEIVE"/>
     </intent-filter>
  </service>

为了启动与 GCM 的初始注册,我们将创建一个 Activity,该 Activity 将启动 RegistrationServiceIntentService 以检索所需的令牌。然而,在我们尝试检索令牌之前,我们必须检查设备上是否提供了 Google Play 服务,并且安装的版本不早于客户端要求的版本。

让我们从实现 Activty.onCreate 方法开始,触发与 GCM 平台的交互:

public class MyChatActivity extends Activity {
  public static final String SENT_TOKEN_TO_SERVER = "sent2Server";
  private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (checkPlayServices()) {
      Log.i(LOG_TAG, "Registering to GCM");
      SharedPreferences sharedPref = PreferenceManager.
                                     getDefaultSharedPreferences(this);
      // Registering is started when there is no available token
      boolean sentToken = sharedPref.
                          getBoolean(SENT_TOKEN_TO_SERVER, false);
      if (!sentToken) {
        //...Print an error
      }
      Intent int = new Intent(this, RegistrationIntentService.
                              class);
      startService(int);
    }
  }
}

在我们开始注册服务之前,checkPlayServices 将验证设备上是否安装了 Google Play 服务。如果服务不可用,将向用户显示一个对话框,允许用户从 Play 商店下载它或在设备系统设置中启用它:

private boolean checkPlayServices() {

  // Returns the singleton instance of GoogleApiAvailability.
  GoogleApiAvailability apiAvailability = GoogleApiAvailability.
                                          getInstance();
  // Verify if the Google Play Service installed is 
  // installed and compatible with GCM Library used
  int rc = apiAvailability.isGooglePlayServicesAvailable(this);
  if ( rc != ConnectionResult.SUCCESS ) {

    // The error can be resolved with a user action
    if (apiAvailability.isUserResolvableError(rc)) {

      // Shows a user action dialog to resolve the issue
      apiAvailability.getErrorDialog(this, 
        rc,PLAY_SERVICES_RESOLUTION_REQUEST).show();
    } else {
      Log.i(TAG, "This device is not supported.");
     // Finishing the Activiy
      finish();
    }
    return false;
  }
  return true;
}

isGooglePlayServicesAvailable 返回成功时,我们从函数中返回 true 并启动注册服务。

当函数返回一个可以通过用户操作解决的问题时,例如 SERVICE_VERSION_UPDATE_REQUIRED,将向用户显示一个本地化对话框以纠正问题。如果 Google Play 服务过时或缺失,对话框可能会将用户重定向到 Play 商店,或者如果设备上已禁用 Google Play 服务,则重定向到系统设置。

如果返回的错误不能通过用户操作解决,我们只需结束当前的 Activity 并打印一条日志消息,因为设备将无法在 GCM 中注册并接收下行消息。

是的!我们完成了 GCM 引导程序的应用程序,一旦我们启动Activity并注册到 GCM,设备就准备好接收 GCM 的下行消息。

记住,我们的注册服务会将注册令牌打印到日志输出中,所以当你第一次运行MyChatActivity时,不要忘记记录它。

I ...:  GCM Application Instance Identifier: <InstanceId>
I ...:  GCM Registration Token: <Registration Token>

要与 GCM 交互,你可以设置一个使用服务器凭证连接到 GCM 服务的 HTTP 或 XMPP 后端服务器。为了简单和测试,我们将直接构建和提交 HTTP 消息。

要向我们的设备发送下行消息,我们必须发送一个包含 JSON 对象的 HTTP POST 消息,在有效载荷中设置to字段为我们的注册令牌,以及一个data对象字段,包含我们的自定义通知属性:titlebodytype

这里是一个 JSON 格式的消息,一旦NotificationGCMHandler从 GCM 接收到它,就会生成一个 Android 通知:

{
  "data": {
    "title": "Hello from GCM",
    "body": "Hello from your fake server",
    "type": "my_notifications"
   },
   "to": "<DeviceRegistrationToken>"
}

要将 HTTP 消息提交到 GCM 平台,你可以使用 curl 命令或使用 chrome 网络应用程序 Postman(www.getpostman.com/)。以下是提交先前消息到 GCM 的 curl 命令:

$ curl --request POST \
    --url https://gcm-http.googleapis.com/gcm/send \
    --header 'authorization: key=<Server API Key>' \
    --header 'Content-Type: application/json' \
    --data '{"data":{"title":"Hello from GCM","body":"Hello from   
   your fake server","type":"notification"},
            "to":"<DeviceRegistrationToken>"}'

不要忘记将<Server API Key>替换为在 Google Cloud Console 注册时生成的 API 密钥,并将<DeviceRegistrationToken>替换为你设备生成的令牌。请注意,下游数据消息的最大有效载荷为 4KB。

如果你的 GCM 设置一切顺利,你的数据对象属性将传递到数据包对象中的onMessageReceived()方法,GCM 服务将返回一个包含类似以下消息体的 HTTP 响应(200):

{
  "multicast_id": 6425212369847183592,
  "success": 1,
  "failure": 0,
  "canonical_ids": 0,
  "results": [{
    "message_id": "0:1456441876781708%69ee9872f9fd7ecd"
  }]
}

接收主题消息

下行消息允许我们发送短(4KB)消息来提醒用户有新的更新、新内容或甚至提醒。

下行消息是一个单向通信通道,用户可以接收消息,但不能直接响应它们或采取任何立即行动。

要构建交互式体验,例如聊天系统,我们必须支持双向通信,用户可以接收下行消息,也可以向其他设备或设备组发送上行消息。

在我们的下一个示例中,我们将基于 GCM 上行消息和主题消息功能构建一个简单的群组消息系统。群组消息系统将允许多个设备向共享消息通道发布文本消息。

GCM 主题消息允许你的后端服务器向具有特定主题的设备发送消息。一旦 GCM 收到特定主题的消息,它将透明地使用在 GCM 平台上管理的已订阅设备列表路由和传递消息。

主题由以下正则表达式后面的名称标识:

      /topics/[a-zA-Z0-9-_.~%]+

要开始接收与特定主题名称相关的消息,一个 GCM 注册客户端应用程序必须使用自己的注册令牌和所需的主题流在 GCM 中进行订阅。

首先,我们将更新我们的 RegistrationIntentService,使用接收到的注册令牌将我们的应用程序订阅到 "/topics/forum" 消息流:

public class RegistrationIntentService extends IntentService {

  private static final String TOPIC_NAME = "forum";

  @Override
  protected void onHandleIntent(Intent intent) {
    ...
    // Retrieve the token
    String token = instanceID.getToken(senderId, 
        GoogleCloudMessaging.INSTANCE_ID_SCOPE,null);
    ...
    // Subscribe to Topics
    subscribeTopics(token);
  }

  private void subscribeTopics(String token) {
    GcmPubSub pubSub = GcmPubSub.getInstance(this);
    try {
         pubSub.subscribe(token, "/topics/ " + TOPIC_NAME, null);
    } catch (Exception e) {
        Log.e(TAG, "Failed to subscribe to " + TOPIC_NAME, e);
    }
  ...
}

要将设备从 GCM "论坛" 主题中取消订阅,我们可以使用注册令牌和主题名称调用 GcmPubSubunsubscribe() 方法。

主题消息以与我们在上一个示例中推送通知 GCM 消息相同的方式发送到我们的 GcmListenerService (NotificationGCMHandler)。主题消息发送到我们的应用程序,其中 from 字段存储主题名称 /topics/forum

我将给出一个关于我们主题的典型主题消息的示例:

{ 
  "to": "/topics/forum",
  "data": {
    "username": "heldervasc",
    "text": "I need to learn more about Android Development"
  }
}

数据对象字段是消息上的字段,我们可能用它来向应用程序传递自定义属性。在我们的示例中,它携带有关用户编写的用户名和文本的信息。

接下来,考虑到 NotificationGCMHandler 将接收从 GCM 发送的主题消息,我们将更新它以处理接收到的主题消息,并将每个主题消息广播到任何本地 BroadcastReceiver

我们的 NotificationGCMHandler 将简单地封装主题消息到 Intent 中,并将它们调度到您进程内的本地 Activity。这种异步通信技术,在之前的章节中已解释,由于您的消息不会离开您的应用程序,因此更快、更安全:

public class NotificationGCMHandler extends GcmListenerService {

  public static final String FORUM_TOPIC = "/topics/forum";
  public static final String USERNAME_KEY = "username";
  public static final String TEXT_KEY = "text";
  public static final String MSG_DELIVERY = "asyncforum";

  @Override
  public void onMessageReceived(String from, Bundle data) {

       // Verify if it is a forum message
    if (from.equals(FORUM_TOPIC)) {

     // Build an intent from the forum topic message. 
      Intent intent = new Intent(MSG_DELIVERY);
      intent.putExtra(USERNAME_KEY, data.getString(USERNAME_KEY));
      intent.putExtra(TEXT_KEY, data.getString(TEXT_KEY));

	  // Broadcast the intent to local interested objects
     LocalBroadcastManager.
       getInstance(this).sendBroadcast(intent);
    } else ... {
      ...
    }
  }
}

随着 GcmListenerService 将从我们的消息主题接收到的消息转发,现在是时候构建一个将要显示接收到的消息并使用 GCM 上行消息将消息发布到群聊的 Activity 了。

从上一章完成的工作开始,我们将创建一个 MessagingActivity,它还将验证 Google Play 服务是否可用,并在没有注册令牌的情况下启动 RegistrationIntentService

public class MessagingActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.chat_layout);
    if (checkPlayServices()) {
     ...    
    }
  }   
}

要在我们的 Activity 中接收和显示主题消息,我们将创建一个匿名 BroadcastReceiver 子类,该类可以动态注册和注销接收动作为 MSG_DELIVERY 的本地 Intent。

由于我们只想在 Activity 处于前台时接收主题消息,我们将在 onResumeonPause 回调中注册和注销到本地广播:

public class MessagingActivity extends Activity {
     ...
  @Override
  protected void onResume() {
    super.onResume();

    // Create an intent filter to receive forum Intents
    IntentFilter filter = new IntentFilter(
      NotificationGCMHandler.MSG_DELIVERY);

    // Register the local Receive to receive the Intents
    LocalBroadcastManager.getInstance(this).
      registerReceiver(onMessageReceiver, filter);
  }

  @Override
  protected void onPause() {
    super.onPause();
    // Unregister the Local Receiver
    LocalBroadcastManager.getInstance(this).
      unregisterReceiver(onMessageReceiver);
  }
}

剩下的工作就是在 UI 上显示我们的群聊消息,并实现接收广播 Intent 并使用消息用户名和文本更新 UI 的 BroadcastReceiver

要处理广播 Intent,我们必须覆盖 BroadcastReceiveronReceive 方法以接收本地 Intent

BroadcastReceiver onMessageReceiver = new BroadcastReceiver(){

  @Override
  public void onReceive(Context context, Intent intent) {

      TextView chatText = (TextView)findViewById(R.id.chatWindow);
      String username = intent.getStringExtra("username");
      String bodyText = intent.getStringExtra("text");
      String line = String.format("%s : %s%n", username,bodyText)
      // Prepend the message 
      chatText.setText( line + chatText.getText().toString());
  }
};

现在,如果您使用以下 curl 命令向 GCM 提交主题消息,您将在 UI TextView上看到一个新消息弹出:

curl --request POST  \
--url "https://gcm-http.googleapis.com/gcm/send" \
--header 'authorization: <SERVER_API_KEY>'  \
--header 'Content-Type: application/json'  \
--data '{ "data": {  
        "username": "heldervasc",
        "text": "Welcome to Asynchronous Android group chat"
     },       
    "to": "/topics/forum"
   }'

发送上行消息

尽管我们能够接收聊天群组消息,但我们无法与应用程序的消息流进行交互。此外,为了使用 GCM 平台发送和处理上游消息,需要一个实现 XMPP 连接服务器协议的应用程序服务器来连接到 GCM 服务器并接收上游 XMPP 消息。

为了处理我们的群组消息,我们构建了一个非常基本的 XMPP 服务器,该服务器处理来自设备的上游消息,并将消息转发到主题消息。

基本 XMPP 服务器源代码可在 Packt Publishing 网站上找到。从 Packt 网站获取它,在运行之前,请更新GCMServer.java类文件中的静态字段,包括你的SenderIDServerKey

private static final String SENDER_ID = "<YOUR_SENDER_ID>"; 
private static final String SERVER_KEY = "<SERVER_KEY>";

服务器将连接到 GCM 平台,启动一个 XMPP 会话,并处理发送到<SENDER_ID>@gcm.googleapis.com的所有消息。

要生成上游消息,我们在 UI 上创建了一个EditText,并创建了一个按钮,一旦触发,就会发送上游消息。要在 GCM 平台上发送上游消息,应用程序需要提供以下字段:

  • 我们在 GCM 平台上的服务器地址是<SENDER_ID>@gcm.googleapis.com

  • 唯一的消息标识符(message_id

  • 带有自定义键/值对的报文负载

现在,让我们更新MessagingActivity以根据EditText输入字段发送上游消息。由于上游分发需要网络访问,而且正如你所知,我们无法在主Thread上执行网络操作,我们必须使用AsyncTask子类在主线程之外执行。在Activity类中,我们实现了一个基本的异步结构AsyncJob,用于在后台执行网络操作,并捕获上游请求过程中发生的任何异常。这个特殊用途的类可以用于不产生任何结果的背景任务:

public abstract class AsyncJob 
  extends AsyncTask<Void, Void, Result<Void> > {

  @Override
  protected Result<Void> doInBackground(Void ...args) {
    Result<Void> result = new Result<Void>();
    try { runOnBackground() } 
    catch (Throwable e)  { result.error = e; }
    return result;
  }
  @Override
  protected void onPostExecute(Result<Void> result) {
    if ( result.error != null ) { onFailure(result.error);} 
    else { onSuccess();}
  }
  // Backrgound Execution Task
  abstract void runOnBackground() throws Exception;
  // Error Callback
  abstract void onFailure(Throwable e);
  // Success Function
  abstract void onSuccess();
}

使用AsyncJob,我们声明了三个抽象方法,任何AsyncJob子类都应该提供实现。runOnBackground应该实现后台任务,OnFailure应该用于处理执行异常,而onSuccess回调被调用以通知开发者任务已成功完成。

现在我们已经准备好实现OnClicklistener,它将在后台构建上游消息并将其发送到我们的 XMPP 服务器:

OnClickListener sendListener = new OnClickListener() {

  @Override
  public void onClick(View v) {

    TextView msgText = (TextView) findViewById(R.id.msg);
    final String msgToSend = msgText.getText().toString();
    msgText.setText("");

    new AsyncJob() {
      @Override
      void runOnBackground() throws Exception {

        // Build the data Bundle wit our key/value pairs
        Bundle data = new Bundle();
        data.putString(USERNAME_KEY, "Helder");
        data.putString(EXT_KEY, msgToSend);
        data.putString("topic", NotificationGCMHandler.
                                FORUM_TOPIC);
        // Generate a random message Id
        String id = Integer.toString(new Random().nextInt());

       // Get the GCMMessaging instance
        GoogleCloudMessaging gcm = GoogleCloudMessaging.
          getInstance(MessagingActivity.this);

        // Sends the Message to the GCM platform
        gcm.send(getString(R.string.gcm_SenderId) + 
                 "@gcm.googleapis.com", id, data);
      }
      @Override
      void onFailure(Throwable e) {
//… Handle the exception
Log.e(TAG,"Failed to send upstream message to forum",e);
      }
      @Override
      void onSuccess() {
        //.. No Exception thrown 
}
    }.execute();
  }
};

在我们的示例中,我们创建了一个包含所有要分发的有效载荷数据的Bundle对象。除此之外,我们还使用java.util.Random.nextInt实例方法创建了一个唯一的消息 ID。

此消息接收以下参数:遵循格式<SENDER_ID>@gcm.googleapis.com的地址、从随机整数生成的唯一消息 ID 字符串,以及包含你的有效载荷的 bundle。

一旦我们调用GoogleCloudMessaging.send,如果有一个活跃的连接可用,新的上游消息将立即发送,否则消息将被排队。一旦重新建立连接,排队中的消息将被发送到 GCM 服务器。

注意

如果客户端在达到 20 条消息限制后尝试发送更多消息,它将返回一个错误。

GoogleCloudMessaging API 将以高效的方式重用和管理与 GCM 平台的连接,为我们透明地最大化设备电池寿命。

一旦消息被我们的 XMPP 服务器接收,消息将被发送到/topics/forum,因此它将更新我们输入的消息的 UI 消息流。

GcmListenerService 投递回调

在某些情况下,由于缺乏网络连接,无法与 GCM 服务器建立连接时,消息可能会在本地队列中长时间保留。因此,为了在指定时间内丢弃未发送到 GCM 服务的消息,GoogleCloudMessaging API 提供了一个额外的发送方法,该方法可以接收一个 TTL(生存时间)时间来设置消息过期时间:

void send (String to, String msgId, long timeToLive, Bundle data)

当您有只在特定时间范围内相关的消息时,这效果很好。如果生存时间为 0,我们将在不连接的情况下立即尝试发送并返回错误。这种情况不适用于我们的示例,因此我们将保留原始代码,使用不丢弃旧未发送消息的发送方法。

重要的是要理解,当长时间没有连接到 GCM 平台时,应用 GCM 客户端只能最多排队 20 条消息。

除了上游过期功能之外,GcmListenerService还允许我们通过重写onMessageSentonSendError回调来接收上游消息的投递状态:

    void onMessageSent(String msgId) 
    void onSendError(String msgId, String error)

当消息被投递到 GCM 并且有错误将消息投递到 GCM 连接服务器时,将调用onMessageSent回调。请注意,这两个回调都带有消息标识符作为参数,因此您应该使用此标识符来定位已发送或因错误而失败的消息。

由于效率原因,GCM 消息投递报告是以批量形式交付的,因此不要期望在您上传单条消息后立即收到回调执行。

要在我们的聊天示例中接收上游消息的投递状态,我们将更新我们的NotificationGCMHandler并重写onMessageSentonSendError

public class NotificationGCMHandler extends GcmListenerService {
       ...
    @Override
    public void onMessageSent(String msgId) {
        super.onMessageSent(msgId);
        Log.i(TAG, "Message w/ id="+msgId+" send to GCM Server ");
    }

    @Override
    public void onSendError(String msgId, String error) {
        super.onSendError(msgId, error);
        Log.e(TAG, "Message w/ id=" + msgId + 
                   " send failed with error "+error);
    }
}

在我们的GcmListenerService回调中定义的回调方法将打印一条消息到应用程序日志输出,其中包含已发送或失败的消息。如果消息过期时间到达或达到上游排队消息的最大大小时,消息的投递可能会失败。

太好了!我们已经完成了基于 GCM 平台的群聊。在我们的旅程中,我们学习了如何使用一个电池高效的 API 向上和向下发送主题消息,该 API 维护与 Google 服务器的网络连接。该 API 允许我们在服务器和设备之间,或设备组之间创建双向通信通道。

使用 GCM 网络管理器执行任务

除了消息框架之外,GCM 库还包含 GcmNetworkManager,这是一个 API,允许我们在 API 级别 9(Android 2.1)及以上运行的设备上高效地安排重复或周期性任务。对于运行 API 级别 21(Lollipop)及以上的设备,GCM 网络管理器内部使用本地的 JobScheduler API,这在 第七章 中详细介绍了,探索 JobScheduler API。与 JobScheduler API 一样,它将尝试批量处理作业并减少从空闲状态唤醒的次数,以改善用户设备的电池寿命。

此外,使用 GCM 网络管理器,我们还可以设置启动作业执行应满足的条件,例如当设备处于充电状态或可不计费 WIFI 连接可用时。尽管 GCM API 提供了与 JobScheduler API 相同的标准,但它可以用于安装了 Google Play 服务的旧设备和新设备。

因此,在尝试使用它之前,您需要确保设备上可用的 Google Play 服务版本,使用 GoogleApiAvailability 类,就像我们在 GCM 示例中所做的那样。

查看我们之前示例中的 checkPlayServices() 函数,以获得更完整的解决方案。之前的函数将在需要任何用户操作来更新或安装 Google Play 服务时显示对话框。

我们可以在 GCM 网络管理器上安排任务执行,在以下条件下运行,例如:

  • 当有特定的网络连接可用(任何网络可用,不计费的网络连接)

  • 当设备连接到充电器时

  • 在未来预定义的时间窗口内运行的任务

  • 指定即使在重启后也要运行的任务

虽然支持的标准与之前覆盖的 Scheduler API 相同,并且适用于运行 Android Lollipop 的设备,但此 API 需要一些额外的必填标准,您应指定以在 GCM 网络上注册服务任务执行。

要构建和构造一个 GCM 任务,有两个 Builder 类可用:用于创建单次任务的 OneoffTask.Builder,以及用于注册定期以固定间隔运行的任务的 PeriodicTask.Builder

构建一次性任务

OneoffTask 是一个将在未来指定的时间窗口内执行一次的任务。从 OneoffTask.Builder 配置 OneoffTask 可用的选项包括:

  • 执行窗口范围(必填)

  • 标签标识符(必填)

  • 运行我们的任务所需的 GcmTaskService 子类(必填)

  • 额外参数(可选)

  • 任务持久性(可选)

  • 必需网络(可选)

  • 充电需求(可选)

  • 更新当前任务(可选)

在我们的下一个示例中,我们将使用 GCM 网络管理器来安排账户设置的备份。当账户设置更新时,它们将存储在本地文件中,一旦备份运行,账户详情将通过上游消息推送到我们的 XMPP 服务器。为了保存我们的账户设置,我们将创建一个 Activity,显示一个表单来填写我们的个人详情。

表单将包含一个按钮,点击后会将我们的账户详情保存到本地文件,并注册一个 GCM 网络任务执行,将我们的详情推送到我们的网络 XMPP 服务器。

为了延长电池寿命并减少我们的计费移动互联网使用,我们将我们的备份任务注册为仅在 WIFI 网络可用且设备正在充电时运行,最多在调度后 4 小时内。

在我们将任务注册到 GCM 网络管理器之前,我们将我们的GcmTaskService添加到应用程序清单中:

<service
  android:name=".chapter10.MyBackupService"
  android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
  <intent-filter>
    <action 
   android:name="com.google.android.gms.gcm.ACTION_TASK_READY"/>
  </intent-filter>
</service>

在 Android 清单中,我们添加了接收 GCM 启动广播所需的 Intent 过滤器,并且为了保护我们的服务不被除 Google Play Services 之外的其他程序启动,我们添加了com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE权限。

接下来,我们准备注册一个一次性任务来备份存储在应用程序默认共享偏好文件中的本地账户详情。每当用户更新账户详情并在 UI 上点击保存按钮时,账户详情将被本地存储,并在 GCM NM 上构建和注册一个OneoffTask任务来发布更改到我们的网络服务器。

让我们看看保存按钮的OnClickListener是什么样的:

public class AccountSettingsActivity extends Activity {

  public static final String TASK_BACKUP = "backup";

  public static long FOUR_HOUR = 3600*4L;

  // Executed when the user taps on save button
  OnClickListener listener = new OnClickListener() {

    @Override
    public void onClick(View v) {
      // Store the details on the default shared preferences file
      ...
	   // Obtain a GCM NM Instance 
      GcmNetworkManager gcmNM = GcmNetworkManager.
        getInstance(AccountSettingsActivity.this);      
      OneoffTask task = new OneoffTask.Builder()
        // Sets the Service to start
        .setService(MyBackupService.class)
        // Task Identifier
        .setTag(TASK_BACKUP)
           // Will run in the next 4 hours      
        .setExecutionWindow(0L, FOUR_HOUR)
        // Requires WIFI Network
        .setRequiredNetwork(Task.NETWORK_STATE_UNMETERED)
        // Requires Charging
        .setRequiresCharging(true)
           .build();

      gcmNM.schedule(task);
    }
  };
}

要从您的Activity注册任务,我们使用Activity上下文获取GcmNetworkManager 的实例。接下来,我们创建了一个OneoffTask.Builder对象,并将任务设置为启动MyBackupService服务以完成任务,并在调度后至少 4 小时运行任务。

注意,框架将在所有条件都满足并且考虑到其他计划运行的任务的情况下立即启动您的作业。如前所述,GCM NM 将延迟作业执行并批量作业以减少从空闲状态唤醒 CPU 的次数。

现在,我们将创建一个扩展自GcmTaskService并实现以下方法的MyBackupService

int onRunTask(TaskParams args);

我们的OnRunTask方法将发布我们的账户详情更新到我们的 XMPP 服务器:

public class MyBackupService extends GcmTaskService {

  @Override
  public int onRunTask(TaskParams taskParams) {
    Log.i(TAG, "Backing up the account settings");
    try {

      // Obtain the default Shared preference object
      SharedPreferences sp =PreferenceManager.
        getDefaultSharedPreferences(this);

      // Builds the upstream data bundle
      Bundle data = new Bundle();
      data.putString(FIRST_NAME, sp.getString(FIRST_NAME, ""));
      data.putString(LAST_NAME,sp.getString(LAST_NAME, ""));
      data.putString(AGE, sp.getString(AGE, ""));

      // Specify the resource to update (Optional)
      data.putString("resource","/account");
      data.putString("operation","update");

      String msgId = Integer.toString(new Random().nextInt());
      GoogleCloudMessaging gcm = GoogleCloudMessaging.
        getInstance(MyBackupService.this);
      gcm.send( SENDER_ID + "@gcm.googleapis.com", msgId, data);
    } catch (IOException e) {
      Log.e(TAG, "Failed to backup account", e);
      return GcmNetworkManager.RESULT_RESCHEDULE;
    }
    return GcmNetworkManager.RESULT_SUCCESS;
  }
}

为了执行onRunTask方法,GCM NM 启动的GcmTaskService将使用THREAD_PRIORITY_BACKGROUND优先级创建一个后台线程,并保持设备唤醒,持有 CPU Wakelock最多 3 分钟。执行 3 分钟后,如果您的任务没有返回,GCM NM 认为您的任务已超时,并将释放 CPU Wakelock

注意

如果您的服务一次接收多个请求,您应该使用同步段序列化作业执行,以避免线程安全问题。

onRunTask 返回的结果代码将决定任务的执行成功(RESULT_SUCCESS)、失败(RESULT_FAILURE)或失败后重新安排(RESULT_RESCHEDULE)。在我们的特定示例中,如果在提交上行消息期间抛出异常,返回的结果代码 RESULT_RESCHEDULE 将强制任务在退避期(指数退避)后再次执行。

摘要

在本章中,我们学习了如何使用 GCM 平台提供的节能通信通道发送和接收数据。

首先,我们学习了轮询和推送/拉取通信技术之间的区别,以与网络服务器交互。GCM 使用的推送和拉取消息能够通过避免重复的服务器查询来更新用户数据,从而降低应用程序的电池效率。

同时,我们学习了如何在我们的应用程序中设置和配置 GCM 库。为了与 Google 服务交互,我们的设备获得了一个 instanceID 和注册令牌,以在 GCM 服务上验证和识别我们的设备。

接下来,我们学习了如何在我们的应用程序中处理通知消息和主题消息,并使用 GCM 上行消息与自定义 XMPP 服务器进行交互。同时,我们构建了一个群聊系统,该系统能够从不同的用户那里聚合消息,并在屏幕上显示一个统一的消息流。

最后,我们学习了如何使用 GCM 网络管理器来安排网络任务,这些任务在设备满足某些条件时运行,例如设备连接到 WIFI 网络。

在下一章中,我们将向读者介绍 RXJava,这是一个用于通过可观察数据流在 Java 中组合异步和基于事件的任务的库。

第十一章:探索基于总线通信

在前面的章节中,我们使用不同的技术在不同 Android 应用程序组件(ActivityFragmentServiceBroadcastReceiver等)之间传播数据/事件/通知:

  • 意图通过系统发送,携带通信消息或通知,以通知ServiceActivity启动

  • 使用广播意图从后台进程报告结果

  • 处理器被用来在不同的进程和线程执行之间进行通信

这些技术通常涉及发送消息的组件和接收消息的组件之间的紧密耦合。通常,发送者将消息调度到特定的接收者,并处理接收者的生命周期以检测任何接收者不可用的情况。

在本章中,我们将向读者介绍一个由 EventBus 库提供的新构造和模式,这个模式在大多数情况下通过解耦事件生产者和事件消费者组件来简化不同应用程序组件之间的通信。

本章将涵盖以下主题:

  • 介绍基于总线的通信

  • 在你的项目中设置 EventBus

  • 在总线中定义和调度事件

  • 注册订阅者

  • 使用 threadMode 异步处理事件

  • 发布和移除粘性事件

基于总线的通信简介

基于总线的通信软件模式,也称为发布/订阅,是一种允许发送者和接收者实体在不要求它们显式知道对方的情况下进行通信的模式。这种通信模型抑制了组件之间的紧密耦合,并允许将消息从单个接收者传递到多个最终接收者。在通信模式中涉及五个不同的实体:发布者、订阅者、事件、总线和代理。

发布者将事件提交到由称为代理的实体控制的共享消息管道,称为总线,该代理管理提交的事件流,并将它们转发到之前在代理中注册以接收特定类型事件的实体列表,称为订阅者

为了接收某些类型的事件,订阅者应在代理中创建一个订阅,代理应保持一个启用订阅的列表,并将事件转发给所有订阅者。

如果消费者对某一种事件失去兴趣,它将终止订阅,因此,代理将停止转发与订阅者相关的未订阅事件。

基于总线的通信简介

在这种松散耦合的通信模型中,发布者在共享的Bus中提交事件 A,而不了解将消费该事件的精确订阅者。同样,订阅者不知道提交事件的发送实体,除非事件 A 中发送了某些内容来识别事件的来源。

在 Android 特定情况下,它可以简化FragmentsActivitiesServices或任何其他业务逻辑对象(如持久化服务)之间的通信,这些对象管理您的应用程序或 UI 状态。在我们的示例中,我们将使用该库在活动之间发送通知。然而,同样的结构也可以应用于服务与广播接收器之间的通信。

EventBus 库

尽管有几个开源库能够实现这种模式在 Android 上的传递,但我们将基于流行的event bus库([greenrobot.org/eventbus/](http://greenrobot.org/eventbus/))编写代码示例,因为它提供了高级功能和高性能。

该高性能库针对 Android 操作系统进行了优化,并且已被 Google Play 上许多流行的应用程序使用。

这些是EventBus库提供的先进功能,您应该了解:

  • 基于注解的订阅 - 您可以通过注解 Android ActivityServiceFragment实例方法来定义订阅方法

  • 背景和主线程事件传递 - 订阅者可以定义事件将在哪个线程中传递,无论它是在后台还是主线程生成

  • 事件和订阅者继承 - 我们可以通过扩展(Java 子类)其他事件或订阅者来构建事件或订阅者:

    class OtherEvent extends MyEvent
    
  • 无需配置 - 默认情况下,该库允许我们使用一个现成的默认Bus,无需显式实例化,并且可以从应用程序的任何地方提交事件:

    EventBus.getDefault().post(new MyEvent());
    

在开始使用它之前,我们将向我们的模块或应用程序build.gradle文件中添加 GreenRobot Eventbus依赖项:

dependencies {
   compile 'org.greenrobot:eventbus:3.0.0'
}

在我们深入探讨之前,我们将展示一个简单示例,其中我们使用该库从BroadcastReceiverActivity发布一个简单事件。因此,Activity接收方法将在屏幕上提供通知。

首先,我们将创建一个BroadcastListener,它监听网络变化,并在移动网络不可用时在Bus中提交一个事件,当设备移动网络可用时,提交一个包含详细网络状态的事件。这些事件将在Bus中传播,并传递给所有感兴趣的订阅者,在我们的例子中,将是一个Activity,它将在屏幕上显示显示移动网络状态的消息。

定义事件

首先,我们将定义由发布者提交给Bus的 POJO 类,以通知感兴趣的实体移动网络连接是否可用:

public class MobileNetConnectedEvent{
  public final String detailedState;
  public MobileAvailableEvent(String detailedState) {
    this.detailedState = detailedState;
  }
}
public class MobileNetDisconnectedEvent {}

MobileNetConnectedEvent事件是一个 POJO 类,当移动网络可用时将发送,并携带包含详细网络状态的字符串消息。

MobileNetDisconnectedEvent是一个不携带任何信息的事件,但它将通知事件订阅者网络连接已丢失。

提交事件

现在定义了事件后,我们将创建一个BroadcastListener,它将在设备上检测到任何网络连接变化(Wi-Fi、移动网络等)时从 Android OS 接收 Intent,并在移动连接发生变化时将事件提交到 Bus 中:

public class MobileNetworkListener extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
	 // Retrieve the NetworkInfo from the received Intent
    NetworkInfo info =(NetworkInfo)intent.
      getExtras().get(ConnectivityManager.EXTRA_NETWORK_INFO);
    if ( isMobileNetwork(context, info) && !info.isConnected()) {
	   // Publish an mobile network disconnected Event
      EventBus.getDefault().post(
        new MobileNetDisconnectedEvent());
    } else if ( isMobileNetwork(context, info) && 
                info.isConnected()) {
      // Publish an mobile network connected Event
      EventBus.getDefault().post(
        new MobileNetConnectedEvent(info.getState().toString()));
    }
  }
  public boolean isMobileNetwork(Context context, 
                             NetworkInfo info) {
    return info != null && 
           (info.getType() == ConnectivityManager.TYPE_MOBILE);
  }
}

正如我们之前描述的,默认的、可立即使用的EventBus可以从我们的应用程序的任何地方检索,因此,当收到关于移动网络的网络变化事件时,我们只需通过调用EventBus.getDefault()来获取默认的 Bus,并通过调用Bus.post(Object event)函数向其提交事件。

注意,我们将根据在ConnectivityManager.EXTRA_NETWORK_INFO Intent 额外信息中接收到的NetworkInfo来识别网络。

当检测到与移动网络相关的网络变化时,我们在默认的Bus中提交MobileNetConnectedEventMobileNetDisconnectedEvent

注册订阅者

已经指定了Publisher/Sender类和事件类,剩下的只是将我们的Activity类注册以接收这两个事件并在屏幕上打印发送的事件。

正如我们之前所述,为了从Bus接收任何事件,Subscriber实体(可以是代码中的任何 Java 类)必须注册到 Bus 上并订阅它感兴趣的事件。

任何对象都必须通过调用register函数在 Bus 上注册,并提供一个带有@org.greenrobot.eventbus.Subscribe注解的单一on<eventName>(EventType)方法,以便对它感兴趣的所有类型的事件进行注册:

@Subscribe
void on<EventClassname>(EventClassname event) {
 ...
}

让我们实现将在我们的 Activity 中处理MobileNetDisconnectedEventMobileNetConnectedEvent事件的函数:

@Subscribe
public void 
onMobileNetDisconnectedEvent(MobileNetDisconnectedEvent event){

  String message = String.format(
    "Mobile connection is not available \n");
  appendToConsole(message);
}

@Subscribe
public void 
onMobileNetConnectedEvent(MobileNetConnectedEvent event){

  String message = String.format(
    "Mobile connection is available State - %s\n",
    event.getDetailedState());
  appendToConsole(message);
}

这两个公共回调都有@Subscribe注解,并且只有一个MobileNetDisconnectedEvent/MobileNetConnectedEvent对象作为方法参数。因此,每当我们的BroadcastReceiver发送者将这些事件发布到 Bus 上,并且Activity已经订阅了它们时,我们的回调会被通知,并在 UI 控制台屏幕上添加一条新消息。

最后,为了在我们的默认 Bus 上注册我们的Activity,我们将重写onStartonStop Activity函数以分别进行注册和注销:

    @Override
    public void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }

    @Override
    protected void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
    }

一旦我们注册了我们的类对象,Bus 将通过反射 API 遍历Activity方法,并检查是否有任何带有Subscribe注解的方法。一旦找到任何带有@Subscribe注解的方法,并且方法参数为 POJO 事件,它将注册实例方法,以便在事件在Bus上发布时调用。

一旦我们的Activity被销毁,我们将终止总线订阅,Bus将停止发送事件。在任何 Android 组件中,例如Activity、Fragment 和Service,我们应该根据组件的生命周期在总线上注册和注销。从总线中注销组件非常重要,否则总线将保持对已注册组件的引用,从而阻止其被垃圾回收。结果,它将在应用程序中产生内存泄漏。

线程模式

默认情况下,EventBus在发送者发布事件的同一线程中向订阅者传递事件。尽管这种传递方案可能适用于大多数用例,例如执行 Android UI 更改的事件,但当在事件回调中执行长时间操作时,订阅者可能会阻塞主线程,从而阻止系统及时运行 UI 渲染,并导致丢失一些 UI 帧。

为了应对在事件传递过程中可能发生的耗时操作,EventBus库允许我们定义Bus将调用以将事件传递给订阅者的线程(ThreadMode)。

EventBus支持四种模式,我们可以使用这些模式来控制事件传递行为:

  • ThreadMode.POSTING – 订阅者的回调将在发送者发布事件的同一线程中调用。这是默认行为,事件将同步发送到所有订阅了已分发事件的实体。

  • ThreadMode.MAIN - 总线将在主 UI 线程中调用订阅者的回调。因此,如果发送者在后台线程中运行时向Bus发布事件,总线将在主Looper中排队消息,事件将在主线程中传递。有关Looper和 Handler 如何工作的更多详细信息,请参阅第二章,使用 Looper、Handler 和 HandlerThread 执行工作。当事件在主线程中产生时,它表现得像ThreadMode.POSTING模式。

  • ThreadMode.BACKGROUND – 总线将在一个后台线程中调用订阅者的回调,这可以防止事件处理阻塞 UI 线程。请注意,EventBus仅使用一个后台线程来调用所有回调,因此,任何长时间运行的组件可能会延迟后续事件的传递。当事件在后台线程中产生时,它处于ThreadMode.POSTING模式。

  • ThreadMode.ASYNC- 总线将使用由总线管理的线程组调用订阅者的回调。从Executors.newCachedThreadPool创建的工作线程池将被回收,并可能用于执行阻塞操作,例如网络或长时间计算操作。

你应该根据消费事件所需的处理类型设置你的示例所需的线程模式。例如,当消费者更新 UI 时,如果生产者可以从后台线程发布事件,则应显式指定ThreadMode.MAIN。在其他情况下,如果消费者执行阻塞或密集型操作,你应该使用ThreadMode.ASYNC模式来跨多个线程处理事件。

为了显式确定EventBus在哪个线程上调用方法,我们必须在Subscribe注解中指定threadMode属性:

// Execute the callback on a  Background Thread 
// managed by EventBus
@Subscribe(threadMode = ThreadMode.BACKGROUND )
public void onMyEvent(MyEvent event) {...}

通常,Android 应用程序需要运行后台任务以从网络服务或内容服务获取动态数据。检索到的数据随后被调度到主线程以在 UI 主线程中展示。在之前的章节中,我们使用了不同的技术(AsyncTask、Loader 和 HTTP 异步客户端)来完成这项任务。在我们的下一个示例中,我们将使用ThreadMode.BACKGROUND模式来执行一个 IO 阻塞操作,使用EventBus异步后台线程池检索产品信息。

基于前一次操作的结果,我们将构建一个包含产品详情的事件,并将其报告回主 UI 线程以更新屏幕上的产品。

我们的Activity将展示一个包含产品详情以及下一页上一页按钮的Fragment,以便在产品列表之间浏览。正如之前所解释的,我们将使用EventBus将事件详情请求调度到后台线程,并使用事件将Activity后台方法的结果发布回DetailsFragment片段。

首先,我们将定义用于建模产品详情请求和产品详情的RetrieveProductEventProductDetailEvent POJOs:

public class RetrieveProductEvent {

    // Product Identifier
    final long identifier;
    ...
}

public class ProductDetailEvent {

    final long identifier;
    final String brand;
    final String name;
    final float price;

    ...
}

然后,我们将创建一个将注册到Bus并订阅接收带有产品数据的ProductDetailEvent事件的Fragment。正如你所知,为了防止内存资源泄漏,注册和注销Fragment在总线上是至关重要的,因此,我们将使用FragmentonResumeonPause生命周期回调来实现这一点:

public static class DetailFragment extends Fragment {

  @Override
  public void onResume() {
    EventBus.getDefault().register(this);
    super.onResume();
  }

  @Override
  public void onPause() {
    EventBus.getDefault().unregister(this);
    super.onPause();
  } 
  ...
}

由于我们希望在接收到ProductDetailEvent时更新 UI,我们将创建一个在ThreadMode.MAIN线程模式下运行的订阅者,因此,它将在主Thread中接收事件回调:

public static class DetailFragment extends Fragment {
  ...
  @Subscribe(threadMode = ThreadMode.MAIN)
  public void onProductDetailEvent(ProductDetailEvent event) {
    Log.i(TAG,"Product details received for identifier"
               +event.identifier+" on" +   
               Thread.currentThread().getName());       
    // Update the Product Details on the UI
    brandTv.setText(event.brand);
    nameTv.setText(event.name);
    priceTv.setText(Float.toString(event.price));
  }

  @Override
  public View onCreateView(LayoutInflater inflater,
                           ViewGroup container,
                           Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.detail_fragment, 
                            container, false);
  }

  @Override
  public void onViewCreated(View view, 
    Bundle savedInstanceState) {
    // Initialize the UI widgets
    ...
   }
}

随后,我们将创建一个加载DetailsFragmentActivity,并从产品目录请求加载第一个产品(productId=0):

public class PaginatedActivity extends FragmentActivity {

  int productId = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.paginated_layout);

    // Loads the Details Fragment
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.
                                              beginTransaction();
    DetailFragment fragment = new DetailFragment();
    fragmentTransaction.add(R.id.detail_fragment, fragment);
    fragmentTransaction.commit();

    // Request to load the first product
    EventBus.getDefault().post(
       new RetrieveProductEvent(productId));
    ...
  }
  @Override
  public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
  }
  @Override
  protected void onStop() {
    EventBus.getDefault().unregister(this);
    super.onStop();
  }
}

Activity将为DetailsFragment创建一个FragmentTransaction并将其提交给FragmentManager。最后,它将在总线(bus)上发布一个事件以加载第一个新产品的事件RetrieveProductEvent(productId)

接下来,我们将实现处理RetrieveProductEvent的订阅者方法,在后台获取指定标识符的产品详情,并将新的产品详情事件分发给所有感兴趣的实体:

@Subscribe(threadMode = ThreadMode.ASYNC)
public void onRetrieveProductEvent(RetrieveProductEvent event) {
  Log.i(TAG, "Retrieving the product " + event.identifier 
              + " on " + Thread.currentThread().getName());

  // Retrieve on background the product details 
  // for the product with the event.identifier id
  ProductDetailEvent pde = ...;

  // Post an EventDetailsEvent on the Bus to 
  // publish the event details for the product requested
  EventBus.getDefault().post(pde);
}

使用ThreadMode.ASYNC,我们将强制EventBusEventBus异步线程池中的一个线程上调用回调。这种线程模式用于执行可能需要阻塞一段时间或执行时间较长的异步操作,例如长时间的计算或网络操作。

根据您定义的线程模式,EventBus将管理所有必要的线程切换,以确保事件被发送到正确的线程组或单个线程,无论事件是从主线程还是后台线程分发的。

当请求的产品详情加载完成后,返回的ProductDetailEvent对象将被发布到总线上以进行进一步处理。

由于DetailsFragment已订阅onProductDetailEvent函数以在主线程接收ProductDetailEvent,总线代理将调用 UI 线程中的函数来更新brandTvnameTvpriceTvTextView小部件的产品详情。

使用EventBus threadMode功能,我们可以从应用程序中的任何线程提交事件到主线程,我们甚至可以使用干净简单的接口将工作交给后台执行行。

只为了结束这个例子,我们将添加两个按钮来浏览产品列表序列。下一页按钮将提交一个RetrieveProductEvent请求以获取列表中的下一个产品,而上一页按钮将提交一个RetrieveProductEvent以获取列表中的上一个产品:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.paginated_layout);
  ...
  // Submit an event to load the next Product
  Button next = (Button)findViewById(R.id.next);
  next.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      EventBus.getDefault().post(
        new RetrieveProductEvent(++productId));
    }
  });  
  // Submit an event to load the previous Product
  Button prev = (Button)findViewById(R.id.previous);
  prev.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      if ( productId > 0 ) {
        EventBus.getDefault().post(
       new RetrieveProductEvent( --productId ));
      }
    }
  });
}

通过 EventBus 提供的Publish/Subscribe模式,我们能够更新DetailFragment,而无需与Activity共享严格的接口。此外,事件可能来自任何其他 Android 组件,并且结果将通过 Event Bus 在主线程中分发。

发布粘性事件

每当我们向总线发布一个事件时,EventBus 代理会自动将事件发送给所有当前订阅者,并且默认情况下会立即清除临时事件。在事件发送给当前订阅者之后注册的新订阅者将不会接收到该事件。

有时,新订阅者在总线上注册,但长时间内没有在总线上产生或提交新事件。因此,订阅者将等待总线上出现下一个事件,然后从中产生任何输出。

此外,当新订阅者负责更新 Android UI 组件(如ActivityFragment)时,订阅者必须等待新事件的发生,因此,这可能会延迟 UI 更新很长时间。

为了解决这个问题,EventBus允许我们创建粘性事件,这些事件被保存在内存中,一旦它们在 Bus 上注册,就会传递给订阅者。EventBus将在内存中保留特定类型事件的最新事件,并在注册期间传递它,只要订阅者创建了一个带有粘性传递的订阅。

要在 Bus 上传递粘性事件,我们只需要调用Bus.postSticky函数而不是 post 函数:

void postSticky(new MyEvent())

并创建一个带有sticky属性启用的Subscriber方法:

  @Subscribe(sticky = true)
  public void onMyEvent(MyEvent event)

例如,LocationManager服务允许我们创建一个LocationListener,当设备的位置通过一定的minDistance发生变化时,接收当前的地理位置:

LocationManager.
requestLocationUpdates(String provider,  // GPS or NETWORK
                       long minTime, float minDistance, 
                       LocationListener listener)

如果我们使用LocationListener在 Bus 上发布非粘性LocationEvent,并且设备的位置在一段时间内没有变化,新订阅者将不得不等待直到设备位置改变,才能从 Bus 接收当前位置:

public class LocationEvent {

    final double latitude; // location latitude in degrees.
    final double longitude; // location longitude in degrees.

    LocationEvent(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

此外,为了减少设备的能耗,位置更新的最小时间间隔(minTime)应该足够长,以便应用程序用户能够注意到,以便消除下一次事件等待粘性事件技术的时间。

如果我们注册带有粘性传递启用的Subscriber方法,新的粘性注册将立即从 Bus 获取最新位置,停止订阅者等待LocationListener发布的下一次位置更新。

为了演示这一点,首先我们将创建一个Activity,该Activity管理自己的LocationListener,接收位置更新,并在 Bus 上发布粘性LocationEvent事件:

public class LocationActivity extends Activity {

  @Override
  public void onResume() {
    super.onResume();

    LocationManager manager = (LocationManager)
      getSystemService(Context.LOCATION_SERVICE);
	Location location = manager.getLastKnownLocation(
     LocationManager.GPS_PROVIDER);

   // Post the latest known position if available 
   if ( location != null ){
    EventBus.getDefault().postSticky(
            new LocationEvent(location.getLatitude(),
                              location.getLongitude()));
    }
    // Request a location update only if device location changed
    // Minimum time between updates: 5000ms 
    // Minimum distance between location updates: 100 meters
    manager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 5000, 100, listener);
  }

  @Override
  public void onPause() {
    super.onPause();
    LocationManager manager= (LocationManager)
      getSystemService(Context.LOCATION_SERVICE);
    manager.removeUpdates(listener);
  }

  //Handle location callback events
  private LocationListener listener = new LocationListener() {
    @Override
    public void onLocationChanged(Location location) {
      EventBus.getDefault().postSticky(
        new LocationEvent(location.getLatitude(),
                          location.getLongitude()));
    }
    @Override
    public void onProviderDisabled(String provider) { }
    @Override
    public void onProviderEnabled(String provider) { }
    @Override
    public void onStatusChanged(String provider, 
                                int status, Bundle extras) {}
  };
}

在前面的代码中,我们注册我们的匿名监听器以在Activity进入前台时接收位置更新,并在Activity暂停时注销监听器,以便被销毁或移出前台。我们注册监听器以几乎每五秒接收一次更新,并且当位置变化 100 米时。

同时,当从 GPS 位置提供程序获得最后已知位置时,我们在 Bus 上发布一个粘性事件,以将最后已知位置传递给未来的订阅者。

我们的LocationListener再次将onLocationChanged回调接收到的位置对象转换为LocationEvent对象,并在 Bus 上提交一个粘性事件。这个粘性事件将更新EventBus缓存的LocationEvent,一旦订阅者订阅,所有粘性Subscriber方法将立即获得此事件。

注意,我们假设设备上已启用 GPS 提供程序。为了更完整的示例,在您尝试使用LocationManager之前,请验证 GPS 位置是否可用,并在提供程序不可用时,要求用户在设备设置中启用它。

此外,为了接收位置更新,必须在应用程序权限中声明android.permission.ACCESS_COARSE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION权限,或者在 API 级别大于 23(Marshmallow)的运行时请求这些权限。完整的源代码可在 Packt Publishing 网站上找到。查看完整的源代码可以了解如何请求所需的 Android 操作系统权限。

接下来,我们将创建一个按钮,该按钮启动新的LocationEvent订阅者,它们在Bus上立即注册和注销:

Button newSubs = (Button)findViewById(R.id.launch);
newSubs.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {

    new Runnable() {
      @Subscribe(sticky = true)
      public void onLocationEvent(LocationEvent event) {
        String locTxt = String.format(
             "Lat[%f] Long[%f]", event.latitude, event.longitude);
        Log.i(TAG, "Last known Location is "+ locTxt);
        // Update the UI with the last position  
        // retrieved from the new Subscriber
        TextView locationTv = (TextView) 
                              findViewById(R.id.location);
        locationTv.setText(locTxt);
   }
      @Override
      public void run() {
        EventBus.getDefault().register(this);
        //...
        EventBus.getDefault().unregister(this);
      }
    }.run();
  }
});

按钮的OnClickListener中的代码将在总线上注册一个新的Runnable对象实例,并在之后注销。在注册期间,粘性Subscriber方法onLocationEvent将立即被调用,以处理通过我们的LocationListener在总线上发布的先前发布的 Location 粘性对象。

一旦接收到LocationEventonLocationEvent方法将使用最后的位置经纬度更新 UI,并在 Android 日志上打印位置。使用这种方法,粘性Subscriber方法无需等待位置变化即可接收设备位置并更新 UI。

移除粘性事件

在某些用例中,可能需要从总线中使粘性事件无效,并防止缓存的事件被传递给后续的订阅者。EventBus 允许我们通过调用以下函数来清除粘性事件:

  • removeStickyEvent(<MyEventClass>) – 移除并获取给定事件类型的最近粘性事件

  • removeStickyEvent(Object event) - 如果传入的事件等于指定的事件,则移除粘性事件

  • removeAllStickyEvents() - 移除所有类型的粘性事件

让我们使用一个removeStickyEvent函数来从总线上移除最新的粘性LocationEvent

// Check if the sticky event exist on the Bus  
LocationEvent evt = EventBus.getDefault().
                        getStickyEvent(LocationEvent.class);
// Check if the event is null
if ( evt != null) {
  EventBus.getDefault().removeStickyEvent(stickyEvent);
}

在我们从总线中移除粘性事件之后,最新的LocationEvent将从总线中移除,并且在向新的LocationEvent订阅者注册期间不会传递任何事件。

摘要

在本章中,我们学习了在 Android 应用程序中用于在解耦实体之间通信的发布/订阅消息模式。此模式必须应用于向一个或多个 Android 组件接收者发送事件通知或数据。

接下来,我们向读者介绍了EventBus,这是一个优化的开源库,为 Android 平台提供发布/订阅模式,并提供了如粘性事件和异步事件传递等高级功能。

随后,我们学习了如何设置库,如何建模事件,以及如何在默认的Bus上分发事件。总线作为一个接收事件的共享实体,将充当事件的中介和代理,将事件传递给之前订阅它们的最终接收者。

我们详细研究了 EventBus 的threadMode特性,该特性允许我们定义Bus将事件传递给订阅者的线程。因此,我们能够从发布线程中消费来自不同线程(后台线程、主线程和异步线程)的事件。

为了完成我们的旅程,我们学习了粘性事件,这些事件被缓存到 Bus 中,并在注册期间传递给新的粘性订阅者,从而防止这些方法在没有新数据的情况下等待下一个事件。

第十二章. 使用 RxJava 进行异步编程

在前面的章节中,我们一直在使用基于 Android 的结构,如LoaderAsyncTask,将工作从主线程卸载到低优先级的后台线程。

虽然这些直接的构造能够提供需要密集 I/O 操作或网络数据的成果,但它们不提供开箱即用的异常处理、任务组合和异步事件处理的解决方案。

此外,流行的AsyncTask结构无法处理Activity或片段配置更改或缓存配置更改之间的结果。因此,为了应对这类问题,大多数情况下开发者最终会创建大量的额外代码和复杂的流程来处理这些简单结构的特性。

为了简化可组合的异步工作开发,我们将向您介绍RxJava,这是一个函数式框架,它允许我们观察、转换、过滤并对事件流(点击、触摸、网络、I/O 事件等)做出反应,以便组合能够响应错误和链式异步计算的复杂执行行。

在本章中,我们将涵盖以下主题:

  • RxJava 简介

  • 创建 Observables

  • 转换 Observables

  • 理解调度器

  • 使用调度器执行异步 I/O

  • 使用 RxJava 组合任务

  • 使用 RxJava 观察 UI 事件

  • 使用 RxJava 组合任务

  • 使用 Subjects 进行工作

RxJava 简介

RxJava是 Reactive Extensions (ReactiveX)在 JVM 上的实现,由 Netflix 开发,用于组合对可观察事件源的异步事件处理。

该框架通过允许我们创建可以被操作(输入/输出)函数拦截的事件流来扩展Observer模式,这些函数可以修改原始事件流并将结果或错误传递给最终的Observer。该框架抽象掉了诸如低级线程、同步、线程安全、并发数据结构和非阻塞 I/O 等问题。

RxJava处理中,有三个主要的基本构建块相互作用,即ObservableObserverSubscriber

Observable是一个实体,它在任何时间点可以发出类型为 T(如 String 或任何 Java 类型)的事件序列(零个或多个事件),或者在事件处理过程中发生失败时发出Throwable。除此之外,它还提供了订阅其事件流和管理Observer订阅的方法。

Single是一种特殊的 Observable,它只能发出单个成功事件值或错误事件。

Observer,在注册为订阅者后,会消费由Observable<T>生成的类型为T的事件。一个Observer必须实现Observer<T>

public interface Observer<T> {

    void onCompleted();
    void onError(Throwable e);
    void onNext(T t);
}

任何 Observer 都会在它订阅的 Observable 发出新事件时接收到 onNext 回调,直到它收到 onCompletedonError 以关闭事件流。

Subscriber 是一个辅助抽象类,如果你想要订阅支持,可以用作你的观察者的基类。Subscriber 类提供了取消 Observable 订阅的方法:

abstract class Subscriber<T>
   implements Observer<T>, Subscription

public interface Subscription {

   void unsubscribe();
   boolean isUnsubscribed();
}

unsubscribe 是用于取消 Observer 订阅的函数。因此,一旦 Observer 订阅被终止,订阅者将不再接收由 Observable 生成的事件。

这里是一个简单的图表,展示了 ObservableSubscriber 之间的常见交互:

RxJava 简介

Observable 发出一个新项目时,onNext(T) 观察者回调会被调用。

当发现错误条件并且流将被终止时,onError(Throwable) 观察者回调会被调用以通知。

当流成功完成并且所有事件都成功传递时,onCompleted() 观察者回调会被调用以指示流已完成。

冷与热 Observable

根据 Observable 开始发出事件的时间,它可以被分类为热或冷。一个冷的 Observable 只有当观察者订阅它时才会开始向观察者发出事件。在这种情况下,预期观察者将接收到从开始到现在的流。

一个热的 Observable 会在创建后立即开始发出事件,因此观察者只会接收到订阅创建后发出的事件。在订阅之前发出的事件不会被观察者接收到。

RxJava 设置

在我们继续之前,让我们将所需的库添加到你的项目中。如果你使用的是 Android Studio,只需将以下依赖项添加到模块 build.gradle 脚本中:

dependencies {
    …
    compile 'io.reactivex:rxandroid:1.1.0'
    compile 'io.reactivex:rxjava:1.1.0'
}

rxjava 是一个库,它实现了 Java 上的响应式扩展 (reactivex.io/),而 rxandroid 是一个库,它为 Android 应用程序中用 RxJava 编写响应式组件添加了类。

创建 Observables

要创建一个 Observable,我们可以使用 create 函数从头开始创建一个 Observable 并显式调用观察者方法,或者我们可以使用内置的 Observable 创建方法,这些方法将常见的数据类型转换为 Observable 流。

让我们从简单的例子开始,创建一个使用创建 Observable.from 操作符发出 StringObservable

Observable<String> myObservable =
  Observable.from(Arrays.asList("Hello from RxJava",
                                "Welcome...",
                                "Goodbye"));

Observable.from 静态函数从一个数组创建 Observable,该数组将同步地将 String 项发出到任何观察者。创建的 Observable 将是一个冷 Observable,并且只有在观察者订阅它之后才会开始发出事件。

现在,让我们创建一个 Subscriber,它消费数据并将每个 String 打印到 Android 日志中,直到 Observable 调用 onComplete 回调:

Subscriber<String> mySubscriber = new Subscriber<String>() {

  @Override
  public void onCompleted() {
    Log.i(TAG, "Rx Java events completed");
  }

  @Override
  public void onError(Throwable e) {
    Log.e(TAG, "Error found processing stream", e);
  }

  @Override
  public void onNext(String s) {
    Log.i(TAG, "New event -" + s);
  }
};

接下来,使用Observable和刚刚定义的订阅者类,一旦我们在Observable上订阅了Subscriber类,onNext()函数将被调用三次,传递之前定义的数组中的每个String

随后,在所有Strings都被Subscriber消费后,将调用onCompleted函数来关闭流:

myObservable.subscribe(mySubscriber);

Observable实例负责管理所有订阅,通知所有其Subscribers,并且它不会开始发射项目,直到我们订阅它们。

除了使用Observable.from或其他创建操作符之外,我们还可以通过调用create方法并实现Observable.OnSubscribe<T>来创建Observable,该实现明确调用onNextonErroronCompleted

让我们创建自己的Observable,使用create函数发射整数数字:

Observable<Integer> myObservable = Observable.create(
   new Observable.OnSubscribe<Integer>() {
      @Override
      public void call(Subscriber<? super Integer> sub) {
          // Emitting Numbers
 sub.onNext(10);
 sub.onNext(3);
 sub.onNext(9);
          // Stream completed with success
 sub.onCompleted();
      }
  }
);

记住,一个表现良好的Observable必须在通过调用订阅者的onNext函数发射所有项目后,恰好一次尝试调用观察者的onCompletedonError

注意,之前的Observable也被归类为冷Observable,因为它只有在有订阅者实体订阅它时才会开始发射。

或者,我们可以使用Action函数订阅Observable,以处理在不同分离的函数中分发的项目。你需要做的只是传递一个Action1<T>函数用于事件处理,一个Action1<Throwable>用于错误发射,以及Action0以接收流完成通知。

让我们编写所需的动作函数,以响应我们的Observable<String>发射:

Action1<Integer> onNextAction = new Action1<Integer>() {
  @Override
  public void call(Integer s) { Log.i(TAG, "New number :" + s); }
};
Action1<Throwable> onError = new Action1<Throwable>() {
  @Override
  public void call(Throwable t) {
     Log.e(TAG, "Error: " + t.getMessage(), t);
  }
};
Action0 onComplete = new Action0() {
  @Override
  public void call() { Log.i(TAG, "Rx number stream completed")}
};

myObservable.subscribe(onNextAction, onError, onComplete);

除了from操作符和create操作符函数之外,还有其他简单的Observable函数可以用来构建Observable

  • Observable.just:从少量对象(最多 10 个对象)创建一个Observable

    Observable<Integer>.just(1,2,3)
    
  • Observable.range:发射一系列数字:

    Observable.range(1,10);
    

转换Observable

除了广泛实现Observable-Subscribe软件模式的能力之外,RxJava框架还允许我们通过使用Observable操作符来转换、过滤、转换、聚合、操作和与Observable发射的项目流一起工作。这些实体能够在事件交付给最终的Subscriber之前完全转换事件流。

RxJava附带了一系列方便的操作符,能够转换事件的内容并控制事件交付的时间。

让我们描述在RxJava上可用的最常见操作符:

  • map:对每个发射的项目应用一个函数,并将函数的结果作为新项目发射。

  • flatMap:对源Observable发射的每个项目应用一个函数,其中该函数返回一个Observable,该Observable可以发射不同数量的项目或不同类型的事件。

  • filter: 一个转换操作符,它使用一个函数来验证源Observable发射的每个项目是否满足条件。如果条件通过,则项目被转发到后续的Subscriber

  • first: 仅发射源Observable发射的第一个项目。

  • count: 发射从原始Observable接收到的项目数量。

  • zip: 使用一个函数将两个Observables的发射合并,该函数接收每个原始ObservableN个项作为参数。

  • contains: 发射一个Boolean事件,指示源Observable是否包含指定的Object

  • merge: 将多个Observers的事件合并到一个事件流中。

  • delay: 通过指定的时间延迟发射一个项目。

要获取 RxJava 支持的完整、详细和最新的操作符列表,请查看 GitHub 上的RxJava Wiki(github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-Observable-Operators)。

RxJava操作符通常处理一个Observable并返回一个Observable。这个设计特性允许我们链式调用操作符,创建一个由操作符组成的转换事件流序列。最后一个操作符负责将项目传递给Subscriber,或者在出错时传递错误。

现在,让我们创建第一个操作符示例,该示例将转换源Observable发射的多行文本,并传递一个包含包含单词RxJava的行数的整数。

String content = "This is an example \n " +
                 "Looking for lines with the word RxJava\n" +
                 "We are finished.";
Observable
  .just(content)
  .flatMap(new Func1<String, Observable<String>>() {
    @Override
    public Observable<String> call(final String content) {
      return Observable.from(content.split("\n"));
    }})
  .filter(new Func1<String, Boolean>() {
       @Override
    public Boolean call(final String line) {
      return line.contains("RxJava");
    }
  })
  .count()
  .subscribe(new Subscriber<Integer>() {
    ...
    @Override
    public void onNext(Integer s) {
      Log.i(TAG, "Number of Lines " + s);
    }
  });

首先,我们使用Observable.just创建操作符从原始数据创建一个Observable,并将文本源作为唯一对象传递。

接下来,为了将原始文本拆分为行,我们使用flatMap操作符,它接收第一个Observable发射的原始文本,并返回一个由切片行数组创建的新Observable

来自flatMap操作符的新Observable将为原始内容中的每一行发射一个单独的String,因此,为了计算包含单词RxJava的行数,我们将使用过滤操作符丢弃不包含该单词的行。

最后,我们将计算发射的事件数量,并将结果发布给期望整数结果的Subscriber

这是之前功能管道的图形表示。

转换 Observables

是的。丰富的转换操作符集允许我们创建一个复杂的函数处理链,该链能够在数据传输过程中进行转换,并以可读和功能性的方式将结果传递给任何Subscriber对象。

理解调度器

有一种错误的误解和信念,即RxJava处理默认是多线程的。Observable和由指定操作符应用的转换列表在订阅时发生的同一个线程上执行。

因此,在 Android 上,如果订阅在主线程上执行,操作符链处理将在主线程上运行,直到工作完成才会阻塞 UI。

虽然这种行为可能适用于轻量级处理任务,但当操作需要 IO 交互或 CPU 密集型计算时,任务执行可能会阻塞主 Thread 并导致应用程序崩溃,出现 ANR。

为了简化异步和并发执行,RxJava 框架允许我们定义一个 Scheduler 实体,该实体定义了工作单元执行的线程。

subscribeOn(Scheduler) 操作符允许我们设置定义订阅已发生的线程和 Observable 将开始操作的 Scheduler

如果未指定 Scheduler,Observable 和操作将在调用 subscribe 函数的线程上运行。

在 Android 上,通常从运行在主线程上的 Android Activity 或 Fragment 中调用 subscribe 函数,然后如果任何操作需要大量时间才能完成,它将阻塞 UI 线程并降低 UI 响应性。

通过控制订阅发生的线程,我们控制了 Observable 和其操作符将要执行的线程,甚至控制了订阅者将接收回调的线程。

observeOn(Scheduler) 允许我们设置定义回调(onNextonErroronCompleted)调用的线程的 Scheduler

在 Observable 和操作符链中,我们可以多次使用 ObserveOn 来更改计算将运行的线程。

为了简化 Scheduler 的使用,RxJavaRxAndroid 库编译了一个预定义的 Schedulers 列表,可以直接使用来创建多线程异步链。

  • Schedulers.immediate(): 默认 Scheduler,返回一个在当前线程中立即执行工作的 Scheduler

  • Schedulers.trampoline(): 返回一个 Scheduler,该 Scheduler 将当前线程中的工作排队,在当前工作完成后执行。

  • Schedulers.newThread(): 返回一个 Scheduler,创建一个新线程,并在新 Thread 上执行工作。

  • Schedulers.computation(): 返回一个用于计算密集型工作的 Scheduler。这可以用于事件循环、处理回调和其他计算工作。不要在此 Scheduler 上执行阻塞 IO 工作。此 Scheduler 使用固定大小的线程池,其大小取决于 CPU 以优化 CPU 使用并最小化 CPU 切换。

  • Schedulers.io(): 创建并返回一个 Scheduler,该 Scheduler 执行一个按需增长和缩小的线程池的工作,重用已创建的空闲线程来执行所需的工作。此 Scheduler 旨在异步执行阻塞 IO 任务,如网络或文件系统读写。

  • Scheduler.from(Executor): 创建一个将在作为参数传递的java.util.concurrent.Executor上执行工作单元的Scheduler

  • AndroidSchedulers.mainThread(): 创建一个在 Android 应用程序主线程上执行所需工作的Scheduler。这个由RxAndroid库提供的 Android Scheduler基于运行工作单元的串行HandlerThread

  • HandlerScheduler.from(Handler): 创建一个在指定的Handler上执行工作的SchedulerAndroidSchedulers.mainThread()是这个Scheduler的一个特殊化,它在连接到 Android UI 线程的Handler上运行。

注意

默认情况下,Rxjava使用Schedulers.immediate(),它将观察者订阅到当前线程,并在当前线程上传递事件。

RxJava 允许我们定义自己的调度器,但就本书的范围而言,我们只会使用内置的调度器来满足我们的并发需求。

使用调度器执行 IO 操作

在下一个示例中,我们将使用Schedulers来模拟AsyncTask的行为,并在后台线程上从网络检索文本。随后,结果将发布到一个在主Thread上运行的Subscriber

首先,我们将创建一个函数,该函数创建一个Observable,它发出从网络检索到的String

Observable<String> getTextFromNetwork(final String url) {

  return Observable.create(
    new Observable.OnSubscribe<String>() {
      @Override
      public void call(Subscriber<? super String> sub) {
        try {
          String text = downloadText(url);
 sub.onNext(text);
 sub.onCompleted();

        } catch (Throwable t) {
 sub.onError(t);
        }
      }
    }
  );
}

在我们指定用于运行异步调用的Scheduler之前,我们需要声明两个假设:

  • 由于在Observable上运行代码执行网络操作,我们必须在后台线程上运行Observable

  • 为了发布结果并更新 UI,我们必须在主Thread上执行我们的订阅者回调

现在,让我们根据之前的假设和前面描述的Scheduler实体来构建异步的RxJava执行,以检索文本并更新 UI:

class MySubscriber extends Subscriber<String> {

  @Override
  public void onCompleted() {}

  @Override
  public void onError(Throwable e) {
     // Shows a Toast on Error
     Toast.makeText(RxSchedulerActivity.this,
                    e.getMessage(),
                    Toast.LENGTH_LONG).show();
    Log.e(TAG, "Error retrieving ", e);  
  }

  @Override
  public void onNext(String text) {
    // Updates the UI on Success
    EditText textFrame = (EditText)findViewById(R.id.text);
    textFrame.setText(text);
  }
};   

   ...

getTextFromNetwork("http://demo1472539.mockable.io/mytext")
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(new MySubscriber())
);

subscribeOn(Schedulers.io())将使由getTextFromNetwork函数创建的Observable在用于阻塞 IO 操作的Scheduler.io线程池上运行。

一旦我们调用subscribe函数,downloadText将被排队在由Schedulers.io()创建的Scheduler管理的线程上运行,在onNext()函数中以String的形式发出结果。

observeOn(AndroidSchedulers.mainThread())确保Subscriber回调onNextonCompletedonError将在 Android 主线程上运行。因此,如果网络操作成功完成,将调用OnNext更新 EditText 以显示获得的结果。

如果在网络执行过程中抛出任何异常,则将一个Throwable对象传递给Subscriber.onError回调,该回调在 UI 线程上执行,并在 UI 上显示一个显示错误的Toast

此示例展示了在RxJava上异步call是多么简单和简洁。此外,它抽象了您从线程管理中,就像AsyncTask所做的那样,并提供异常处理功能来处理异常错误。

取消订阅

当活动(Activity)或片段(Fragment)被销毁时,我们的链式操作可能会在后台继续运行,如果链式操作中包含对 Activity 或 Fragment 的引用,则可能会阻止 Activity 被销毁。当你不再需要链式操作的结果时,取消订阅并终止链式操作执行是有意义的。

当我们调用Observable.subscribe()函数时,它返回一个 Subscription 对象,可以用来立即终止链式操作:

Subscription subscription = getTextFromNetwork(
               "http://demo1472539.mockable.io/mytet")
               ...
               .subscribe(new MySubscriber());

再次强调,对于这个任务来说,最合适的 Activity 生命周期方法是onPause,因为它在 Activity 完成之前一定会被调用:

protected void onPause() {
  super.onPause();
  if ((subscription != null) && (isFinishing()))
    subscription.unsubscribe();
}

编写可观察对象

正如我们之前解释的,Observable接口是以一种允许我们链式组合不同的Observables来以函数式和声明式的方式创建复杂任务的方式定义的。

从我们之前的工作开始,在我们的下一个示例中,我们将利用RxJava的组成特性,执行一个依赖于前一个Observable的第二个网络调用,该Observable将使用网络服务下载的文本进行翻译,在我们将翻译后的文本发射到Subscriber之前。

为了在逻辑上独立的单元上执行网络翻译,我们将创建一个新的Observable,该Observable接收要翻译的文本,在网络中执行任务,并将翻译后的文本作为 String 类型输出给后续的Observable

Observable<String> translateOnNetwork(final String url,
                                      final String toTranslate) {
  return Observable.create(
    new Observable.OnSubscribe<String>() {
      @Override
      public void call(Subscriber<? super String> ts){
        try {
          String text = translateText(
            "http://demo1472539.mockable.io/translate",
             toTranslate);

 sub.onNext(text);
 sub.onCompleted();
        } catch (Throwable t) {
 sub.onError(t);
        }
      }
    }
  );
}

接下来,我们准备使用之前使用的相同Subscriber来链式执行网络操作并在 UI 上显示结果:

getTextFromNetwork(RETRIEVE_TEXT_URL)
  .flatMap(new Func1<String, Observable<String>>() {
    @Override
    public Observable<String> call(String toTranslate) {
      return translateOnNetwork(TRANSLATE_URL, toTranslate);
    }
  })
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(new MySubscriber());

translateOnNetwork上定义的网络 IO 操作,它依赖于getTextFromNetwork,只有在前一个操作成功完成后才会运行,并将getTextFromNetwork的结果作为参数。

translateOnNetwork Observable接收到前一个网络操作中的文本内容后,它将使用这些内容作为其操作的输入,并在网络上对前一个内容进行翻译,调用函数translateText(url, content)

由于translateText()成功完成,翻译后的内容被传递到下一个Observable。由于下一个ObservableSubscriber,结果在主线程上透明地传递,以更新 UI。

此外,由于我们重写了SubscriberonError函数,如果在执行网络请求的过程中出现错误,错误将被传播到我们的回调函数中,以便得到适当的处理。因此,通过几行代码,我们能够通知用户异步任务失败,并且我们未能将预期的数据传递给他们。

太好了,我们用几行代码创建了一个复杂的任务,该任务在后台执行一系列异步网络操作,在主线程上交付结果,或者在出现错误时交付错误。

监控事件流

虽然到目前为止我们一直在使用 Observable 操作符来操作流事件,但也有一些操作符允许我们监控事件而不改变它们。这些操作符有时被称为实用操作符,能够在源 Observable 和最终 Subscriber 之间创建的 Observable 链上对事件或错误做出反应,而不产生任何副作用。

让我们列举它们并解释更常见的用于观察事件流的实用操作符:

  • doOnSubscribe(Action0):注册一个 Action0 函数,当 Subscriber 订阅 Observable 时调用。

  • doOnUnsubscribe(Action0):注册一个 Action0 函数,当 SubscriberObservable 取消订阅时调用。

  • doOnNext(Action1):注册一个 Action1,当源 Observable 发出新事件时调用。事件 <T> 对象也作为参数传递给 Action1 函数。

  • doOnCompleted(Action0):注册一个 Action0 函数,当源 Observable 发出 onComplete 事件时调用。

  • doOnError(Action1):注册一个 Action1 函数,当源 Observable 发出错误时调用。OnError 上发出的 Throwable 也传递给 Action1.call 函数。

  • doOnTerminate(Action0):注册一个 Action0 函数,当源 Observable 发出错误或 onComplete 时调用。此回调函数还意味着之前的 Observable 将不再发出更多项目。

这些多用途操作符将允许我们观察和调试通常涉及多个转换的复杂链,创建进度对话框显示进度,缓存结果,甚至生成处理分析。

在我们的下一个示例中,我们将使用这些操作符在 Android 日志中记录我们之前的多网络操作的进度,并在操作进行时在屏幕上显示进度对话框:

Observable.just(RETRIEVE_TEXT_URL)
  .doOnNext(new Action1<String>() { // Runing on the main Thread
    @Override
    public void call(String url) {
      progress = ProgressDialog.show(RxSchedulerActivity.this,
                 "Loading",
                 "Performing async operation", true);
      Log.i(TAG, "Network IO Operation will start "+ tmark());
    }
  })
  .observeOn(Schedulers.io()) // Running on a background Thread
  .flatMap(new Func1<String, Observable<String>>() {
    @Override
    public Observable<String> call(String url) {
      return getTextFromNetwork(url);
    }
  })
  .doOnNext(new Action1<String>() {
    @Override
    public void call(String text) {
      Log.i(TAG, "Text retrieved w/ success " + tMark());
      Log.i(TAG, "Translating the text " + tMark());
    }
  })
  .flatMap(new Func1<String, Observable<String>>() {
    @Override
    public Observable<String> call(String toTranslate) {
      return translateOnNetwork(TRANSLATE_URL, toTranslate);
    }
  })
  .doOnNext(new Action1<String>() {
    @Override
    public void call(String translatedText) {
      Log.i(TAG, "Translation finished " + tMark());
    }
  })
  .observeOn(
    AndroidSchedulers.mainThread() // Executing on main Thread
   )
  .doOnTerminate(new Action0() {
    @Override
    public void call() {  
      if (progress != null)
        progress.dismiss();
      Log.i(TAG, "Dismissing dialog " + tMark());
    }
  })
  // Starts the execution on the main Thread
  .subscribeOn(AndroidSchedulers.mainThread())
  .subscribe(new MySubscriber());

如你所知,要在 Android UI 中进行更改,运行代码在主线程上至关重要。因此,为了在主线程中从第一个 Observable 接收 doOnNext,我们使用 subscribeOn() 并传入 AndroidSchedulers.mainThread(),强制第一个 Observable(使用 just 操作符创建的),向主线程中的 doOnNext 发送通知。

一旦 doOnNext() 收到包含要检索文本的 URL 的字符串的通知,我们就在 UI 中显示进度对话框,并在 Android 日志中记录一条消息。

接下来,由于我们想在主线程之外执行网络操作,使用 observeOn 操作符,我们强制后续 Observable 向由 IO Scheduler 管理的线程发送通知。这意味着后续的操作符和 Observable 将在 IO Scheduler 线程中执行并发出事件。

同时,在每次网络操作之间,我们拦截第二个网络操作的开始,在getTextFromNetworktranslateOnNetwork Observables之间使用doOnNext打印 Android 中的消息。

当网络操作完成时,在我们用结果更新 UI 并关闭进度对话框之前,我们再次通过调用带有主Thread SchedulerobserveOn()操作符将执行切换到主线程。

在我们在屏幕上显示结果之前,使用doOnTerminate操作符注册一个Action函数,以便在之前启动的进度对话框中调用以关闭它。如前所述,无论链路以成功或错误结束,该函数都将被调用。

最后,将调用Subscriber回调来更新 UI 显示返回的结果或显示错误信息。

如果网络操作以成功结束,你应在 Android 日志中看到类似的日志流:

...54.390 I Network IO Operation will start T[main]
...54.850 I Text retrieved w/ success T[RxCachedThreadScheduler-1]
...54.850 I Translating the text T[RxCachedThreadScheduler-1]
...55.160 I Translation finished T[RxCachedThreadScheduler-1]
...55.200 I Dismissing dialog on T[main]

为了调试目的,[<Thread_Name>]显示了记录消息的线程名称。

组合 Observables

在上一个示例中,我们使用了两个Observable来创建一个简单的网络操作序列。第二个异步操作使用第一个操作的结果操作,这两个按顺序执行的操作产生了一个字符串结果,用于更新 UI。

在我们的下一个示例中,我们将并行运行两个任务,并使用一个组合的RxJava操作符将两个操作的结果合并。每个操作将异步地从网络检索 JSON 对象,并将两个结果合并到 JSON 对象中,以生成传递给 UI 主Thread的 JSON 字符串。

由于我们只想从操作中发出一个事件或错误,我们将首次使用一种特殊的观察者类型,即Single

虽然Observable能够调用onNextonErroronCompleted观察者函数,但Single实体将只调用onSuccessonErrorSingleSubscriber

 // Success callback invoked on success
 void onSuccess(T value);

 // Callback to notify that an unrecoverable error has occurred
 void onError(Throwable error);

在其中一个回调函数被调用后,Single完成,对其的订阅结束。像常规Observable一样,Single对象发出的事件可以在到达最终的SingleSubscriber之前使用操作符进行处理。

现在,让我们定义两个从网络检索单个JSONObjectSingle操作:

Single<JSONObject> postSingle = Single.create(
  new Single.OnSubscribe<JSONObject>() {
    @Override
    public void call(SingleSubscriber<? super JSONObject> sub) {
      try {
        // Retrieve the Post content JSON Object
        sub.onSuccess(
         getJson("http://demo1472539.mockable.io/post"));
      } catch (Throwable t) {
        sub.onError(t);
      }
    }
  }
).subscribeOn(Schedulers.newThread());

Single<JSONObject> authorSingle = Single.create(
  new Single.OnSubscribe<JSONObject>() {
    @Override
    public void call(SingleSubscriber<? super JSONObject> sub) {
      try {
        // Retrieve the Author content JSON Object
        sub.onSuccess(
          getJson("http://demo1472539.mockable.io/author"));
      } catch (Throwable t) {
        sub.onError(t);
      }
    }
  }
).subscribeOn(Schedulers.newThread());

就像我们对之前的Observable所做的那样,我们使用了Single.create静态函数来构建一个自定义的Single实体,该实体在网络操作成功完成时显式调用SingleSubscriber.onSuccess函数,或者在getJsonIO 操作抛出错误时调用SingleSubscriber.onError函数。

getJSON函数基本上通过连接到提供的 HTTP URL 来检索 JSON 对象,并返回一个JSONObject

通过强制将 Single 对象 subscribeOnnewThread Scheduler,我们允许每个自定义的 Single 实体在新线程上并发运行它们的操作。

由于这两个操作将并行运行,我们需要使用组合操作符将 Single 结果组合成一个单一的 JSONObject,并将生成的 JSON String 发射到最终的 SingleSubscriber。对于我们的示例,合适的组合操作符是 zip,因为它能够等待两个或更多 Single/Observable 的结果,并对每个 Single 输出对象应用一个函数。

接收发射对象作为参数的函数可以产生相同类型或不同类型的结果。

这是将两个 Single 合并成一个 Single<R>zip 操作符函数定义:

   Single<R> zip(Single<T1> o1, // First Single
                 Single<T2> o2, // Second Single
                 final Func2<T1,T2,R> zipFunction)

在我们的示例中,R 是一个 StringT1T2JSONObject,而 zipFunction 接收 JSONObjects 参数以生成一个 String 作为结果。

现在,我们已经准备好使用 zip 操作符并将每个独立的异步操作的结果组合成一个 String。生成的字符串将更新一个 Widget,因此最终的 Subscriber 应该在主线程中调用。

让我们编写获取 JSONObject 部分并将生成的 String 分派到 UI 的功能代码:

Single.zip(postSingle, authorSingle,
           new Func2<JSONObject, JSONObject, String>() {
  @Override
  public String call(JSONObject post, JSONObject author) {
    String result = null;

    // Create the Root JSON Object
    JSONObject rootObj = new JSONObject();
    try {
      // Add the post object to root JSON Object
      rootObj.put("post", post);
      // Add the author object to root JSON Object
      rootObj.put("author", author);
      // Save the JSON Object, Encode the JSON Object
      // into a String
       result = rootObj.toString(2);
    } catch (Exception e) {
       Exceptions.propagate(e);
    }
    return result;
  }
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);

使用 zip 操作符,我们将两个操作的结果,postSingleauthorSingle,在由 newThread Scheduler 创建的新线程上组合,在接收两个 JSONObjects 作为参数并生成 StringFunc2 上。

由于我们已经将 Single 订阅到在其自己的线程上工作,zip 函数将在由最后定义的 Single (authorSingle) 构建的线程上合并两个 Singles 的结果,产生类似于以下输出的日志:

.040 I ...: Getting the Post Object on RxNewThreadScheduler-1
.050 I ...: Getting the Author Object on RxNewThreadScheduler-2
.660 I ...: Combining objects on RxNewThreadScheduler-2

在合并对象后,Func2 生成的 String 将被发送到主 Thread 中的最终 Subscriber

剩下的只是实现一个简单的 SingleSubscriber 来更新 UI:

SingleSubscriber<String> subscriber =
  new SingleSubscriber<String>() {
  ...
  @Override
  public void onSuccess(String result) { // Updates the UI }
};

使用 RxJava 观察 UI 事件

到目前为止,我们一直在使用 RxJava 处理和操作数据流,这简化了需要 IO 阻塞操作并可能挂起应用程序一段时间的异步开发。

在本节中,我们想要解释如何使用 RxJava 和响应式流来简化处理由 Android Widgets 生成的 UI 事件。

在我们的下一个示例中,我们将展示一个带有即时搜索结果输入字段的足球队伍列表。当你输入输入字段时,如果你输入的文本与列表中任何足球队伍的名称开头匹配,列表中的可用名称将被过滤。

为了达到所需的结果,我们将创建一个自定义的 Observable,该 ObservableTextWatcher 绑定到搜索输入字段,监听 onTextChanged 事件,并在文本更改时发射一个字符串事件。

观察者将提供一个响应式功能流,该流将过滤 Recycler View 中的队伍列表。

首先,我们将编写一个自定义可观察对象,当 Observer 订阅时,它会将 TextWatcher 注册到 EditField,并在订阅结束时注销 TextWatcher

public class TextChangeOnSubscribe
 implements OnSubscribe<String> {
  // Don't Prevent the GC from recycling the Activity
  WeakReference<EditText> editText;

  // Receive the EditText View to verify Changes
  public TextChangeOnSubscribe(EditText editText) {
    this.editText = new WeakReference<EditText>(editText);
  }

  @Override
  public void call(final Subscriber<? super String> subscriber) {
    final TextWatcher watcher = new TextWatcher() {

      @Override
      public void onTextChanged(
        CharSequence s, int start, int before, int count) {

        // Emit a new String when the text changes
        if (!subscriber.isUnsubscribed()) {
 subscriber.onNext(s.toString());
 }
      }
    };
    // Remove the Text change Watcher when the subscription ends
    subscriber.add(new MainThreadSubscription() {
      @Override
      protected void onUnsubscribe() {
        editText.get().removeTextChangedListener(watcher);
      }
    });
    // Sets the Watcher on the EditField
    editText.get().addTextChangedListener(watcher);
    subscriber.onNext("");
  }
};

...
EditText search = (EditText) findViewById(R.id.searchTv);
Observable<String> textChangeObs = Observable.
    create(new TextChangeOnSubscribe(search))
                                                     .debounce(400, TimeUnit.MILLISECONDS);

实现 OnSubscribe<String> 并接收订阅回调的 TextChangeOnSubscribe 类,一旦通过 Subscriber 执行订阅,就会在接收到的 EditField 中设置一个 TextWatcher

TextWatcher.onTextChanged 被调用以通知 EditField 中的文本更改时,应该在订阅者中发出一个新的包含新内容的字符串事件。

要在 EditField 中注销 TextWatcher,我们在订阅者列表中添加一个 MainThreadSubscription 匿名类,该类从 EditField 中移除我们的 TextChangeListener

为了防止文本更改事件在 UI 中生成过多的更新,我们使用了 debounce 操作符,只有在自上次文本更改事件以来有 400 毫秒的延迟时,才会发出新的搜索词。

接下来,我们将使用由我们的可观察对象生成的搜索事件来过滤 ReciclerView 列表中的队伍:

List<String> soccerTeams = Arrays.asList(
  "Real Madrid","Barcelona","Sporting CP",...,"Chelsea");

subcription = Observable.combineLatest(
  // Observables
 Observable.just(soccerTeams), textChangeObs,
  // Combine Function
  new Func2<List<String>, String, List<String>>() {

    // Filter the list with the filter String and sort the list
    @Override
    public List<String> call(List<String> fullList,
                             String filter){
      List<String> result = new ArrayList<String>();
      for (String team : fullList) {
        if (team.startsWith(filter)) {
          result.add(team);
        }
      }
 // Sort the Collection
      Collections.sort(result);
      return result;
    }
  })
 .observeOn(AndroidSchedulers.mainThread())
  .subscribe(new Action1<List<String>>() {
    @Override
    public void call(List<String> teams) {
      // Update the Recycler View with a filtered list of Teams
      mAdapter = new MyAdapter(teams);
      mRecyclerView.setAdapter(mAdapter);
    }
  });

为了通过 textChangeEvent 发射的搜索词过滤足球队伍列表,我们在 textChangeObs 可观察对象和通过 just 操作符创建的足球队伍列表的可观察对象上应用了 combineLatest 操作符。

combineLatest 将使用指定的函数组合每个可观察对象发出的最新项,并根据该函数调用的结果发出项。

组合这两个可观察对象的函数将简单地使用 onTextChanged 发出的最后文本内容过滤足球列表,并对结果列表进行排序。

为了最终完成,创建一个新的 RecyclerView.Adapter,使用结果 List<String>,并将过滤后的队伍列表显示给用户。

注意,为了使用结果过滤列表更新我们的 RecyclerView,我们通过将 Android 主线程 Scheduler 传递给 observeOn 操作符,显式地将 Observer 设置为在主线程上运行。

注意

不要忘记在销毁 Activity 之前通过调用 subcription.unsubscribe(); 终止订阅。

尽管出于教育目的,我们是从 Android EditField 小部件的文本更改事件中构建了自己的可观察对象,但有一个易于使用的开源库名为 RxBinding (github.com/JakeWharton/RxBinding),它能够为 Android SDK 中大多数可用的 Android 小部件创建可观察对象。

如果你不想实现自己的可观察对象,或者以传统方式处理 UI 事件,你可以利用它来使用功能性的 RxJava 反应式范式处理 Android UI 事件。

与主题一起工作

到目前为止,我们一直在使用 ObservablesSubscriberObserverScheduler 实体来创建我们的 RxJava 函数式处理流程。在本节中,我们将向读者介绍 RxJava 框架中的新实体,即 SubjectSubject 是一种适配器或桥梁实体,充当 ObservableObserver

public abstract class      Subject<T,R>
                extends    Observable<R>
                implements Observer<T>

由于它可以充当 Subscriber,它可以订阅一个或多个发出泛型类型 TObjectsObservables,并且由于它充当 Observable,它可以发出泛型类型 R 的事件并接收来自其他 Subscriber 的订阅。因此,它可以发出与接收相同类型的事件或发出不同类型的事件。

例如,Subject<String, Integer> 将接收类型为 String 的事件并发出类型为 Integer 的事件。

Subject 可以接收来自 Observable 的事件,并生成具有不同时序的新事件流,代理事件,转换为新的事件类型,排队事件,转换事件,甚至生成新事件。

Subject 总是被视为热 Observable,并在创建后立即开始发出事件。这是一个非常重要的 Subject 功能,当你想要处理完整的事件流序列时,你应该考虑它。

RxJava 包含一些标准 Subject 类,旨在用于不同的用例。以下列表将列举最常见的几个:

  • AsyncSubject:只有当源 Observer 通过调用 onComplete() 完成流时,才会发出源 Observable 最后发出的项目

  • PublishSubject:该 Subject 只向观察者传递订阅后发出的事件

  • ReplaySubject:发出源 Observable 发出的所有事件,即使是在订阅之前发出的

  • BehaviorSubject:在订阅完成后,发出源 Observable 最后发出的项目,然后继续发出源 Observable 发出的其他任何项目

在以下示例中,我们将向您展示如何使用 PublishSubject 并演示事件如何传播到最终订阅并后来取消订阅 SubjectObserver。此外,我们将在订阅前后向 Subject 提交事件:

PublishSubject<Integer> pubSubject = PublishSubject.create();
pubSubject.onNext(1);
pubSubject.onNext(2);
Subscription subscription = pubSubject.doOnSubscribe(new Action0() {
  @Override
  public void call() {
    Log.i(TAG, "Observer subscribed to PublishSubject");
  }
}).doOnUnsubscribe(new Action0() {
  @Override
  public void call() {
    Log.i(TAG, "Observer unsubscribed to PublishSubject");
  }
}).subscribe(new Action1<Integer>() {
  @Override
  public void call(Integer integer) {
    Log.i(TAG, "New Event received from PublishSubject: " + integer);
  }
});
pubSubject.onNext(3);
pubSubject.onNext(4);
subscription.unsubscribe();
pubSubject.onNext(5);
pubSubject.onCompleted();

首先,我们通过调用 PublishSubject.create 静态函数创建了 PublishSubject,然后我们开始向其中传递整数并调用 onNext 函数。

同时,我们使用 Action1 函数订阅了 Subject 以消费事件。

为了打印订阅和取消订阅的确切时间,我们向 doOnUnsubscribedoOnSubscribe 提供了一个 Action0 函数,该函数将消息打印到 Android 日志。

因此,上面的代码应该输出以下结果:

... 43.230 I Observer subscribed to PublishSubject
... 43.230 I New Event received from PublishSubject: 3
... 43.230 I New Event received from PublishSubject: 4
... 43.230 I Observer unsubscribed to PublishSubject

如前所述,只有当最终的 Observer 订阅时发出的事件才会被发射到 Action 回调。因此,在订阅之前提交和取消订阅之后提交的事件不会被我们的 Subscriber 接收。

现在,为了比较,让我们尝试比较由 ReplaySubject 发射的事件流与提交给 Subject 的确切事件序列。

再次强调,ReplaySubject 类是通过调用 create 静态函数创建的,因此你应该看到以下输出:

.600 I Observer subscribed to ReplaySubject
.600 I New Event received from ReplaySubject: 1
.600 I New Event received from ReplaySubject: 2
.600 I New Event received from ReplaySubject: 3
.600 I New Event received from ReplaySubject: 4
.600 I Observer unsubscribed to ReplaySubject

reactivex.io/documentation/subject.html 网站上,有一些图表可以帮助你直观地理解 Subject、Subscribers 和源 Observable 之间的交互。

如预期的那样,ReplaySubject 会接收提交给 Subject 的所有事件,即使是那些在订阅之前提交的事件也会被 Observer 接收。在 Observer 取消订阅后,它停止接收来自 Subject 的事件。

作为练习,你可以尝试为 AsyncSubjectBehaviorSubject 创建相同的操作。

概述

在本章的最后,我们学习了如何使用 RxJava,这是一个开源库,它帮助我们使用函数式和响应式处理管道处理 Android 应用程序的数据或事件流。

在前面的章节中,我们详细学习了 RxJava 的基本构建块——ObservableObserverSubscriber

接下来,我们介绍了一些 RxJava 最常见的操作符,这些操作符能够操作、转换和组合由 Observable 生成的事件流。

为了执行异步和并发操作,我们学习了 Scheduler,这是一个神奇的 RxJava 实体,它控制并发,并且能够将 RxJava 工作单元调度到后台线程,并将结果反馈到主 Android 线程。

接下来,通过使用自定义 Observables 和组合操作符,我们学习了如何关联和组合相互依赖的复杂阻塞或长时间计算操作,例如 REST API 网络操作。

同时,我们还学习了如何使用 RxJava 事件功能管道对发射 Android 小部件 UI 事件的自定义 Observable 进行响应。

最后,我们了解了 SubjectRxJava 实体,它可以充当 ObserverObservable,并且可以作为我们的源 Observable 和最终 Observer 之间的代理。

在本书的整个过程中,我们武装了自己,掌握了一系列强大的工具来构建响应式的 Android 应用程序。我们发现尽可能地将工作从主线程移除非常重要,并探讨了多种构造和异步技术,以提供最流畅和最酷的用户体验。

记住,为了保持应用程序的响应性并避免任何 UI 丢失帧,在主 UI 线程上运行的 Android 回调(ServiceActivity 等)应该在 16 毫秒内终止。

posted @ 2025-10-25 10:46  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报