Android5 高级教程(四)
十四、构建和消费服务
Android 平台提供了完整的软件堆栈。这意味着你得到一个操作系统和中间件,以及工作应用(如电话拨号器)。除此之外,您还有一个 SDK,可以用来为该平台编写应用。到目前为止,我们已经看到,我们可以构建通过用户界面直接与用户交互的应用。然而,我们还没有讨论后台服务或者构建在后台运行的组件的可能性。
在这一章中,我们将关注在 Android 中构建和消费服务。首先我们将讨论使用 HTTP 服务,然后我们将介绍一种完成简单后台任务的好方法,最后我们将讨论进程间通信——即同一设备上的应用之间的通信。
消费 HTTP 服务
Android 应用和移动应用通常都是具有大量功能的小应用。移动应用在如此小的设备上提供如此丰富的功能的方式之一是它们从各种来源获取信息。例如,大多数 Android 智能手机都带有地图应用,它提供了复杂的地图功能。然而,我们知道该应用集成了 Google Maps API 和其他服务,提供了大部分的复杂性。
也就是说,您编写的应用很可能也会利用来自其他应用和 API 的信息。一种常见的集成策略是使用 HTTP。例如,您可能在互联网上有一个 Java servlet,它提供了您希望从一个 Android 应用中利用的服务。你如何用 Android 做到这一点?有趣的是,Android SDK 附带了 Apache 的 http client(【hc.apache.org/httpcompone… 版本已经针对 Android 进行了修改,但是 API 与 Apache 版本中的 API 非常相似。
Apache HttpClient 是一个全面的 HTTP 客户端。它提供了对 HTTP 协议的全面支持。在这一节中,我们将讨论使用 HttpClient 来进行 HTTP GET 和 HTTP POST 调用。如果您正在使用 RESTful 服务,您可能还会使用其他 HTTP 操作(PUT、DELETE 等。).
对 HTTP GET 请求使用 HttpClient
下面是使用 HttpClient 的一般模式之一:
- 创建一个 HttpClient (或者获取一个现有的引用)。
- 实例化一个新的 HTTP 方法,比如 PostMethod 或者 GetMethod 。
- 设置 HTTP 参数名称/值。
- 使用 HttpClient 执行 HTTP 调用。
- 处理 HTTP 响应。
清单 14-1 展示了如何使用 HttpClient 执行 HTTP GET。
注意我们在本章末尾给了你一个 URL,你可以从本章下载项目。这将允许您将这些项目直接导入到 IDE 中。此外,因为代码试图使用互联网,当使用 HttpClient 进行 HTTP 调用时,您需要将 Android . permission . Internet 添加到您的清单文件中。
还要注意,在下面的例子中,所有的 web 服务调用都应该放在后台线程中,以免阻塞主 UI 线程。参见本章后面的内容,以及第十五章的,了解如何做到这一点。出于本章的目的,排除这些细节是为了帮助理解服务。
清单 14-1 。使用 HttpClient 和 http get:
public class HttpGetDemo extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
BufferedReader in = null;
try {
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet("[`code.google.com/android/`](http://code.google.com/android/)");
HttpResponse response = client.execute(request);
in = new BufferedReader(
new InputStreamReader(
response.getEntity().getContent()));
StringBuffer sb = new StringBuffer("");
String line = "";
String NL = System.getProperty("line.separator");
while ((line = in.readLine()) != null) {
sb.append(line + NL);
}
in.close();
String page = sb.toString();
System.out.println(page);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
HttpClient 能够使用各种 HTTP 请求类型,比如 HttpGet 、 HttpPost 等等。清单 14-1 使用 HttpClient 获取code.google.com/android/URL 的内容。实际的 HTTP 请求通过调用 client.execute() 来执行。执行请求后,代码将整个响应读入一个 string 对象。注意, BufferedReader 在 finally 块中是关闭的,这也关闭了底层的 HTTP 连接。
对于我们的例子,我们将 HTTP 逻辑嵌入到活动中,但是我们不需要在活动的上下文中使用 HttpClient 。您可以在任何 Android 组件的上下文中使用它,或者将其作为独立类的一部分使用。事实上,您不应该在活动中直接使用 HttpClient,因为 web 调用可能需要一段时间才能完成,并导致应用没有响应(ANR) 弹出窗口。我们将在本章的后面讨论这个话题。现在我们要稍微作弊一下,这样我们就可以专注于如何进行 HttpClient 调用。
清单 14-1 中的代码执行一个 HTTP 请求,而不向服务器传递任何 HTTP 参数。通过将名称/值对附加到 URL,您可以将名称/值参数作为请求的一部分传递,如清单 14-2 所示。
清单 14-2 。将 参数 添加到 HTTP GET 请求
HttpGet request =
new HttpGet("[`somehost/Upload.aspx?one=value1&two=value2`](http://somehost/Upload.aspx?one=value1&two=value2)");
client.execute(request);
当执行 HTTP GET 时,请求的参数(名称和值)作为 URL 的一部分传递。以这种方式传递参数有一些限制。也就是说,URL 的长度应该保持在 2048 个字符以下。如果要提交的数据超过这个数量,应该使用 HTTP POST。POST 方法更加灵活,它将参数作为请求体的一部分传递。
将 HttpClient 用于 HTTP POST 请求(一个多部分示例)
进行 HTTP POST 调用与进行 HTTP GET 调用非常相似(参见清单 14-3 )。这个例子叫做 SimpleHTTPPost。
清单 14-3 。用 HttpClient 制作一个 HTTP POST 请求
HttpClient client = new DefaultHttpClient();
HttpPost request = new HttpPost(
"[`www.androidbook.com/akc/display`](http://www.androidbook.com/akc/display)");
List<NameValuePair> postParameters = new ArrayList<NameValuePair>();
postParameters.add(new BasicNameValuePair("url", "DisplayNoteIMPURL"));
postParameters.add(new BasicNameValuePair("reportId", "4788"));
postParameters.add(new BasicNameValuePair("ownerUserId", "android"));
postParameters.add(new BasicNameValuePair("aspire_output_format", "embedded-xml"));
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(
postParameters);
request.setEntity(formEntity);
HttpResponse response = client.execute(request);
清单 14-3 中的代码将替换清单 14-1 中的三行代码,其中使用了 HttpGet 。其他一切都可以保持不变。要使用 HttpClient 进行 HTTP POST 调用,必须使用 HttpPost 的实例调用 HttpClient 的 execute() 方法。当进行 HTTP POST 调用时,通常将 URL 编码的名称/值表单参数作为 HTTP 请求的一部分进行传递。要使用 HttpClient 来实现这一点,您必须创建一个包含 NameValuePair 对象实例的列表,然后用一个 urlencodeformentity 对象包装该列表。 NameValuePair 包装了一个名称/值组合,UrlEncodedFormEntity 类知道如何编码一个适合 HTTP 调用(一般是 POST 调用)的 NameValuePair 对象列表。在您创建了一个 urlencodeformentity 之后,您可以将 HttpPost 的实体类型设置为 urlencodeformentity 然后执行请求。
在清单 14-3 中,我们创建了一个 HttpClient ,然后用 HTTP 端点的 URL 实例化了 HttpPost 。接下来,我们创建了一个由 NameValuePair 对象组成的列表,并用几个名称/值参数填充它。然后,我们创建了一个 urlencodeformentity 实例,将 NameValuePair 对象的列表传递给它的构造函数。最后,我们调用 POST 请求的 setEntity() 方法 ,然后使用 HttpClient 实例执行请求。
HTTP POST 其实比这个厉害多了。通过 HTTP POST,我们可以传递简单的名称/值参数,如清单 14-3 所示,也可以传递复杂的参数,如文件。HTTP POST 支持另一种称为多部分 POST 的请求正文格式。使用这种类型的 POST,您可以像以前一样发送名称/值参数以及任意文件。不幸的是,Android 自带的 HttpClient 版本不直接支持多部分 POST。为了在过去实现这个目标,我们建议您获取另外三个库:Apache Commons IO、Mime4j 和 HttpMime。
现在我们建议您下载 Ion 库,它有两个依赖项。这三个 jar 文件都可以在以下两个站点找到:
- 【https://github.com/koush/ion#jars】(离子与安卓同步)
- (gson)
清单 14-4 展示了一个使用 Android 的多部分帖子。这个例子叫做 MultipartHTTPPost。
清单 14-4 。制作一个 多部分帖子调用
public class TestMultipartPost extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
try {
Ion.with(this, "[`www.androidbook.com/akc/update/PublicUploadTest`](http://www.androidbook.com/akc/update/PublicUploadTest)")
.setMultipartParameter("field1", "This is field number 1")
.setMultipartParameter("field2", "Field 2 is shorter")
.setMultipartFile("datafile",
new File(Environment.getExternalStorageDirectory()+"/testfile.txt"))
.asString()
.setCallback(new FutureCallback<String>() {
@Override
public void onCompleted(Exception e, String result) {
System.out.println(result);
}});
} catch(Exception e) {
// Do something about exceptions
System.out.println("Got exception: " + e);
}
}
}
注意多部分示例使用了几个。不包含在 Android 运行时中的 jar 文件。确保。jar 文件将被打包成您的的一部分。apk 文件,你需要将它们添加为外部。Eclipse 中的 jar 文件。为此,在 Eclipse 中右键单击您的项目,选择 Properties,选择 Java Build Path,选择 Libraries 选项卡,然后选择 Add External JARs。
遵循这些步骤将使。jar 文件在编译时和运行时都可用。
要使用 Ion 库执行多部分 POST,只需将适当的调用放在一起构建 URL、添加参数、定义返回类型并设置回调方法。这将异步运行,一旦从 web 服务器收到响应,将在 UI 线程上调用回调。在该示例中,结果字符串被写入 LogCat。您的应用可能会接收回一个 JsonObject,然后回调函数会对其进行处理。但是要意识到,来自 web 服务器的响应已经被转换为 JsonObject,这使得回调中的处理变得更加容易。清单 14-4 给请求添加了三个部分:两个字符串部分和一个文本文件。要自己运行这个示例,您需要将 testfile.txt 文件放到设备或仿真器的外部存储区域。
最后,如果您正在构建一个需要向 web 资源传递多部分 POST 的应用,您可能需要在本地工作站上使用服务的虚拟实现来调试解决方案。当您在本地工作站上运行应用时,通常您可以通过使用 localhost 或 IP 地址 127.0.0.1 来访问本地机器。然而,对于 Android 应用,你将无法使用 localhost (或 127.0.0.1 ),因为设备或仿真器将是它自己的 localhost 。您不想将此客户端指向 Android 设备上的服务;你想指向你的工作站。要从设备或仿真器中运行的应用引用您的开发工作站,您必须在 URL 中使用工作站的 IP 地址。
SOAP、JSON 和 XML 解析器
肥皂呢?互联网上有很多基于 SOAP 的 web 服务,但是到目前为止,Google 还没有在 Android 中提供对调用 SOAP web 服务的直接支持。相反,谷歌更喜欢类似 REST 的网络服务,似乎是为了减少客户端设备所需的计算量。然而,代价是开发人员必须做更多的工作来发送数据和解析返回的数据。理想情况下,对于如何与 web 服务交互,您将有一些选择。一些开发人员已经使用 kSOAP2 开发工具包来为 Android 构建 SOAP 客户端。我们不会讨论这种方法,但是如果您感兴趣,它就在那里。
注原 kSOAP2 源位于此:。开源社区已经(谢天谢地!)贡献了一个安卓版的 kSOAP2,可以在这里了解更多:code.google.com/p/ksoap2-android/。
一种已经成功使用的方法是在互联网上实现您自己的服务,它可以与目的地服务进行 SOAP(或其他)对话。然后你的 Android 应用只需要和你的服务对话,你现在就有了完全的控制权。如果目标服务发生了变化,您也许能够处理它,而不必更新和发布应用的新版本。你只需要更新服务器上的服务。这种方法的另一个好处是,您可以更容易地为您的应用实现付费订阅模型。如果用户让他们的订阅失效,您可以在您的服务器上关闭它们。
Android是否支持 JavaScript 对象符号(JSON) 。这是在 web 服务器和客户端之间打包数据的一种相当常见的方法。JSON 解析类使得从响应中解包数据变得非常容易,因此您的应用可以对其进行操作。或者更深入地研究本章前面提到的 Gson 包。Gson 是 Google 的一个 JSON Java 库,它的主要好处是很容易将 JSON 输入解析成 Java 对象,反之亦然。也很快。
*Android 也有一些 XML 解析器,可以用来解释 HTTP 调用的响应;推荐的是 XMLPullParser 。
处理异常
处理异常是任何程序的一部分,但是使用外部服务(比如 HTTP 服务)的软件必须额外注意异常,因为出错的可能性被放大了。在使用 HTTP 服务时,您可能会遇到几种类型的异常。这些是传输异常、协议异常和超时。您应该了解这些异常可能发生的时间。
传输异常可能因多种原因而发生,但移动设备最有可能出现的情况是网络连接不良。协议异常(例如,ClientProtocolException)是 HTTP 协议层的异常。这些错误包括身份验证错误、无效的 cookies 等等。例如,如果您必须提供登录凭证作为 HTTP 请求的一部分,但却没有这样做,那么您可能会看到协议异常。关于 HTTP 调用的超时,有两种类型:连接超时和套接字超时。如果 HttpClient 无法连接到 HTTP 服务器,例如,如果服务器不可用,则可能发生连接超时(例如,ConnectTimeoutException)。如果 HttpClient 未能在定义的时间段内接收到响应,则会发生套接字超时(例如 SocketTimeoutException)。换句话说, HttpClient 能够连接到服务器,但是服务器无法在分配的时间限制内返回响应。
现在您已经了解了可能发生的异常类型,那么您如何处理它们呢?幸运的是, HttpClient 是一个健壮的框架,可以帮您卸下大部分负担。事实上,您唯一需要担心的异常类型是那些您能够轻松管理的异常类型。 HttpClient 通过检测传输问题和重试请求来处理传输异常(这对于这种类型的异常非常有效)。协议异常是通常可以在开发过程中清除的异常。超时是您必须处理的最有可能的异常。处理这两种超时(连接超时和套接字超时)的一种简单而有效的方法是用一个 try / catch 包装 HTTP 请求的 execute() 方法,然后在失败时重试。
当使用 HttpClient 作为现实世界应用的一部分时,您需要注意可能出现的多线程问题。现在就来深究这些吧。
解决多线程问题
到目前为止,我们展示的例子为每个请求创建了一个新的 HttpClient 。然而,实际上,您可以为整个应用创建一个 HttpClient ,并将其用于所有的 HTTP 通信。可以将连接池与这个 HttpClient 相关联,您现在将看到这一点。用一个 HttpClient 服务所有的 HTTP 请求,您应该注意多线程问题,如果您通过同一个 HttpClient 同时发出请求,可能会出现多线程问题。幸运的是, HttpClient 提供了使这变得容易的工具——您所要做的就是使用 ThreadSafeClientConnManager 创建 DefaultHttpClient ,如清单 14-5 所示。这个示例项目是 HttpSingleton。
清单 14-5 。为多线程创建一个 http client:CustomHttpClient.java
public class CustomHttpClient {
private static HttpClient customHttpClient;
/** A private Constructor prevents instantiation */
private CustomHttpClient() {
}
public static synchronized HttpClient getHttpClient() {
if (customHttpClient == null) {
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params,
HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
HttpProtocolParams.setUserAgent(params,
System.getProperty("http.agent")
// Could also have used the following which is browser-oriented as opposed to
// device-oriented:
// new WebView(getApplicationContext()).getSettings().getUserAgentString()
);
ConnManagerParams.setTimeout(params, 1000);
HttpConnectionParams.setConnectionTimeout(params, 5000);
HttpConnectionParams.setSoTimeout(params, 10000);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https",
SSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager conMgr = new
ThreadSafeClientConnManager(params,schReg);
customHttpClient = new DefaultHttpClient(conMgr, params);
}
return customHttpClient;
}
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
如果您的应用需要进行多次 HTTP 调用,那么您应该创建一个 HttpClient 来服务您所有的 HTTP 请求。最简单的方法是创建一个可以从应用的任何地方访问的单例类,就像我们在这里展示的那样。这是一个相当标准的 Java 模式,在这种模式中,我们同步对 getter 方法的访问,getter 方法为单例对象返回唯一的 HttpClient 对象,在必要时第一次创建它。
现在,看看 CustomHttpClient 的 getHttpClient() 方法 。这个方法负责创建我们的单体 HttpClient 。我们设置一些基本参数,一些超时值,以及我们的 HttpClient 将支持的方案(即 HTTP 和 HTTPS)。注意,当我们实例化 DefaultHttpClient() 时,我们传入了一个 ClientConnectionManager。ClientConnectionManager 负责管理 HttpClient 的 HTTP 连接。因为我们想对所有 HTTP 请求使用一个单独的 HttpClient (如果我们使用线程,请求可能会重叠),所以我们创建了一个 ThreadSafeClientConnManager。
我们还向您展示了一种从 HTTP 请求中收集响应的更简单的方法,使用一个 BasicResponseHandler 。使用我们的 CustomHttpClient 的活动代码在清单 14-6 中。
清单 14-6 。使用我们的 custom http client:HttpActivity.java
public class HttpActivity extends Activity
{
private HttpClient httpClient;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
httpClient = CustomHttpClient.getHttpClient();
getHttpContent();
}
public void getHttpContent()
{
try {
HttpGet request = new HttpGet("[`www.google.com/`](http://www.google.com/)");
String page = httpClient.execute(request,
new BasicResponseHandler());
System.out.println(page);
} catch (IOException e) {
// covers:
// ClientProtocolException
// ConnectTimeoutException
// ConnectionPoolTimeoutException
// SocketTimeoutException
e.printStackTrace();
}
}
}
对于这个示例应用,我们对 Google 主页进行了简单的 HTTP get。我们还使用一个 BasicResponseHandler 对象来将页面呈现为一个大的字符串,然后我们将它写到 LogCat 中。如您所见,向 execute() 方法添加一个 BasicResponseHandler 非常容易。
每个 Android 应用都有一个关联的应用对象,您可能会想利用这一事实。默认情况下,如果不定义自定义应用对象,Android 使用 android.app.Application 。关于 application 对象有一件有趣的事情:对于您的应用,始终只有一个 application 对象,并且您的所有组件都可以访问它(使用全局上下文对象)。可以扩展应用类并添加功能,比如我们的 CustomHttpClient 。然而,在我们的例子中,实际上没有理由在应用类本身中这样做,当您可以简单地创建一个单独的单例类来处理这种类型的需求时,您最好不要弄乱应用类。
超时的乐趣
为我们的应用设置一个单独的 HttpClient 还有其他非常好的优势。我们可以在一个地方修改它的属性,每个人都可以利用它。例如,如果我们想为我们的 HTTP 调用设置公共超时值,我们可以在创建我们的 HttpClient 时,通过对我们的 HttpParams 对象调用适当的 setter 函数来实现。请参考清单 14-5 和 getHttpClient() 方法。请注意,我们可以使用三种暂停。第一个是连接管理器的超时,它定义了从连接管理器管理的连接池中获取连接需要等待多长时间。在我们的例子中,我们将其设置为 1 秒。我们唯一可能需要等待的时候是池中的所有连接都在使用中。第二个超时值定义了我们应该等待多长时间才能通过网络连接到另一端的服务器。这里,我们使用了 2 秒的值。最后,我们将套接字超时值设置为 4 秒,以定义我们应该等待多长时间来获取请求的数据。
对应于前面描述的三个超时,我们可以得到这三个异常:ConnectionPoolTimeoutException、 ConnectTimeoutException 或 SocketTimeoutException 。所有这三个异常都是 IOException 的子类,我们在 HttpActivity 中使用了它,而不是单独捕获每个子类异常。
如果您研究我们在 getHttpClient() 中使用的每个参数设置类,您可能会发现更多有用的参数。
我们已经为您描述了如何建立一个带有连接池的 HttpClient,以便在您的应用中使用。这意味着,每当您需要使用连接时,各种设置将适用于您的特定需求。但是,如果您希望对特定的消息进行不同的设置,该怎么办呢?谢天谢地,有一个简单的方法可以做到这一点。我们向您展示了如何使用一个 HttpGet 或一个 HttpPost 对象来描述通过网络发出的请求。以类似于我们在 HttpClient 上设置 HttpParams 的方式,您可以在 HttpGet 和 HttpPost 对象上设置 HttpParams 。您在消息级别应用的设置将覆盖 HttpClient 级别的设置,而不会更改 HttpClient 的设置。清单 14-7 显示了如果我们想让一个特定请求的套接字超时为 1 分钟而不是 4 秒钟,这可能会是什么样子。您可以使用这些行来代替清单 14-6 中 getHttpContent() 的 try 块中的行。
清单 14-7 。在请求级别覆盖套接字超时
HttpGet request = new HttpGet("[`www.google.com/`](http://www.google.com/)");
HttpParams params = request.getParams();
HttpConnectionParams.setSoTimeout(params, 60000); // 1 minute
request.setParams(params);
String page = httpClient.execute(request,
new BasicResponseHandler());
System.out.println(page);
使用 HttpURLConnection
Android 提供了另一种处理 HTTP 服务的方式,那就是使用 Java . net . httpurlconnection 类。这与我们刚刚讨论过的 HttpClient 类没有什么不同,但是 HttpURLConnection 倾向于需要更多的语句来完成任务。HttpURLConnection 也不是线程安全的。另一方面,这个类比 HttpClient 小得多,也轻得多,所以您可以简单地创建您需要的类。从 Gingerbread 版本开始,它也相当稳定,所以当您只需要基本的 HTTP 功能并且想要一个紧凑的应用时,您应该考虑将其用于更新设备上的应用。
使用 AndroidHttpClient
Android 2.2 引入了 HttpClient 的一个新子类,叫做 AndroidHttpClient 。这个类背后的想法是通过提供适用于 Android 应用的默认值和逻辑,使 Android 应用的开发变得更加容易。例如,连接和套接字(即操作)的超时值都默认为 20 秒。连接管理器默认为线程安全客户端连接管理器。在很大程度上,它可以与我们在前面的例子中使用的 HttpClient 互换。但是,您应该知道一些不同之处:
-
为了创建一个 AndroidHttpClient ,您调用 AndroidHttpClient 类的静态 newInstance() 方法,就像这样:
AndroidHttpClient httpClient = AndroidHttpClient.newInstance("my-http-agent-string"); -
Notice that the parameter to the newInstance() method is an HTTP agent string. You most likely don’t want to hardcode this, so you have two options as follows, which unfortunately can return different strings. The second one is probably the one you want to use as it looks more like what a browser would send (at least in our experiments).
// The first option is a device-level agent string String httpAgent = System.getProperty("http.agent"); // This second option looks like a browser’s agent string httpAgent = new WebView(context).getSettings().getUserAgentString();当然,你也可以使用任何你的应用可用的东西来构建你自己的代理字符串;服务器将解析它以更好地理解设备,如果你控制服务器,你可以使用你从应用发送的任何值。
-
当在这个客户端上调用 execute() 时,您必须在一个独立于主 UI 线程的线程中。这意味着如果你试图用一个和一个来替换我们之前的 HttpClient ,你会得到一个异常。从主 UI 线程进行 HTTP 调用是不好的做法,所以 AndroidHttpClient 不会让你这样做。我们将在下一节讨论线程问题。
-
当您完成时,必须在 AndroidHttpClient 实例上调用 close() 。这样可以适当地释放内存。
-
有一些方便的静态方法来处理来自服务器的压缩响应,包括
- modifyrequesttoacceptgziprense(http request)
- get comprehensity(字节[]日期, 内容解析器 【解析器】
- getUngzippedContent(HttpEntity 实体)
一旦获得了 AndroidHttpClient 的实例,就不能修改其中的任何参数设置,也不能向其中添加任何参数设置(例如 HTTP 协议版本)。您的选择是覆盖前面所示的 HttpGet 对象中的设置,或者不使用 AndroidHttpClient 。
这就结束了我们对通过 HttpClient 使用 HTTP 服务的讨论。要获得关于使用 HttpClient 和这些其他概念的精彩教程,请访问 Apache 网站HC . Apache . org/httpcomponents-client-ga/tutorial/html/。
我们已经向您展示了如何操作基于 HTTP 的服务。但是,如果我们想要运行一些持续时间超过一小段时间的后台处理,或者如果我们想要调用另一个 Android 应用中存在的一些非 UI 功能,该怎么办呢?针对这些需求,Android 提供了服务。我们接下来将讨论它们。
使用安卓服务
Android 支持服务的概念。服务是在后台运行的组件,没有用户界面。您可以将这些组件视为类似于 Windows 服务或 Unix 守护程序。与这些类型的服务类似,Android 服务可以一直可用,但不必主动做些什么。更重要的是,Android 服务可以拥有独立于活动的生命周期。当一个活动暂停、停止或被销毁时,您可能希望继续进行一些处理。服务业对此也有好处。
Android 支持两种类型的服务:本地服务和远程服务。一个本地服务 是一个只能被托管它的应用访问的服务,它不能被设备上运行的其他应用访问。通常,这些类型的服务只是支持托管服务的应用。除了托管服务的应用之外,还可以从设备上的其他应用访问远程服务 。远程服务使用 Android 接口定义语言(AIDL) 向客户端定义自己。我们将讨论这两种类型的服务,尽管在接下来的几章中,我们将深入讨论本地服务。因此,我们将在这里介绍它们,但不会花太多时间。我们将在本章中更详细地讨论远程服务。
了解 Android 中的服务
Android Service 类是一种具有类似服务行为的代码包装器。然而,服务对象不会自动创建自己的线程。对于一个使用线程的服务对象,开发者必须让它发生。这意味着在没有给服务添加线程的情况下,服务的代码将在主线程上运行。如果我们的服务正在执行的操作会很快完成,这就不是问题。如果我们的服务可能会运行一段时间,我们肯定希望包含线程。请记住,在服务中使用 AsyncTask s 进行线程处理没有任何问题。
Android 支持服务的概念有两个原因:
- 首先,允许您轻松实现后台任务。
- 第二,允许您在同一设备上运行的应用之间进行进程间通信。
这两个原因对应了 Android 支持的两类服务:本地服务和远程服务。第一种情况的例子可能是作为电子邮件应用的一部分实现的本地服务。该服务可以处理向电子邮件服务器发送新电子邮件,包括附件和重试。因为这可能需要一段时间才能完成,所以服务是包装该功能的一种很好的方式,这样主线程就可以启动它并返回给用户。此外,如果电子邮件活动停止,您仍然希望发送的电子邮件被传递。第二种情况的一个例子是语言翻译应用,我们将在后面看到。假设您有几个应用在一个设备上运行,您需要一个服务来接受需要从一种语言翻译成另一种语言的文本。您可以编写一个远程翻译服务,让应用与服务对话,而不是在每个应用中重复逻辑。
本地服务由使用 bindService() 绑定到它的客户端初始化,或者由使用 startService() 启动它的客户端初始化。远程服务通常总是用 bindService() 初始化。绑定的服务在第一个客户端绑定到它时被实例化,在最后一个客户端解除绑定时被销毁。当客户端进出前台时,它们可以根据需要绑定和解除绑定,以确保服务不会不必要地运行。这有助于延长电池寿命。但是,在 onResume()中绑定而在 onPause()中取消绑定是不明智的,因为这可能会导致大量不必要的服务启动和停止。最好在 onCreate()和 onDestroy()中绑定和解除绑定,或者在 onStart()和 onStop()中绑定和解除绑定。仅允许从应用上下文、活动、另一个服务或内容提供者进行绑定。这意味着不是来自片段,也不是来自广播接收器。
相反,当使用 startService()启动服务时,它将一直运行,直到被客户端或告诉自己停止而停止。对于希望在后台执行工作的本地服务,可以考虑用 startService()实例化它,这样即使启动它的活动消失,它也可以保持运行。从技术上讲,广播接收器可以使用 startService()启动服务,因为一旦短暂的广播接收器终止,服务就可以继续存在。如果您确实创建了一个即使在活动已经消失的情况下也将在后台运行的服务,那么您可能希望实现 onBind(),以便用户能够重新获得对该服务的控制。一个新的活动可以绑定到现有的服务,然后调用它的服务方法。
有不创建后台线程的本地服务的例子,但是这在实践中可能不是很有用。服务本身并不创建任何线程,因此默认情况下,服务的代码将在主 UI 线程上运行。将这些代码包装在服务中可能没有任何真正的好处,因为您可以只调用类的方法来执行该逻辑。更常见的是,本地服务有自己的执行线程,这些线程可以在第一个客户端绑定到它时启动,也可以因为 startService()命令而启动。
现在,我们可以开始详细检查这两种类型的服务。我们将从讨论本地服务开始,然后讨论远程服务。如前所述,本地服务是仅由承载它们的应用调用的服务。远程服务是支持远程过程调用(RPC)机制 的服务。这些服务允许同一设备上的外部客户端连接到服务并使用其设施。调用远程服务有两种主要方式:使用 AIDL 接口和使用信使。两者都包括在内。
注意Android 中的第二种服务有几个名字:远程服务、AIDL 支持服务、AIDL 服务、外部服务和 RPC 服务。这些术语都是指同一种类型的服务——一种可以被设备上运行的其他应用远程访问的服务。
了解本地服务
本地服务是通常通过 Context.startService() 启动的服务。一旦启动,这些类型的服务将继续运行,直到客户端调用服务上的 Context.stopService() 或者服务本身调用 stopSelf() 。注意,当 Context.startService() 被调用,服务还没有被创建时,系统会实例化服务,并调用服务的 onStartCommand() 方法 。请记住,在服务启动后(即当它存在时)调用 Context.startService() 不会导致服务的另一个实例,但会重新调用正在运行的服务的 onStartCommand() 方法。这里有几个本地服务的例子:
- 一种服务,用于监控来自设备的传感器数据并进行分析,如果达到某个条件,就会发出警报。该服务可能会持续运行。
- 一个任务执行程序服务,允许您的应用的活动提交作业并将它们排队等待处理。此服务可能仅在提交作业的操作期间运行。
清单 14-8 通过实现一个执行后台任务的服务来演示一个本地服务。我们最终将得到创建和消费服务所需的四个构件:【BackgroundService.java】(服务本身)【main . XML】(活动的布局文件)【MainActivity.java】(调用服务的活动类),以及 AndroidManifest.xml 。清单 14-8 仅包含 BackgroundService.java。我们将首先剖析这段代码,然后再看其他三段。
清单 14-8 。实现本地服务:
public class BackgroundService extends Service
{
private static final String TAG = "BackgroundService";
private NotificationManager notificationMgr;
private ThreadGroup myThreads = new ThreadGroup("ServiceWorker");
@Override
public void onCreate() {
super.onCreate();
Log.v(TAG, "in onCreate()");
notificationMgr =(NotificationManager)getSystemService(
NOTIFICATION_SERVICE);
displayNotificationMessage("Background Service is running");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
int counter = intent.getExtras().getInt("counter");
Log.v(TAG, "in onStartCommand(), counter = " + counter +
", startId = " + startId);
new Thread(myThreads, new ServiceWorker(counter),
"BackgroundService")
.start();
return START_STICKY;
}
class ServiceWorker implements Runnable
{
private int counter = -1;
public ServiceWorker(int counter) {
this.counter = counter;
}
public void run() {
final String TAG2 = "ServiceWorker:" +
Thread.currentThread().getId();
// do background processing here... we'll just sleep...
try {
Log.v(TAG2, "sleeping for 10 seconds. counter = " +
counter);
Thread.sleep(10000);
Log.v(TAG2, "... waking up");
} catch (InterruptedException e) {
Log.v(TAG2, "... sleep interrupted");
}
}
}
@Override
public void onDestroy()
{
Log.v(TAG, "in onDestroy(). Interrupting threads and cancelling notifications");
myThreads.interrupt();
notificationMgr.cancelAll();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
Log.v(TAG, "in onBind()");
return null;
}
private void displayNotificationMessage(String message)
{
PendingIntent contentIntent =
PendingIntent.getActivity(this, 0,
new Intent(this, MainActivity.class), 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle(message)
.setContentText("Touch to turn off service")
.setSmallIcon(R.drawable.emo_im_winking)
.setTicker("Starting up!!!")
// .setLargeIcon(aBitmap)
.setContentIntent(contentIntent)
.setOngoing(true)
.build();
notificationMgr.notify(0, notification);
}
}
一个服务对象的结构有点类似于一个活动。有一个 onCreate() 方法可以用来进行初始化,还有一个 onDestroy() 方法可以用来进行清理。服务不像活动那样暂停或恢复,所以我们不使用 onPause() 或 onResume() 方法。在这个例子中,我们不会绑定到本地服务,但是因为服务需要实现 onBind() 方法 ,所以我们提供了一个返回 null 的服务。值得一提的是,您可以有一个实现 onBind()而不使用 onStartCommand()的本地服务。
回到我们的 onCreate() 方法,除了通知用户这个服务已经创建,我们不需要做太多事情。我们使用通知管理器来完成这项工作。你可能已经注意到了 Android 屏幕左上角的通知栏。通过拉下这个按钮,用户可以查看重要的消息,通过触摸通知可以对通知进行操作,这通常意味着返回到与通知相关的一些活动。对于服务,因为它们可以在后台运行,或者至少存在于后台,而没有可见的活动,所以必须有某种方法让用户重新接触到服务,也许是关闭它。因此,我们创建一个通知对象,用一个 pending content 填充它,这将使我们返回到我们的控件活动,并发布它。这一切都发生在 displayNotificationMessage()方法中。请注意,只要我们的服务存在,我们的通知对象就需要存在,因此我们使用 setOngoing(true) 将它保留在通知列表中,直到我们自己从服务的 onDestroy() 方法中将其清除。我们在 onDestroy() 中使用的清除通知的方法是 NotificationManager 上的 cancelAll() 。
这个例子还需要另外一个东西。您需要创建一个名为 emo_im_winking 的 drawable,并将其放在项目的 drawable 文件夹中。出于演示目的,一个很好的 drawables 来源是查看 Android 平台文件夹下的 AndroidSDK/platforms//data/RES/drawable,其中 < version > 是您感兴趣的版本。不幸的是,你不能从你的代码中可靠地引用 Android 系统的 drawables,所以你需要把你想要的复制到你的项目的 drawables 文件夹中。如果您为您的示例选择了不同的 drawable 文件,只需在通知的构造函数中重命名资源 ID。
当使用 startService() 将意图发送到我们的服务中时,如果需要,将调用 onCreate() ,并调用我们的 onStartCommand() 方法来接收调用者的意图。在我们的例子中,我们不打算对它做任何特别的事情,除了打开计数器并用它来启动一个后台线程。在真实世界的服务中,我们希望任何数据都通过 intent 传递给我们,例如,这可能包括 URIs。注意在创建线程时使用了线程组 。这将被证明是有用的,当我们想摆脱我们的背景线程。还要注意 startId 参数。这是由 Android 为我们设置的,是自该服务启动以来服务调用的唯一标识符。
我们的 ServiceWorker class 是一个典型的 runnable,是我们服务的工作发生的地方。在我们的特殊情况下,我们只是记录一些消息和睡眠。我们也会捕捉任何干扰并记录下来。我们没有做的一件事是操纵用户界面。例如,我们不会更新任何视图。因为我们不再在主线程上,所以我们不能直接接触 UI。我们的服务人员有很多方法可以改变用户界面,我们将在接下来的几章中详细介绍这些方法。
我们 BackgroundService 中最后要注意的一项是 onDestroy() 方法 。这是我们进行清理的地方。在我们的例子中,我们想要去掉我们之前创建的线程,如果有的话。如果我们不这样做,它们可能只是四处游荡,占用内存。第二,我们想摆脱我们的通知消息。因为我们的服务正在消失,用户不再需要通过活动来摆脱它。然而,在实际应用中,我们可能希望让我们的员工继续工作。如果我们的服务是发送电子邮件,我们当然不想简单地杀死线程。我们的例子过于简单,因为我们暗示通过使用 interrupt() 方法 可以很容易地杀死后台线程。然而实际上,你最多只能打断一下。不过,这不一定会杀死一个线程。有一些不推荐使用的方法来终止线程,但是您不应该使用这些方法。它们会给你和你的用户带来内存和稳定性问题。在我们的例子中,打断是有效的,因为我们在睡觉,这是可以被打断的。
看一下 ThreadGroup 类是值得的,因为它提供了访问线程的方法。我们在服务中创建了一个单独的线程组对象,然后在创建我们自己的线程时使用它。在我们的服务的 onDestroy() 方法中,我们简单地对线程组执行中断(),它向线程组中的每个线程发出一个中断。
这就是一个简单的本地服务的构成。在我们展示我们活动的代码之前,清单 14-9 展示了我们用户界面的 XML 布局文件。
清单 14-9 。实现本地服务: main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/layout/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button android:id="@+id/startBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Service" android:onClick="doClick" />
<Button android:id="@+id/stopBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop Service" android:onClick="doClick" />
</LinearLayout>
我们将在用户界面上显示两个按钮,一个执行 startService() ,另一个执行 stopService() 。我们可以选择使用一个 ToggleButton,但是这样你就不能连续多次调用 startService() 。这是很重要的一点。 startService() 和 stopService() 之间不是一一对应的关系。当调用 stopService() 时,服务对象将被销毁,所有 startService() 调用创建的所有线程也将消失。现在,让我们看看清单 14-10 中的活动代码。
清单 14-10 。实现本地服务:
public class MainActivity extends Activity
{
private static final String TAG = "MainActivity";
private int counter = 1;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.startBtn:
Log.v(TAG, "Starting service... counter = " + counter);
Intent intent = new Intent(MainActivity.this,
BackgroundService.class);
intent.putExtra("counter", counter++);
startService(intent);
break;
case R.id.stopBtn:
stopService();
}
}
private void stopService() {
Log.v(TAG, "Stopping service...");
if(stopService(new Intent(MainActivity.this,
BackgroundService.class)))
Log.v(TAG, "stopService was successful");
else
Log.v(TAG, "stopService was unsuccessful");
}
@Override
public void onDestroy()
{
stopService();
super.onDestroy();
}
}
我们的主要活动看起来很像你见过的其他活动。有一个简单的 onCreate() 来从 main.xml 布局文件设置我们的用户界面。有一个 doClick() 方法来处理按钮回调。在我们的示例中,当按下启动服务按钮时,我们调用 startService() ,当按下停止服务按钮时,我们调用 stopService() 。当我们启动服务时,我们希望传入一些数据,这是通过 intent 实现的。我们选择在 Extras 包中传递数据,但是如果我们有一个 URI,我们可以使用 setData() 来添加它。当我们停止服务时,我们检查返回结果。它通常应该是真的,但是如果服务没有运行,我们可能会得到假的返回。最后,当我们的活动终止时,我们想要停止服务,所以我们也在我们的 onDestroy() 方法中停止服务。还有一项需要讨论,那就是 AndroidManifest.xml 文件,我们在清单 14-11 中展示了它。
清单 14-11 。实现本地服务:Android manifest . XML
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.services.simplelocal"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" />
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="BackgroundService"/>
</application>
</manifest>
除了我们清单文件中常规的 <活动> 标签之外,我们现在还有一个 <服务> 标签。因为这是一个我们使用类名显式调用的本地服务,所以我们不需要在 <服务> 标签中放太多内容。所需要的只是我们服务的名称。但是关于这个清单文件还有一点需要指出。我们的服务会创建一个通知,以便用户可以返回到我们的 MainActivity ,例如,如果用户在不停止服务的情况下按下了 MainActivity 上的 Home 键。
主活动仍然存在;只是看不出来而已。返回到主活动的一种方法是单击我们的服务创建的通知。通知管理器将我们的意图传递回我们的应用,通常会导致一个新的主活动实例来处理新的意图。为了防止这种情况发生,我们在清单文件中为名为 android:launchMode 的 MainActivity 设置了一个属性,并将其设置为 singleTop 。这将有助于确保现有的不可见的 MainActivity 将被前移并显示,而不是创建另一个 MainActivity 。
当您运行这个应用时,您会看到我们的两个按钮。通过单击 Start Service 按钮,您将实例化服务并调用 onStartCommand() 。我们的代码将几条消息记录到 LogCat 中,因此您可以跟着做。继续,连续几次点击“启动服务”,甚至更快。您将看到为处理每个请求而创建的线程。您还会注意到,计数器的值被传递给每个 ServiceWorker 线程。当您按下停止服务按钮时,我们的服务将会消失,您将会看到来自我们的 MainActivity 的 stopService() 方法的日志消息,来自我们的 BackgroundService 的 onDestroy() 方法的日志消息,如果线程被中断,还可能会看到来自 ServiceWorker 线程的日志消息。
您还应该注意到服务启动时的通知消息。随着服务的运行,在我们的 MainActivity 中点击 Back 按钮,注意通知消息消失了。这意味着我们的服务也消失了。要重启我们的主活动,点击 Start Service 让服务再次运行。现在,按下主页按钮。我们的主活动从视图中消失了,但是通知仍然存在,这意味着我们的服务仍然存在。继续点击通知,您将再次看到我们的主活动。
请注意,我们的示例使用活动与服务进行交互,但是您的应用中的任何组件都可以使用该服务。这包括其他服务、活动、泛型类等等。还要注意,我们的服务不会自行停止;它依赖于活动来完成。有一些方法可供服务使用,允许服务自行停止,即 stopSelf() 和 stopSelfResult() 。显然,如果这个服务有多个客户端,我们不希望其中一个客户端停止服务,而其他客户端仍在使用它。对于一个有多个客户端的已启动服务,您更有可能在服务本身中加入逻辑来决定服务何时能够或者应该停止,并且服务将使用 stop*()方法 之一来完成这一任务。
我们的 BackgroundService 是托管服务的应用组件所使用的服务的典型例子。换句话说,运行服务的应用也是唯一的消费者。因为该服务不支持来自其进程之外的客户端,所以该服务是本地服务。本地服务的关键方法有 onCreate() 、 onStartCommand() 、onBind()、 stop*() 和 onDestroy() 。
本地服务还有另一种选择,那就是只有一个服务实例和一个后台线程。在这种情况下,在 BackgroundService 的 onCreate() 方法中,我们可以创建一个线程来完成服务的繁重工作。我们可以在 onCreate() 而不是 onStartCommand() 中创建并启动线程。我们可以这样做,因为 onCreate() 只被调用一次,并且我们希望线程在服务的生命周期中只被创建一次。然而,我们在 onCreate() 中没有的一件事是 startService() 传递的意图的内容。如果我们需要,我们也可以使用前面描述的模式,我们只需要知道 onStartCommand() 应该只被调用一次。
Android 还有另一种方法来实现自动包含后台线程的本地服务:IntentService。服务的子类 IntentService 接收来自 startService()调用的传入意图,为您创建一个后台(worker)线程,并调用回调 onHandleIntent(Intent intent)。如果在工作线程完成前一个意图之前将另一个意图传递给此服务,则新的意图将处于等待状态,直到处理完前一个意图,此时队列中的下一个意图将被传递给 onHandleIntent()方法。当来自入站队列的所有意图都完成处理后,服务将自行停止(不需要您这样做)。
我们对本地服务的介绍到此结束。请记住,我们将在后续章节中深入了解本地服务的更多细节。让我们转到 AIDL 服务——一种更复杂的服务。
了解 AIDL 服务
在上一节中,我们向您展示了如何编写一个 Android 服务,由托管该服务的应用使用。现在,我们将向您展示如何通过远程过程调用(RPC) 构建一个可以被其他流程使用的服务。与许多其他基于 RPC 的解决方案一样,在 Android 中,您需要一个接口定义语言(IDL)来定义将向客户端公开的接口。在 Android 世界,这个 IDL 被称为 Android 接口定义语言(AIDL) 。要构建远程服务,您需要执行以下操作:
- 编写一个 AIDL 文件,定义你与客户的接口。AIDL 文件使用 Java 语法,并有一个。aidl 扩展。在你的 AIDL 文件中使用和你的 Android 项目相同的包名。
- 将 AIDL 文件添加到 Eclipse 项目的 src 目录下。Android Eclipse 插件将调用 AIDL 编译器从 AIDL 文件生成 Java 接口(AIDL 编译器作为构建过程的一部分被调用)。
- 实现一个服务,从 onBind() 方法返回接口。
- 将服务配置添加到您的 AndroidManifest.xml 文件中。接下来的部分向您展示了如何执行每个步骤。
在 AIDL 定义服务接口
为了演示一个远程服务的例子,我们将编写一个股票报价服务。该服务将提供一个方法,该方法获取股票代码并返回股票价值。要在 Android 中编写远程服务,第一步是在 AIDL 文件中定义服务接口定义。清单 14-12 显示了 IStockQuoteService 的 AIDL 定义。对于您的 StockQuoteService 项目,该文件与常规 Java 文件放在同一个位置。
清单 14-12 。AIDL 定义的 服务
// This file is IStockQuoteService.aidl
package com.androidbook.services.stockquoteservice;
interface IStockQuoteService
{
double getQuote(String ticker);
}
IStockQuoteService 以字符串形式接受股票代码,并以双精度形式返回当前股票价值。当您创建 AIDL 文件时,Android Eclipse 插件运行 AIDL 编译器来处理您的 AIDL 文件(作为构建过程的一部分)。如果您的 AIDL 文件编译成功,编译器会生成一个适合 RPC 通信的 Java 接口。注意,生成的文件将位于您的 AIDL 文件中命名的包中,在本例中为 com . androidbook . services . stock quote service。
清单 14-13 显示了为我们的 IStockQuoteService 接口生成的 Java 文件。生成的文件将被放到我们的 Eclipse 项目的 gen 文件夹中。
清单 14-13 。编译器生成的 Java 文件
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: C:\\android\\StockQuoteService\\src\\com\\androidbook\\
services\\stockquoteservice\\IStockQuoteService.aidl
*/
package com.androidbook.services.stockquoteservice;
import java.lang.String;
import android.os.RemoteException;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Binder;
import android.os.Parcel;
public interface IStockQuoteService extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements
com.androidbook.services.stockquoteservice.IStockQuoteService
{
private static final java.lang.String DESCRIPTOR = 
"com.androidbook.services.stockquoteservice.IStockQuoteService";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an IStockQuoteService interface,
* generating a proxy if needed.
*/
public static com.androidbook.services.stockquoteservice.IStockQuoteService
asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.androidbook.services.stockquoteservice.IStockQuoteService))) {
return ((com.androidbook.services.stockquoteservice.IStockQuoteService)iin);
}
return new com.androidbook.services.stockquoteservice.IStockQuoteService.Stub.Proxy(obj);
}
public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getQuote:
{
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
double _result = this.getQuote(_arg0);
reply.writeNoException();
reply.writeDouble(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements
com.androidbook.services.stockquoteservice.IStockQuoteService
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
public double getQuote(java.lang.String ticker) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
double _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(ticker);
mRemote.transact(Stub.TRANSACTION_getQuote, _data, _reply, 0);
_reply.readException();
_result = _reply.readDouble();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_getQuote = (IBinder.FIRST_CALL_TRANSACTION + 0);
}
public double getQuote(java.lang.String ticker) throws android.os.RemoteException;
}
关于生成的类,请注意以下要点:
- 我们在 AIDL 文件中定义的接口在生成的代码中实现为一个接口(即有一个名为 IStockQuoteService 的接口)。
- 一个名为存根的静态抽象类扩展了 android.os.Binder 并实现了 IStockQuoteService 。请注意,该类是一个抽象类。
- 名为 Proxy 的内部类实现了代理存根类的 IStockQuoteService 。
- AIDL 文件必须位于生成的文件所在的包中(如 AIDL 文件的包声明中所指定的)。
现在,让我们继续,在服务类中实现 AIDL 接口。
实现 AIDL 接口
在上一节中,我们为股票报价机服务定义了一个 AIDL 文件,并生成了绑定文件。现在,我们将提供该服务的实现。为了实现服务的接口,我们需要编写一个类来扩展 android.app.Service 并实现 IStockQuoteService 接口。我们要写的类我们称之为股票报价服务。为了向客户端公开服务,我们的 StockQuoteService 将需要提供 onBind() 方法的实现,并且我们需要向 AndroidManifest.xml 文件添加一些配置信息。清单 14-14 显示了 IStockQuoteService 接口的实现。该文件也放入 StockQuoteService 项目的 src 文件夹中。
清单 14-14 。IStockQuoteService**服务 实现
public class StockQuoteService extends Service
{
private static final String TAG = "StockQuoteService";
public class StockQuoteServiceImpl extends IStockQuoteService.Stub
{
@Override
public double getQuote(String ticker) throws RemoteException
{
Log.v(TAG, "getQuote() called for " + ticker);
return 20.0;
}
}
@Override
public void onCreate() {
super.onCreate();
Log.v(TAG, "onCreate() called");
}
@Override
public void onDestroy()
{
super.onDestroy();
Log.v(TAG, "onDestroy() called");
}
@Override
public IBinder onBind(Intent intent)
{
Log.v(TAG, "onBind() called");
return new StockQuoteServiceImpl();
}
}
清单 14-14 中的 StockQuoteService.java 类类似于我们之前创建的本地后台服务,但是没有通知管理器。重要的区别是我们现在实现了 onBind() 方法。回想一下,从 AIDL 文件生成的存根类是一个抽象类,它实现了 IStockQuoteService 接口。在我们的服务实现中,我们有一个内部类,它扩展了名为 StockQuoteServiceImpl 的存根类。这个类充当远程服务实现,这个类的一个实例从 onBind() 方法返回。这样,我们就有了一个功能性的 AIDL 服务,尽管外部客户端还不能连接到它。
为了向客户端公开服务,我们需要在 AndroidManifest.xml 文件中添加一个服务声明,这一次,我们需要一个意图过滤器来公开服务。清单 14-15 显示了股票报价服务的服务声明。 <服务> 标签是 <应用> 标签的子标签。
清单 14-15 。清单声明 为 IStockQuoteService
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.services.stockquoteservice"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<service android:name="StockQuoteService">
<intent-filter>
<action android:name=
"com.androidbook.services.stockquoteservice.IStockQuoteService" />
</intent-filter>
</service>
</application>
<uses-sdk android:minSdkVersion="4" />
</manifest>
与所有服务一样,我们用一个 <服务> 标签定义我们想要公开的服务。对于一个 AIDL 服务,我们还需要为我们想要公开的服务接口添加一个带有 <动作> 条目的<>。
有了这些,我们就拥有了部署服务所需的一切。当您准备好从 Eclipse 部署服务应用时,只需像对任何其他应用一样选择 Run。Eclipse 将在控制台中注释这个应用没有启动程序,但它无论如何都会部署这个应用,这就是我们想要的。现在让我们看看如何从另一个应用调用服务(当然是在同一个设备上)。
从客户端应用调用服务
当客户端与服务对话时,两者之间必须有一个协议或契约。对于 Android,合同在我们的 AIDL 文件中。因此,使用服务的第一步是获取服务的 AIDL 文件,并将其复制到您的客户端项目中。当您将 AIDL 文件复制到客户端项目时,AIDL 编译器会创建与服务实现时创建的相同的接口定义文件(在服务实现项目中)。这向客户端公开了服务上的所有方法、参数和返回类型。让我们创建一个新项目并复制 AIDL 文件:
- 创建一个新的 Android 项目,命名为 StockQuoteClient 。使用不同的包名,比如 com . androidbook . stock quote client。将主活动用于创建活动字段。
- 在这个项目中,在 src 目录下创建一个名为 com . androidbook . services . stock quote service 的新 Java 包。
- 将 StockQuoteService 项目中的 IStockQuoteService.aidl 文件复制到这个新包中。请注意,在您将文件复制到项目中之后,AIDL 编译器将会生成相关的 Java 文件。
您重新生成的服务接口充当客户端和服务之间的契约。下一步是获取对服务的引用,这样我们就可以调用 getQuote() 方法。对于远程服务,我们必须调用 bindService() 方法,而不是 startService() 方法。清单 14-16 显示了一个活动类,它作为 IStockQuoteService 服务的客户端。清单 14-17 包含了活动的布局文件。
清单 14-16 显示了我们的【MainActivity.java】文件的文件。认识到客户端活动的包名并不重要——您可以将活动放在任何您喜欢的包中。然而,您创建的 AIDL 工件是包敏感的,因为 AIDL 编译器从 AIDL 文件的内容生成代码。
清单 14-16 。istock quote Service 服务的客户端
public class MainActivity extends Activity {
private static final String TAG = "StockQuoteClient";
private IStockQuoteService stockService = null;
private ToggleButton bindBtn;
private Button callBtn;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
bindBtn = (ToggleButton)findViewById(R.id.bindBtn);
callBtn = (Button)findViewById(R.id.callBtn);
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.bindBtn:
if(((ToggleButton) view).isChecked()) {
bindService(new Intent(
IStockQuoteService.class.getName()),
serConn, Context.BIND_AUTO_CREATE);
}
else {
unbindService(serConn);
callBtn.setEnabled(false);
}
break;
case R.id.callBtn:
callService();
break;
}
}
private void callService() {
try {
double val = stockService.getQuote("ANDROID");
Toast.makeText(MainActivity.this,
"Value from service is " + val,
Toast.LENGTH_SHORT).show();
} catch (RemoteException ee) {
Log.e("MainActivity", ee.getMessage(), ee);
}
}
private ServiceConnection serConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name,
IBinder service)
{
Log.v(TAG, "onServiceConnected() called");
stockService = IStockQuoteService.Stub.asInterface(service);
bindBtn.setChecked(true);
callBtn.setEnabled(true);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "onServiceDisconnected() called");
bindBtn.setChecked(false);
callBtn.setEnabled(false);
stockService = null;
}
};
protected void onDestroy() {
Log.v(TAG, "onDestroy() called");
if(callBtn.isEnabled())
unbindService(serConn);
super.onDestroy();
}
}
该活动显示了我们的布局并获取了对 Call Service 按钮的引用,因此我们可以在服务运行时正确地启用它,并在服务停止时禁用它。当用户点击 Bind 按钮时,活动调用 bindService() 方法 。同样,当用户点击 UnBind 时,活动调用 unbindService() 方法 。注意,有三个参数被传递给了 bindService() 方法:一个带有 AIDL 服务名称的 Intent ,一个 ServiceConnection 实例,以及一个自动创建服务的标志。
清单 14-17 。istock quote Service 服务客户端布局
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/layout/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ToggleButton android:id="@+id/bindBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOff="Bind" android:textOn="Unbind"
android:onClick="doClick" />
<Button android:id="@+id/callBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Call Service" android:enabled="false"
android:onClick="doClick" />
</LinearLayout>
对于绑定服务,比如 AIDL 服务,您需要提供一个 ServiceConnection 接口的实现。这个接口定义了两个方法:一个由系统在建立到服务的连接时调用,另一个在到服务的连接被破坏时调用。在我们的活动实现中,我们为 IStockQuoteService 定义了 ServiceConnection 。当我们调用 bindService() 方法时,我们传入对此方法的引用(即 serConn )。当建立到服务的连接时,调用 onServiceConnected() 回调,然后我们使用存根获取对 IStockQuoteService 的引用,并启用调用服务按钮。
注意, bindService() 调用是一个异步调用。它是异步的,因为流程或服务可能没有运行,因此可能必须创建或启动。我们不能在主线程上等待服务启动。因为 bindService() 是异步的,平台提供了 ServiceConnection 回调,所以我们知道什么时候服务已经启动,什么时候服务不再可用。这些 ServiceConnection 回调将在主线程上运行,所以如果需要的话,它们可以访问 UI 组件。
请注意 onServiceDisconnected() 回调。当我们从服务中解除绑定时,这个不会被调用。如果服务崩溃或者 Android 决定终止服务,例如内存不足,就会调用这个函数。如果这个回调触发,我们不应该认为我们仍然是连接的,我们可能需要重新调用 bindService() 调用。这就是为什么当这个回调被调用时,我们要改变 UI 中按钮的状态。但是请注意,我们说过“我们可能需要重新调用 bindService() 调用。”Android 可以为我们重启服务,并调用我们的 onServiceConnected() 回调。您可以自己尝试一下,运行客户端,绑定到服务,并使用 DDMS 在股票报价服务应用上停留一下。
当您运行这个示例时,请观察 LogCat 中的日志消息,了解一下幕后发生了什么。
到目前为止,在我们的服务示例中,我们已经严格处理了简单 Java 原语类型的传递。Android 服务实际上也支持传递复杂类型。这非常有用,尤其是对于 AIDL 服务,因为您可能需要向服务传递不限数量的参数,而将它们都作为简单的原语传递是不合理的。更有意义的做法是将它们打包成复杂类型,然后传递给服务。
让我们看看如何将复杂类型传递给服务。
将复杂类型传递给服务
与传递 Java 基本类型相比,在服务之间传递复杂类型需要更多的工作。在开始这项工作之前,您应该了解一下 AIDL 对非原始类型的支持:
- AIDL 支持串和串。
- AIDL 允许您传递其他 AIDL 接口,但是您需要为您引用的每个 AIDL 接口拥有一个 import 语句(即使被引用的 AIDL 接口在同一个包中)。
- AIDL 允许你传递实现 Android . OS . parcelable 接口的复杂类型。对于这些类型,在您的 AIDL 文件中需要有一个 import 语句。
- AIDL 支持 java.util.List 和 java.util.Map ,有一些限制。集合中项目允许的数据类型包括 Java 原语、字符串、 CharSequence 和 android.os.Parcelable 。对于列表或映射,您不需要导入语句,但是对于 Parcelable s,您需要它们。
- 除了字符串之外的非基本类型需要一个方向指示器。方向指示灯包括 in 、 out 和 inout。in 表示值由客户端设置, out 表示值由服务设置, inout 表示值由客户端和服务共同设置。如果值没有按照指示的方向流动,Android 会避免序列化这些值,这有助于整体性能。
Parcelable 接口告诉 Android 运行时如何在编组和解组过程中序列化和反序列化对象。清单 14-18 显示了一个 Person 类,它实现了 Parcelable 接口。
清单 14-18 。实现**Parcelable 接口
// This file is Person.java
package com.androidbook.services.stock2;
import android.os.Parcel;
import android.os.Parcelable;
public class Person implements Parcelable {
private int age;
private String name;
public static final Parcelable.Creator<Person> CREATOR =
new Parcelable.Creator<Person>()
{
public Person createFromParcel(Parcel in) {
return new Person(in);
}
public Person[] newArray(int size) {
return new Person[size];
}
};
public Person() {
}
private Person(Parcel in) {
readFromParcel(in);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(age);
out.writeString(name);
}
public void readFromParcel(Parcel in) {
age = in.readInt();
name = in.readString();
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
要开始实现这个,在 Eclipse 中创建一个名为 StockQuoteService2 的新 Android 项目。对于创建活动,使用名称 MainActivity ,使用包 com . androidbook . services . stock 2。然后将清单 14-18 中的 Person.java 文件添加到我们新项目的 com . androidbook . services . stock 2 包中。
Parcelable 接口定义了对象编组和解组的契约。在 Parcelable 接口的底层是包裹容器对象。package 类是一种快速序列化/反序列化机制,专门为 Android 中的进程间通信而设计。类提供了一些方法,您可以使用这些方法将成员展开到容器中,以及将成员从容器中展开回来。为了正确地实现进程间通信的对象,我们必须执行以下操作:
- 实现 Parcelable 接口。这意味着您实现了 writeToParcel() 和 readfrompacelle()。write 方法将把对象写到包裹中,read 方法将从包裹中读取对象。请注意,写入属性的顺序必须与读取属性的顺序相同。
- 向名为创建者的类添加一个静态最终属性。属性需要实现 Android . OS . parcelable . creator接口。
- 为包提供一个构造函数,它知道如何从包中创建对象。
- 在中定义一个可打包的类。与匹配的 aidl 文件。包含复杂类型的 java 文件。AIDL 编译器在编译你的 AIDL 文件时会寻找这个文件。一个 Person.aidl 文件的例子如清单 14-19 所示。这个文件应该和 Person.java 在同一个地方。
注意看到 Parcelable 可能会引发一个问题,为什么 Android 不使用内置的 Java 序列化机制?事实证明,Android 团队得出的结论是,Java 中的序列化速度太慢,无法满足 Android 的进程间通信需求。因此,团队构建了可打包解决方案。 Parcelable 方法要求你显式地序列化你的类的成员,但是最终,你得到了一个更快的对象序列化。
还要认识到,Android 提供了两种机制,允许您将数据传递给另一个进程。第一个是使用 intent 将包传递给活动,第二个是将 Parcelable 传递给服务。这两种机制不可互换,也不应混淆。也就是说,可打包的不应该被传递给活动。如果您想启动一个活动并向其传递一些数据,请使用包。Parcelable 仅用于 AIDL 定义的一部分。
清单 14-19 。一个person . aidl文件 的例子
// This file is Person.aidl
package com.androidbook.services.stock2;
parcelable Person;
你将需要一个。项目中每个包的 aidl 文件。在这种情况下,我们只有一个包装,这就是人。您可能会注意到,您没有在 gen 文件夹中创建一个 Person.java 文件。这是可以预料的。我们在之前创建该文件时就已经有了它。
现在,让我们在远程服务中使用 Person 类。为了简单起见,我们将修改我们的 IStockQuoteService 来接受一个 Person 类型的输入参数。这个想法是,客户将传递一个人给服务,告诉服务谁在请求报价。新的 istockquoteservice . aidl 看起来像清单 14-20 中的。
清单 14-20 。将包裹传递给服务
// This file is IStockQuoteService.aidl
package com.androidbook.services.stock2;
import com.androidbook.services.stock2.Person;
interface IStockQuoteService
{
String getQuote(in String ticker,in Person requester);
}
getQuote() 方法现在接受两个参数:股票的股票代码和一个 Person 对象来指定是谁发出的请求。注意,我们在参数上有方向指示器,因为参数包括非主类型,并且我们有一个用于 Person 类的 import 语句。 Person 类也和服务定义(com . androidbook . services . stock 2)在同一个包中。
服务实现现在看起来像清单 14-21 中的,主要活动布局在清单 14-22 中的中。
清单 14-21 。stockquoteservice 2实现**
package com.androidbook.services.stock2;
// This file is StockQuoteService2.java
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class StockQuoteService2 extends Service
{
private NotificationManager notificationMgr;
public class StockQuoteServiceImpl extends IStockQuoteService.Stub
{
public String getQuote(String ticker, Person requester)
throws RemoteException {
return "Hello " + requester.getName() +
"! Quote for " + ticker + " is 20.0";
}
}
@Override
public void onCreate() {
super.onCreate();
notificationMgr =
(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
displayNotificationMessage(
"onCreate() called in StockQuoteService2");
}
@Override
public void onDestroy()
{
displayNotificationMessage(
"onDestroy() called in StockQuoteService2");
// Clear all notifications from this service
notificationMgr.cancelAll();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent)
{
displayNotificationMessage(
"onBind() called in StockQuoteService2");
return new StockQuoteServiceImpl();
}
private void displayNotificationMessage(String message)
{
PendingIntent contentIntent =
PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("StockQuoteService2")
.setContentText(message)
.setSmallIcon(R.drawable.emo_im_happy)
.setTicker(message)
// .setLargeIcon(aBitmap)
.setContentIntent(contentIntent)
.setOngoing(true)
.build();
notificationMgr.notify(R.id.app_notification_id, notification);
}
}
清单 14-22 。stock quote service2布局**
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/layout/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="This is where the service could ask for help." />
</LinearLayout>
这个实现与前一个实现的不同之处在于,我们带回了通知,现在我们以字符串而不是双精度值的形式返回股票值。返回给用户的字符串包含来自 Person 对象的请求者的名字,这表明我们读取了从客户端发送的值,并且 Person 对象被正确地传递给了服务。
要实现这一点,还需要做一些其他的事情:
- 从 AndroidSDK/platforms/Android-19/data/RES/drawable-mdpi 下找到 emo_im_happy.png 镜像文件,复制到我们项目的 /res/drawable 目录下。或者在代码中更改资源的名称,然后将您想要的任何图像放入 drawables 文件夹中。
- 在 /res/values/strings.xml 文件中添加一个新的 <项目 type = " id " name = " app _ notification _ id "/>标签。
- 我们需要修改 AndroidManifest.xml 文件中的应用,如清单 14-23 所示。
清单 14-23 。修改<应用>中的androidmanifest . XML文件 为 StockQuoteService2
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.services.stock2"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" />
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<service android:name="StockQuoteService2">
<intent-filter>
<action android:name="com.androidbook.services.stock2.IStockQuoteService" />
</intent-filter>
</service>
</application>
</manifest>
虽然我们的 android:name= " "可以使用点符号。MainActivity" 属性,在服务的 < intent-filter > 标签内的 < action > 标签内不能使用点符号。我们需要把它拼出来;否则,我们的客户将找不到服务规范。
最后,我们将使用默认的 MainActivity.java 文件,该文件简单地显示了一个带有简单消息的基本布局。我们之前向您展示了如何从通知启动活动。这个活动在现实生活中也可以达到这个目的,但是在这个例子中,我们将保持这个部分简单。现在我们已经有了服务实现,让我们创建一个名为 StockQuoteClient2 的新 Android 项目。使用 com.dave 作为包,使用 MainActivity 作为活动名称。要实现将 Person 对象传递给服务的客户机,我们需要将客户机需要的所有东西从服务项目复制到客户机项目。需要一个名为 com . Android book . services . stock 2 的新 src 包来接收这些复制的文件。在我们之前的例子中,我们所需要的就是文件 istockquoteservice . aidl。我们还需要复制 Person.java 和 Person.aidl 文件,因为 Person 对象现在是接口的一部分。将这三个文件复制到客户端项目的 com . androidbook . services . stock 2 src 包后,根据清单 14-24 修改 main.xml ,根据清单 14-25 修改 MainActivity.java。或者简单地从我们网站上的源代码导入这个项目。
清单 14-24 。更新main . XML为 StockQuoteClient2
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/layout/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ToggleButton android:id="@+id/bindBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOff="Bind" android:textOn="Unbind"
android:onClick="doClick" />
<Button android:id="@+id/callBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Call Service" android:enabled="false"
android:onClick="doClick" />
</LinearLayout>
清单 14-25 。调用 服务 与
package com.dave;
// This file is MainActivity.java
import com.androidbook.services.stock2.IStockQuoteService;
import com.androidbook.services.stock2.Person;
public class MainActivity extends Activity {
protected static final String TAG = "StockQuoteClient2";
private IStockQuoteService stockService = null;
private ToggleButton bindBtn;
private Button callBtn;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
bindBtn = (ToggleButton)findViewById(R.id.bindBtn);
callBtn = (Button)findViewById(R.id.callBtn);
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.bindBtn:
if(((ToggleButton) view).isChecked()) {
bindService(new Intent(
IStockQuoteService.class.getName()),
serConn, Context.BIND_AUTO_CREATE);
}
else {
unbindService(serConn);
callBtn.setEnabled(false);
}
break;
case R.id.callBtn:
callService();
break;
}
}
private void callService() {
try {
Person person = new Person();
person.setAge(47);
person.setName("Dave");
String response = stockService.getQuote("ANDROID", person);
Toast.makeText(MainActivity.this,
"Value from service is "+response,
Toast.LENGTH_SHORT).show();
} catch (RemoteException ee) {
Log.e("MainActivity", ee.getMessage(), ee);
}
}
private ServiceConnection serConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name,
IBinder service)
{
Log.v(TAG, "onServiceConnected() called");
stockService = IStockQuoteService.Stub.asInterface(service);
bindBtn.setChecked(true);
callBtn.setEnabled(true);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "onServiceDisconnected() called");
bindBtn.setChecked(false);
callBtn.setEnabled(false);
stockService = null;
}
};
protected void onDestroy() {
if(callBtn.isEnabled())
unbindService(serConn);
super.onDestroy();
}
}
现在可以运行了。记住,在发送客户端运行之前,要将服务发送到设备或模拟器。用户界面应该看起来像图 14-1 。
图 14-1 。股票行情 2 用户界面
让我们看看我们有什么。像以前一样,我们绑定到我们的服务,然后我们可以调用服务方法。在 onServiceConnected() 方法中,我们被告知我们的服务正在运行,因此我们可以启用 Call Service 按钮,这样该按钮就可以调用 callService() 方法。如图所示,我们创建一个新的人物对象,并设置其年龄和姓名属性。然后,我们执行服务并显示服务调用的结果。结果看起来像图 14-2 。
图 14-2 。用包调用服务的结果
请注意,当调用该服务时,您会在状态栏中收到一个通知。这是来自服务本身。我们在前面简要地谈到了通知是服务与用户交流的一种方式。通常,服务在后台,不显示任何类型的 UI。但是如果一个服务需要与用户交互呢?虽然很容易认为服务可以调用活动,但是服务应该永远不要直接调用活动。相反,服务应该创建一个通知,通知应该是用户如何获得所需的活动。这在我们上次的练习中已经展示过了。我们为服务定义了一个简单的布局和活动实现。当我们在服务中创建通知时,我们在通知中设置活动。用户可以触摸通知,它会将用户带到我们的活动,这是该服务的一部分。这将允许用户与服务进行交互。
通知被保存,以便您可以通过从状态栏下拉来查看它们。请注意,我们对每条消息都重复使用相同的 ID。这意味着我们每次都更新唯一的通知,而不是创建新的通知条目。所以如果你在 Android 中点击绑定后进入通知屏幕,再次调用,几次解除绑定,你只会在通知中看到一条消息,而且会是 StockQuoteService2 发送的最后一条。如果我们使用不同的 id,我们可以有多个通知消息,我们可以分别更新每一个。通知还可以设置附加的用户“提示”,如声音、灯光和/或振动。
查看服务项目和调用它的客户端的工件也是有用的(参见图 14-3 )。
图 14-3 。服务和客户端的工件
图 14-3 显示了服务(左)和客户端(右)的 Eclipse 项目工件。注意,客户机和服务之间的契约由双方交换的 AIDL 工件和可打包的对象组成。这就是我们在两边看到【Person.java】、 IStockQuoteService.aidl 、 Person.aidl 的原因。因为 AIDL 编译器从 AIDL 工件生成 Java 接口、存根、代理等等,所以当我们将契约工件复制到客户端项目时,构建过程会在客户端创建 IStockQuoteService.java 文件。
现在您知道了如何在服务和客户端之间交换复杂类型。让我们简单地谈谈调用服务的另一个重要方面:同步和异步服务调用。
您对服务进行的所有调用都是同步的。这就带来了一个明显的问题:您需要在一个工作线程中实现所有的服务调用吗?不一定。在大多数其他平台上,客户端使用完全黑盒子的服务是很常见的,因此客户端在进行服务调用时必须采取适当的预防措施。使用 Android,您可能会知道服务中有什么(通常是因为您自己编写了服务),因此您可以做出明智的决定。如果您知道您正在调用的方法正在做大量繁重的工作,那么您应该考虑使用一个辅助线程来进行调用。如果您确定该方法没有任何瓶颈,就可以安全地在 UI 线程上进行调用。如果您认为最好在工作线程中进行服务调用,那么您可以创建线程,然后调用服务。然后,您可以将结果传递给 UI 线程。
信使和处理者
在 Android 中,还有一种与服务通信的方式,那就是使用信使和处理器。这种机制是建立在 AIDL 服务之上的,但你不必看到或处理 AIDL。与 AIDL 服务一样,当服务在独立于客户端的进程中时,可以使用它。客户端和服务都将实现 Messenger 和 Handler,并继续来回发送消息。您不需要指定任何。aidl 文件;所有东西都编码在 Java 类中。这是在 Android 上进行进程间服务调用的一种相当常见的方式,并且比自己处理 AIDL 要容易得多。
这里有一个它如何工作的快速概述。客户端绑定到服务,并设置一个信使和处理器来接收来自服务的响应。处理器中的回调处理服务发回的消息。客户端还创建一个 Messenger 来向服务发送消息。在服务端,有一个类似的 Messenger 和处理器来接收来自客户端的传入消息。来自客户端的消息包括用于回复该客户端的信使。因此,服务只创建一个 Messenger,而客户端创建两个。客户端是异步的,服务响应在后面。服务调用的问题会生成一个 RemoteException,客户端可以捕获并处理它。
让我们来看一个例子。这个示例应用有两个部分:MessengerClient 和 MessengerService。它们将在设备上作为独立的进程运行。客户端将使用非 UI 片段来包含服务客户端连接。这意味着客户端活动可能会由于配置更改而消失并被重新创建,而底层服务连接仍然存在。这是从活动连接到服务的首选方式,因为您不希望仅仅因为设备发生了旋转就必须重新构建服务客户端连接。清单 14-26 显示了来自 MessengerService.java 的设置处理器和信使的重要代码。有关完整的列表,请参考本章的 MessengerService 源项目。
清单 14-26 。基于的 服务代码
public class MessengerService extends Service {
NotificationManager mNM;
ArrayList<Messenger> mClients = new ArrayList<Messenger>();
int mValue = 0;
public static final int MSG_REGISTER_CLIENT = 1;
public static final int MSG_UNREGISTER_CLIENT = 2;
public static final int MSG_SET_SIMPLE_VALUE = 3;
public static final int MSG_SET_COMPLEX_VALUE = 4;
public static final String TAG = "MessengerService";
/**
* Handler of incoming messages from clients.
*/
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REGISTER_CLIENT:
mClients.add(msg.replyTo);
Log.v(TAG, "Registering client");
break;
case MSG_UNREGISTER_CLIENT:
mClients.remove(msg.replyTo);
Log.v(TAG, "Unregistering client");
break;
case MSG_SET_SIMPLE_VALUE:
mValue = msg.arg1;
Log.v(TAG, "Receiving arg1: " + mValue);
showNotification("Received arg1: " + mValue);
for (int i=mClients.size()-1; i>=0; i--) {
try {
mClients.get(i).send(Message.obtain(null,
MSG_SET_SIMPLE_VALUE, mValue, 0));
} catch (RemoteException e) {
// The client is dead. Remove it from the list;
// we are going through the list from back to front
// so this is safe to do inside the loop.
mClients.remove(i);
}
}
break;
case MSG_SET_COMPLEX_VALUE:
Bundle mBundle = msg.getData();
Log.v(TAG, "Receiving bundle: ");
if(mBundle != null) {
showNotification("Got complex msg: myDouble = "
+ mBundle.getDouble("myDouble"));
for(String key : mBundle.keySet()) {
Log.v(TAG, " " + key);
}
}
break;
default:
Log.v(TAG, "Got some other message: " + msg.what);
super.handleMessage(msg);
}
}
}
// Target for clients to send messages to IncomingHandler.
final Messenger mMessenger = new Messenger(new IncomingHandler());
@Override
public void onCreate() {
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
// Display a notification about us starting.
Log.v(TAG, "Service is starting");
showNotification(getText(R.string.remote_service_started));
}
@Override
public void onDestroy() {
// Cancel the persistent notification.
mNM.cancel(R.string.remote_service_started);
// Tell the user we stopped.
Toast.makeText(this, R.string.remote_service_stopped, Toast.LENGTH_SHORT).show();
}
/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
/**
* Show a notification while this service is running. Note that
* we don't include an intent since we're just a service here. The
* service stops when the client tells it to.
*/
private void showNotification(CharSequence text) {
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("MessengerService")
.setContentText(text)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setTicker(text)
.setOngoing(true)
.build();
mNM.notify(R.string.remote_service_started, notification);
}
}
在此示例中,客户端向服务注册、向服务注销、发送简单消息或发送复杂消息。当客户端注册时,服务通过将传入的客户端 Messenger (即 msg.replyTo )保存在 mClients 中来记住它。如果收到一条简单的消息,服务会将收到的参数值的副本发送给所有已知的客户端。注意如何使用来自每个客户端的 mClients 中的 Messenger s 向每个客户端发送回复。消息的 what 字段只是一个 int,用来指示正在调用什么服务操作。基于 what 操作,服务将提取适当的参数。因为一个消息对象有两个可用的 int 参数,所以简单的例子只使用其中一个消息字段。当必须发送更复杂的数据时,会创建、填充 Bundle 对象,并将其附加到消息中,以便传输到服务。
请注意,对于所有正在进行的服务调用,服务有 1MB 的缓冲区用于传递数据(传入和传出),因此您希望将消息数据保持在最低限度。如果有许多并发的服务调用,您可能会超出缓冲区并获得 TransactionTooLargeException。
在客户端,有一个 MainActivity 和一个 ClientFrag(非 UI 片段)。为了简单起见,活动向用户提供 UI,而不使用 UI 片段。清单 14-27 显示了主活动。有关该项目的完整列表,请参见本章的 MessengerClient 项目。
清单 14-27 。基于信使/处理器的 客户端活动代码
public class MainActivity extends FragmentActivity implements ISampleServiceClient {
protected static final String TAG = "MessengerClient";
private TextView mCallbackText;
private ClientFrag clientFrag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCallbackText = (TextView)findViewById(R.id.callback);
// Get a non-UI fragment to handle the service interface.
// If our activity gets destroyed and recreated, the fragment
// will still be around and we just need to re-fetch it.
if((clientFrag = (ClientFrag) getSupportFragmentManager()
.findFragmentByTag("clientFrag")) == null) {
updateStatus("Creating a clientFrag. No service yet.");
clientFrag = ClientFrag.getInstance();
getSupportFragmentManager().beginTransaction()
.add(clientFrag, "clientFrag")
.commit();
}
else {
updateStatus("Found existing clientFrag, will use it");
}
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.startBtn:
clientFrag.doBindService();
break;
case R.id.stopBtn:
clientFrag.doUnbindService();
break;
case R.id.simpleBtn:
clientFrag.doSendSimple();
break;
case R.id.complexBtn:
clientFrag.doSendComplex();
break;
}
}
@Override
public void updateStatus(String status) {
mCallbackText.setText(status);
}
}
注意这个活动中没有提到服务,只有一个文本视图、按钮和一个客户端片段。updateStatus()方法是此活动实现的 ISampleServiceClient 接口的结果,它所要做的就是将 UI 中的文本设置为传入的文本。按钮只是调用客户端片段的一个方法。在实际的应用中,在这个活动中或者在与服务调用分离的其他片段中会有更多的业务和 UI 逻辑。
客户端片段是有趣的地方。清单 14-28 显示了来自客户端片段的代码。
列表 14-28 。基于信使/处理器的 客户端片段代码
public class ClientFrag extends Fragment {
private static final String TAG = "MessengerClientFrag";
static private ClientFrag mClientFrag = null;
// application context will be used to bind to the service because
// fragments can't bind and activities can go away.
private Context appContext = null;
// Messenger for sending to service.
Messenger mService = null;
// Flag indicating whether we have called bind on the service.
boolean mIsBound;
// Instantiation method for the client fragment. We just want one
// and we use setRetainInstance(true) so it hangs around during
// configuration changes.
public static ClientFrag getInstance() {
if(mClientFrag == null) {
mClientFrag = new ClientFrag();
mClientFrag.setRetainInstance(true);
}
return mClientFrag;
}
// Handler for response messages from the service
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MessengerService.MSG_SET_SIMPLE_VALUE:
updateStatus("Received from service: " + msg.arg1);
break;
default:
break;
}
super.handleMessage(msg);
}
}
// Need a Messenger to receive responses. Send this with the
// Messages to the service.
final Messenger mMessenger = new Messenger(new IncomingHandler());
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className,
IBinder service) {
// This is called when the connection with the service has been
// established, giving us the service object we can use to
// interact with the service. We are communicating with our
// service through a Messenger, so get a client-side
// representation of that from the raw service object.
mService = new Messenger(service);
updateStatus("Attached.");
// We want to monitor the service for as long as we are
// connected to it. This is not strictly necessary. You
// do not need to register with the service before using
// it. But if this failed you'd have an early warning.
try {
Message msg = Message.obtain(null,
MessengerService.MSG_REGISTER_CLIENT);
msg.replyTo = mMessenger;
mService.send(msg);
} catch (RemoteException e) {
// In this case the service has crashed before we could even
// do anything with it; we can count on soon being
// disconnected (and then reconnected if it can be restarted)
// so there is no need to do anything here.
Log.e(TAG, "Could not establish a connection to the service: " + e);
}
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null;
updateStatus("Disconnected.");
}
};
public void doBindService() {
// Establish a connection with the service. We use the String name
// of the service since it exists in a separate process and we do
// not want to require the service jar in the client. We also grab
// the application context and bind the service to that since the
// activity context could go away on a configuration change but the
// application context will always be there.
appContext = getActivity().getApplicationContext();
if(mIsBound = appContext.bindService(
new Intent("com.androidbook.messengerservice.MessengerService"),
mConnection, Context.BIND_AUTO_CREATE)
) {
updateStatus("Bound to service.");
}
else {
updateStatus("Bind attempt failed.");
}
}
public void doUnbindService() {
if (mIsBound) {
// If we have received the service, and hence registered with
// it, then now is the time to unregister. Note that the
// replyTo value is only used by the service to unregister
// this client. No response message will come back to the client.
if (mService != null) {
try {
Message msg = Message.obtain(null,
MessengerService.MSG_UNREGISTER_CLIENT);
msg.replyTo = mMessenger;
mService.send(msg);
} catch (RemoteException e) {
// There is nothing special we need to do if the service
// has crashed.
}
}
// Detach our existing connection.
appContext.unbindService(mConnection);
mIsBound = false;
updateStatus("Unbound.");
}
}
// If you can simplify and send only one or two integers, this
// is the easy way to do it.
public void doSendSimple() {
try {
Message msg = Message.obtain(null,
MessengerService.MSG_SET_SIMPLE_VALUE, this.hashCode(), 0);
mService.send(msg);
updateStatus("Sending simple message.");
} catch (RemoteException e) {
Log.e(TAG, "Could not send a simple message to the service: " + e);
}
}
// If you have more complex data, throw it into a Bundle and
// add it to the Message. Can also pass Parcelables if you like.
public void doSendComplex() {
try {
Message msg = Message.obtain(null,
MessengerService.MSG_SET_COMPLEX_VALUE);
Bundle mBundle = new Bundle();
mBundle.putString("stringArg", "This is a string to pass");
mBundle.putDouble("myDouble", 1138L);
mBundle.putInt("myInt", 42);
msg.setData(mBundle);
mService.send(msg);
updateStatus("Sending complex message.");
} catch (RemoteException e) {
Log.e(TAG, "Could not send a complex message to the service: " + e);
}
}
private void updateStatus(String status) {
// Make sure the latest status is updated in the GUI, which
// is handled by the parent activity.
ISampleServiceClient uiContext = (ISampleServiceClient) getActivity();
if(uiContext != null) {
uiContext.updateStatus(status);
}
}
}
客户端片段代码相当简单。当用户单击 Bind Service 按钮时,客户端片段绑定到远程服务并设置 ServiceConnection。绑定是从应用上下文中完成的。这是首选,因为片段不能绑定服务,但活动和应用可以。但是,因为活动可能在配置更改期间消失,所以最好绑定到始终存在的应用。当 ServiceConnection 被连接时,一个传出的 Messenger 被设置为向服务发送 MSG_REGISTER_CLIENT 注册客户端消息。客户端不等待服务的回复,而是返回等待用户的下一次交互。这可以防止可怕的 ANR 弹出窗口。按发送简单信息创建一条简单信息并发送。
对于一个简单的消息,服务会回复消息,由客户端的处理器接收并处理。客户端处理器所做的只是用从服务接收的值更新 TextView。请注意,客户端片段使用父活动的 ISampleServiceClient 接口来调用适当的方法来更新 UI。这是因为客户端片段是非 UI 的,我们不希望在其中嵌入 UI 逻辑。接口将客户端片段与活动分离开来,使得活动在配置更改期间离开和返回变得容易。按 Send Complex 创建一个消息,该消息带有一个包含几个不同值的包,该包被发送到服务。服务将在通知中使用 double 值来证明该值已从客户端正确传输到服务。该服务不会为复杂消息发送回复消息。
对于这种进程间服务调用机制,需要注意的一点是:服务的处理器是从传入消息队列开始工作的,因此默认情况下是单线程的。除非您自己创建一些线程,否则不会有多线程来处理传入的服务消息。因为客户端不会阻止来自服务的回复,所以如果服务需要一段时间来响应消息,客户端应用不会崩溃。然而,如果你的服务有多个客户端的话,你需要记住这一点。AIDL 服务可以更容易地同时处理请求,因此如果您需要更可预测的响应时间,它可能是更好的选择。
如果您的客户端向多个服务发送消息,您可以使用一个 Messenger/Handler 对来处理来自这些服务的回复消息。您只需将相同的 Messenger 放入每个出站消息中,每个服务就会回复。
另一件要注意的事情是,客户端不能保证服务会永远响应。信使/处理器交互没有固有的超时。如果服务终止,您将通过 onServiceDisconnected() 得到通知,但如果服务挂起或运行时间过长,您将不会得到通知。因此,为了确保服务及时响应,客户端可以选择设置一个计时器,或者设置一个闹钟来再次唤醒它。如果在计时器/闹钟响起之前回复回来,客户端处理器可以清除它。如果计时器/闹钟唤醒了客户端,这意味着服务花费了太长时间,客户端可以采取适当的措施。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
- 【www.androidbook.com/proandroid5/projects】:与本书相关的可下载项目列表。对于这一章,寻找一个名为 proandroid 5 _ Ch14 _ services . ZIP 的 ZIP 文件。这个 ZIP 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 ZIP 文件之一导入到您的 IDE 中。
HC . Apache . org/http components-client-ga/tutorial/html/:关于使用 HttpClient 类的优秀教程,包括认证和 cookies 的使用。Developer . android . com/Guide/components/Bound-Services . html:绑定服务的 Android 开发者指南。
摘要
这一章是关于服务的,特别是:
- 我们讨论了使用 Apache HttpClient 消费外部 HTTP 服务。
- 关于使用 HttpClient ,我们向您展示了如何进行 HTTP GET 调用和 HTTP POST 调用。
- 我们还向您展示了如何进行多部分发布。
- 您了解了 SOAP 可以在 Android 上实现,但是它不是调用 web 服务的首选方式。
- 我们讨论了如何设置一个 Internet 代理来代表您的应用从某个服务器管理 SOAP 服务,这样您的应用就可以对您的代理使用 RESTful 服务,并使应用更简单。
- 然后,我们讨论了异常处理和应用可能遇到的异常类型(主要是超时)。
- 您看到了如何使用 ThreadSafeClientConnManager 在您的应用中共享一个公共的 HttpClient 。
- 您学习了如何检查和设置网络连接的超时值。
- 我们讨论了连接 web 服务的几个选项,包括 HttpURLConnection 和 AndroidHttpClient 。
- 我们解释了本地服务和远程服务之间的区别。本地服务是在与服务相同的流程中由组件(如活动)消费的服务。远程服务是其客户端在托管服务的进程之外的服务。
- 您了解了即使一个服务应该在一个单独的线程上,它仍然由开发人员来创建和管理与服务相关联的后台线程。
- 您了解了如何启动和停止本地服务,以及如何创建和绑定到远程服务。
- 您看到了如何使用 NotificationManager 来跟踪正在运行的服务。
- 我们讲述了如何使用复杂类型的 Parcelables 将数据传递给服务。
- 您了解了如何使用信使和处理器来调用远程服务。*******
十五、高级异步任务和进度对话框
在第十三章中,我们介绍了处理器和工作线程来运行长时间运行的任务,同时主线程保持 UI 的有序。Android SDK 已经认识到这是一种模式,并将处理器和线程细节抽象成一个名为 AsyncTask 的工具类。您可以使用 AsyncTask 在 UI 环境中运行耗时超过 5 秒的任务。(我们将在第十六章的“长时间运行的接收器和服务”中讲述如何运行真正长时间运行的任务,从几分钟到几小时不等。)
本章将从一个异步任务 的基础开始,转到显示进度对话框和进度条所需的代码,这些对话框和进度条能正确显示一个异步任务的状态,即使设备改变了它的配置。让我们通过清单 15-1 中的伪代码来介绍异步任务。
清单 15-1 。活动对异步任务的使用模式
public class MyActivity {
void respondToMenuItem() { //menu handler
performALongTask();
}
void performALongTask() { //using an AsyncTask
//Derive from an AsyncTask, and Instantiate this AsyncTask
MyLongTask myLongTask = new MyLongTask(...CallBackObjects...);
myLongTask.execute(...someargs...); //start the work on a worker thread
//have the main thread get back to its UI business
}
//Hear back from the AsyncTask
void someCallBackFromAsyncTask(SomeParameterizedType x) {
//Although invoked by the AsyncTask this code runs on the main thread.
//report back to the user of the progress
}
}
使用一个 AsyncTask 首先从扩展 AsyncTask 开始,就像清单 15-1 中的 MyLongTask 一样。一旦实例化了 AsyncTask 对象,就可以对该对象调用 execute() 方法。 execute() 方法在内部启动一个单独的线程来完成实际的工作。 AsyncTask 实现将依次调用多个回调来报告任务的开始、任务的进度和任务的结束。清单 15-2 显示了扩展一个 AsyncTask 的伪代码以及需要被覆盖的方法。(请注意,这是伪代码,不打算编译。添加@override 注释是为了显式声明它们是从基类中重写的。)
清单 15-2 。扩展 AsyncTask:示例
public class MyLongTask extends AsyncTask<String,Integer,Integer> {
//... constructors stuff
//Calling execute() will result in calling all of these methods
@Override
void onPreExecute(){} //Runs on the main thread
//This is where you do all the work and runs on the worker thread
@Override
Integer doInBackground(String... params){}
//Runs on the main thread again once it finishes
@Override
void onPostExecute(Integer result){}
//Runs on the main thread
@Override
void onProgressUpdate(Integer... progressValuesArray){}
//....other methods
}
在主线程上调用清单 15-1 中的 execute() 方法。这个调用将触发清单 15-2 中的一系列方法,从 onPreExecute() 开始。在主线程上也调用了 onPreExecute() 。您可以使用此方法来设置执行任务的环境。您还可以使用此方法来设置一个对话框或启动一个进度条,以向用户指示工作已经开始。在完成 onPreExecute() 之后, execute() 方法将返回,活动的主线程继续其 UI 职责。到那时, execute() 将会产生一个新的工作线程,因此 doInBackground() 方法被调度在该工作线程上执行。在这个 doInBackground() 方法中,你将完成所有繁重的工作。因为这个方法运行在一个工作线程上,所以主线程不受影响,您也不会得到“应用没有响应”的消息。从 doInBackground() 方法中,您可以调用 onprogress update()来报告进度。这个 onProgressUpdate() 方法在主线程上运行,这样您就可以影响主线程上的 UI。
简单 AsyncTask 的要点
让我们进入扩展 AsyncTask 的细节。 AsyncTask 类使用泛型为其方法提供类型安全,包括被覆盖的方法。当您查看 AsyncTask 类的部分定义(清单 15-3 )时,您可以看到这些泛型。(请注意清单 15-3 是 AsyncTask 类的一个极其精简的版本。它实际上只是客户端代码最常用的接口元素。)
清单 15-3 。快速浏览一下 AsyncTask 类的定义
public class AsyncTask<Params, Progress, Result> {
//A client will call this method
AsyncTask<Params, Progress, Result> execute(Params... params);
//Do your work here. Frequently triggers onProgressUpdate()
Result doInBackGround(Params... params);
//Callback: After the work is complete
void onPostExecute(Result result);
//Callback: As the work is progressing
void onProgressUpdate(Progress... progressValuesArray);
}
研究清单 15-3 ,可以看到 AsyncTask (通过泛型)在扩展时需要以下三个参数化类型( Params 、 Progress 和 Result )。让我们简单解释一下这些类型:
- Params(execute()方法的参数类型):当扩展 AsyncTask 时,您需要指出您将传递给 execute() 方法的参数类型。如果您说您的 Params 类型是字符串,那么 execute() 方法将期望在它的调用中有任意数量的由逗号分隔的字符串,例如 execute(s1,s2,s3) 或 execute(s1,s2,s3,s4,s5) 。
- Progress (进度回调方法的参数类型):该类型指示在通过回调 onProgressUpdate(Progress)报告进度时传递回调用者的值的数组...。传递进度值数组的能力允许对任务的多个方面进行监控和报告。例如,如果一个 AsyncTask 正在处理多个子任务,就可以使用这个特性。
- Result (用于通过 onPostExecute() 方法报告结果的类型):该类型表示通过回调 onpost execute(Result final Result)作为执行的最终结果返回的返回值的类型。
现在知道了一个 AsyncTask 所需的泛型类型,假设我们为我们特定的 AsyncTask 决定了以下参数:Params :一个字符串,结果:一个 int,Progress :一个整数。然后,我们可以声明一个扩展的 AsyncTask 类,如清单 15-4 所示。
清单 15-4 。扩展通用 AsyncTask 到具体类型
public class MyLongTask
extends AsyncTask<String,Integer,Integer>
{
//...other constructors stuff
//...other methods
//Concrete methods based on the parameterized types
protected Integer doInBackground(String... params);
protected void onPostExecute(Integer result);
protected void onProgressUpdate(Integer... progressValuesArray);
//....other methods
}
请注意清单 15-4 、 MyLongTask 中的这个具体类是如何消除类型名的歧义并得到类型安全的函数签名的。
实现您的第一个异步任务
现在让我们来看看一个简单但完整的 MyLongTask 的实现。我们已经充分注释了清单 15-5 中的代码,指出哪些方法运行在哪个线程上。还要注意 MyLongTask 的构造函数,它接收调用上下文(通常是一个活动)的对象引用,以及一个特定的简单接口,如 IReportBack 来记录进度消息。
IReportBack 接口对于您的理解并不重要,因为它只是一个日志的包装器。对于工具类也是如此。你可以在本章的两个可下载项目中看到这些额外的类。可下载项目的 URL 位于本章末尾的参考资料部分。清单 15-5 显示了 MyLongTask 的完整代码。
清单 15-5 。实现 AsyncTask 的完整源代码
//The following code is in MyLongTask.java (ProAndroid5_Ch15_TestAsyncTask.zip)
//Use menu item: Test Async1 to invoke this code
public class MyLongTask extends AsyncTask<String,Integer,Integer>
{
IReportBack r; // an interface to report back log messages
Context ctx; //The activity to start a dialog
public String tag = null; //Debug tag
ProgressDialog pd = null; //To start, report, and stop a progress dialog
//Constructor now
MyLongTask(IReportBack inr, Context inCtx, String inTag) {
r = inr; ctx = inCtx; tag = inTag;
}
//Runs on the main ui thread
protected void onPreExecute() {
Utils.logThreadSignature(this.tag);
pd = ProgressDialog.show(ctx, "title", "In Progress...",true);
}
//Runs on the main ui thread. Triggered by publishProgress called multiple times
protected void onProgressUpdate(Integer... progress) {
Utils.logThreadSignature(this.tag);
Integer i = progress[0];
r.reportBack(tag, "Progress:" + i.toString());
}
protected void onPostExecute(Integer result) {
//Runs on the main ui thread
Utils.logThreadSignature(this.tag);
r.reportBack(tag, "onPostExecute result:" + result);
pd.cancel();
}
//Runs on a worker thread. May even be a pool if there are more tasks.
protected Integer doInBackground(String...strings) {
Utils.logThreadSignature(this.tag);
for(String s :strings) {
Log.d(tag, "Processing:" + s);
}
for (int i=0;i<3;i++) {
Utils.sleepForInSecs(2);
publishProgress(i); //this calls onProgressUpdate
}
return 1; //this value is then passed to the onPostExecute as input
}
}
在简要介绍了客户端如何使用(或调用) MyLongTask 之后,我们将深入研究清单 15-5 中强调的每一个方法的细节。
调用异步任务
一旦我们实现了类 MyLongTask ,客户端将会使用这个类,如清单 15-6 所示。
清单 15-6 。调用异步任务
//You will find this class AsyncTester.java(ProAndroid5_Ch15_TestAsyncTask.zip)
//Use menu item: Test Async1 to invoke this code
void respondToMenuItem() {
//An interface to log some messages back to the activity
//See downloadable project if you need the details.
IReportBack reportBackObject = this;
Context ctx = this; //activity
String tag = "Task1"; //debug tag
//Instantiate and execute the long task
MyLongTask mlt = new MyLongTask(reportBackObject,ctx,tag);
mlt.execute("String1","String2","String3");
}
注意 execute() 方法是如何在清单 15-6 中被调用的。因为我们已经将其中一个泛型类型指定为一个字符串,并且 execute() 方法接受该类型的可变数量的参数,所以我们可以将任意数量的字符串传递给 execute() 方法。在清单 15-6 的例子中,我们传递了三个字符串参数。你可以根据需要或多或少地通过。
一旦我们在 AsyncTask 上调用了 execute() 方法,这将导致调用 onPreExecute() 方法,然后调用 doInBackground() 方法。一旦 doInBackground() 方法完成,系统也将调用 onPostExecute() 回调。关于这些方法是如何实现的,请参考清单 15-5 。
了解 onPreExecute()回调和进度对话框
回到 MyLongTask 实现在清单 15-5 中,在 onPreExecute() 方法中,我们启动了一个进度对话框来指示任务正在进行中。图 15-1 显示了该对话框的图像。(使用菜单项 Test Async1 从项目下载 pro Android 5 _ Ch15 _ testasync task . zip 调用该视图。)
图 15-1 。与异步任务交互的简单进度对话框
显示进度对话框的代码段(取自清单 15-5 )在清单 15-7 中重现。
清单 15-7 。显示不确定的进度对话框
pd = ProgressDialog.show(ctx, "title", "In Progress...",true);
变量 pd 已经在构造函数中声明了(见清单 15-5 )。清单 15-7 中的这个调用将创建一个进度对话框,并显示如图 15-1 所示。清单 15-7 中 show() 方法的最后一个参数表示对话框是否不确定(对话框是否可以预先估计有多少工作量)。我们将在后面的章节中讨论确定性的情况。
注意可靠地显示异步任务的进度是相当复杂的。这是因为一个活动可以来来去去,要么是因为配置改变,要么是因为另一个 UI 优先。我们将在本章的后面讨论这个基本需求和解决方案。
了解 doInBackground()方法
由 AsyncTask 执行的所有后台工作都在 doInBackground() 方法中完成。这个方法由 AsyncTask 编排,在一个工作线程上运行。因此,这项工作被允许花费超过五秒钟,不像在主线程上完成的工作。
在清单 15-5 的例子中,在 doInBackground() 方法中,我们简单地检索任务的每个输入字符串,就好像它们是一个数组。在这个方法定义中,我们没有定义一个显式的字符串数组。然而,这个函数的单个参数被定义为变长参数,如清单 15-8 所示。
清单 15-8 。 doInBackground() 方法签名
protected Integer doInBackground(String...strings)
然后,Java 将参数视为函数内部的数组。因此,在我们的代码中的 doInBackground() 方法中,我们读取每个字符串并记录它们,以表明我们知道它们是什么。然后,我们等待足够长的时间来模拟长时间运行的操作。因为这个方法运行在一个工作线程中,所以我们不能从这个工作线程访问 Android 的 UI 功能。例如,你不能直接更新任何视图,即使你可以从这个线程访问它们。你甚至不能在这里发祝酒辞。接下来的两种方法可以让我们克服这个问题。
通过 publishProgress()触发 onProgressUpdate()
在 doInBackground() 方法中,可以通过调用 publishProgress() 方法来触发 onProgressUpdate() 。被触发的 onProgressUpdate() 方法然后在主线程上运行。这允许 onProgressUpdate() 方法适当地更新 UI 元素,比如视图。也可以从这里发祝酒辞。在清单 15-5 中,我们简单地记录了一条消息。一旦所有的工作都完成了,我们从 doInBackground() 方法返回一个结果代码。
了解 onPostExecute()方法
来自 doInBackground() 方法的结果代码然后被传递给 onPostExecute() 回调方法。这个回调也在主线程上执行。在这个方法中,我们告诉进度对话框关闭。在主线程上,您可以不受限制地访问该方法中的任何 UI 元素。
升级到确定性进度对话框
在清单 15-5 的的前一个例子中,我们使用了一个进度对话框(图 15-1 ),它并没有告诉我们工作的哪一部分已经完成。这个进度对话框称为不确定进度对话框。如果您在这个进度对话框中将不确定属性设置为 false ,您将看到一个进度对话框,它会逐步跟踪进度。如图 15-2 中的所示。(使用菜单项“Test Async2”从项目下载中调用该视图 pro Android 5 _ Ch15 _ testasynctask . zip .)
图 15-2 。显示明确进度的进度对话框,与异步任务交互
清单 15-9 显示了来自清单 15-5 的前一个任务,它被重写以将进度对话框的行为改变为确定性进度对话框。我们还添加了一个 onCancelListener 来查看我们是否需要在取消对话框时取消任务。用户可以点击图 15-2 中的后退按钮取消对话框。代码的关键部分在清单 15-9 中给出(完整代码见下载文件 pro Android 5 _ Ch15 _ testasynctask . zip)。
清单 15-9 。利用确定性进度对话框的长任务
//Following code is in MyLongTask1.java(ProAndroid5_Ch15_TestAsyncTask.zip)
//Use menu item: Test Async2 to invoke this code
public class MyLongTask1 extends AsyncTask<String,Integer,Integer>
implements OnCancelListener
{
//..other code taken from Listing 15-5
//Also refer to the java class MyLongTask1.java in the downloadable project
//for full code listing.
protected void onPreExecute() {
//....other code
pd = new ProgressDialog(ctx);
pd.setTitle("title");
pd.setMessage("In Progress...");
pd.setCancelable(true);
pd.setOnCancelListener(this);
pd.setIndeterminate(false);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMax(5);
pd.show();
}
public void onCancel(DialogInterface d) {
r.reportBack(tag,"Cancel Called");
this.cancel(true);
}
//..other code taken from Listing 15-5
}
注意我们是如何准备清单 15-9 中的进度对话框的。在这种情况下,我们没有使用静态方法 show() ,这与我们在进度对话框的清单 15-5 中所做的相反。相反,我们显式地实例化了进度对话框。变量 ctx 代表 UI 进度对话框运行的上下文(或活动)。然后我们单独设置对话框的属性,包括它的确定性或不确定性行为。方法 setMax() 表示进度对话框有多少步。当取消被触发时,我们还将自身引用( AsyncTask 本身)作为监听器传递。在取消回调中,我们在 AsyncTask 上显式发出一个取消。如果我们用布尔参数 false 调用 cancel() 方法,它将尝试停止工作线程。布尔参数真将强制停止工作线程。
异步任务和线程池
考虑清单 15-10 中的代码,其中一个菜单项一个接一个地调用两个 AsyncTasks 。
清单 15-10 。调用两个长期运行的任务
void respondToMenuItem() {
MyLongTask mlt = new MyLongTask(this.mReportTo,this.mContext,"Task1");
mlt.execute("String1","String2","String3");
MyLongTask mlt1 = new MyLongTask(this.mReportTo,this.mContext,"Task2");
mlt1.execute("String1","String2","String3");
}
这里我们在主线程上执行两个任务。你可能会认为这两项任务开始的时间很接近。但是,默认行为是,这些任务使用从线程池中抽出的单个线程按顺序运行。如果想要并行执行,可以在 AsyncTask 上使用 executeOnExecutor() 方法。有关此方法的详细信息,请参见 SDK 文档。同样根据 SDK 文档,在单个 AsyncTask 上多次调用 execute() 方法是无效的。如果您想要这种行为,您必须实例化一个新任务并再次调用 execute() 方法。
正确显示异步任务进度的问题和解决方案
如果你学习这一章的主要目标是学习 AsyncTask 的基本知识,那么我们到目前为止所学的已经足够了。然而,当一个 AsyncTask 与一个进度对话框配对时,会出现一些问题,如前面的清单所示。其中一个问题是当设备旋转时, AsyncTask 将丢失正确的活动参考,从而也丢失了它对进度对话框的参考。另一个问题是,我们在前面代码中使用的进度对话框不是托管对话框。现在让我们来理解这些问题。
处理活动指针和设备轮换
当由于配置改变而重新创建活动时,由 AsyncTask 持有的活动指针变得陈旧。这是因为 Android 创建了一个新的活动,旧的活动不再显示在屏幕上。因此,抓住旧的活动及其对应的对话框不放是不好的,原因有两个。首先,用户看不到异步任务试图更新的活动或对话。第二个原因是旧的活动需要进行垃圾收集,而您正在阻止它进行垃圾收集,因为 AsyncTask 正在保留它的引用。如果您聪明地对旧活动使用 Java 弱引用,那么您不会泄漏内存,但会得到一个空指针异常。陈旧指针不仅适用于活动指针,也适用于间接指向活动的任何其他指针。
有两种方法可以解决过时的活动引用问题。推荐的方法是使用无头保留片段。(片段在第八章有所涉及。保留片段是由于配置更改而重新创建活动时保留下来的片段。这些片段也被称为无头的,因为它们不一定要保存任何 UI。)解决陈旧活动指针的另一种方法是使用来自活动的保留对象回调。我们将介绍这两种解决陈旧活动指针问题的方法。
处理托管对话
即使我们能够解决过时的活动引用问题并重新建立与当前活动的连接,本章迄今为止使用进度对话框的方式仍存在缺陷。我们已经直接实例化了一个进度对话框。以这种方式创建的 ProgressDialog 不是一个“受管理的”对话框。如果它不是托管对话,则当设备经历旋转或任何其他配置更改时,该活动将不会重新创建该对话。因此,当设备旋转时,异步任务仍然不间断地运行,但是对话框不会显示。也有几种方法可以解决这个问题。建议不要使用进度对话框,而是在活动本身中使用嵌入式 UI 控件,比如进度条。因为进度条是活动视图层次结构的一部分,所以希望它能被重新创建。虽然进度条听起来不错,但有时模式进度对话框更有意义。例如,如果您不希望用户在 AsyncTask 运行时与活动的任何其他部分进行交互,就会出现这种情况。在这些情况下,我们发现使用片段对话框代替进度条并没有什么矛盾。
现在是我们进入解决方案来处理活动引用问题和托管对话框问题的时候了。我们将提出三种不同的解决方案。第一种使用保留对象和片段对话框。第二种使用了无头保留片段和片段对话框。第三种解决方案使用无头的保留片段和进度条。
行为良好的进度对话框的测试场景
在本章的三个解决方案中,无论您使用哪一个来正确显示 AsyncTask 的进度对话框,该解决方案都应该在以下所有测试场景中工作:
- 如果不改变方向,进度对话框必须开始,显示其进度,结束,并清除对 AsyncTask 的引用。这必须重复工作,以显示没有从以前的运行留下痕迹。
- 解决方案应该在任务执行过程中处理方向变化。旋转应该会重新创建对话框,并在它停止的地方显示进度。该对话框应正确结束并清除异步任务参考。这必须反复进行,以显示没有留下任何痕迹。
- 任务开始运行时,应禁用背面。
- 即使任务正在执行中,也应该允许回家。
- 回家重访活动要显示对话框,正确反映当前进度,进度绝对不能小于之前的进度。
- 当任务在返回之前完成时,回家并重新进行活动也应该有效。应该正确关闭该对话框,并删除 AsyncTask 引用。
这组测试用例应该总是为所有处理异步任务的活动执行。既然我们已经列出了每个解决方案应该如何满足,让我们从第一个解决方案开始,这个解决方案使用保留对象和片段对话框。
使用保留对象和片段对话框
在第一个解决方案中,让我们向您展示如何使用保留的对象和片段对话框来正确显示 AsyncTask 的进度。该解决方案包括以下步骤:
- 活动必须通过其 onRetainNonConfigurationInstance()回调来跟踪外部对象。这个外部对象必须保留下来,并且在活动关闭并返回时验证其引用。这就是这个对象被称为保留对象的原因。这个被保留的对象既可以是 AsyncTask 对象本身,也可以是一个保存对 AsyncTask 的引用的中间对象。让我们称之为根保留的活动相关对象(或根 RADO)。它被称为“根”是因为 onRetainNonConfigurationInstance()只能使用一个保留的对象引用。
- 然后,根 RADO 将有一个指向异步任务的指针,并且可以随着活动的到来和结束,设置和重置异步任务上的活动指针。因此,这个根 RADO 充当活动和 AsyncTask 之间的中介。
- 然后, AsyncTask 将实例化一个片段进度对话框,而不是一个普通的非托管进度对话框。 AsyncTask 将使用由根 RADO 设置的活动指针来完成这一任务,因为您将需要一个活动来创建一个包含片段对话框的片段。
- 活动将在对话片段旋转时重新创建对话片段,并适当地保持其状态,因为对话片段是受管理的。只要活动被设置并且可用, AsyncTask 就可以在片段对话框上增加进度。请注意,这个对话片段本身不是保留片段。它作为活动生命周期的一部分被重新创建。
- 片段对话框可以进一步禁止取消,这样当 AsyncTask 正在进行时,用户不能从对话框返回到活动。
- 然而,用户可以通过点击 Home 并使用其他应用来回家。这将把我们的活动以及与之相关的对话推到后台。这件事必须处理。当用户返回到活动或应用时,对话框可以继续显示进度。如果任务在活动隐藏时完成,AsyncTask 必须知道如何关闭片段对话框。作为一个片段对话框,如果活动不在前台,关闭此对话框将引发无效状态异常。因此,AsyncTask 必须等到活动重新打开并处于正确的状态时才能关闭对话框。
探索相应的关键代码片段
我们现在将展示用于实现所概述的方法的关键代码片段。其余的实现可以在本章的可下载项目 proandroid 5 _ Ch15 _ testasynctaskswithconfigchanges . zip 中找到。由于这个问题的所有解决方案都要求对话框是一个片段对话框,以便可以管理这个对话框,清单 15-11 首先给出了这个片段对话框的源代码。
清单 15-11。将 ProgressDialog 封装在 DialogFragment 中
//The following code is in ProgressDialogFragment.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
/**
* A DialogFragment that encapsulates a ProgressDialog.
* This is not expected to be a retained fragment dialog.
* Gets re-created as activity rotates following any fragment protocol.
*/
public class ProgressDialogFragment extends DialogFragment {
private static String tag = "ProgressDialogFragment";
ProgressDialog pd; //Will be set by onCreateDialog
//This gets called from ADOs such as retained fragments
//typically done when activity is attached back to the AsyncTask
private IFragmentDialogCallbacks fdc;
public void setDialogFragmentCallbacks(IFragmentDialogCallbacks infdc) {
Log.d(tag, "attaching dialog callbacks");
fdc = infdc;
}
//This is a default constructor. Called by the framework all the time
//for reintroduction.
public ProgressDialogFragment() {
//Should be safe for me to set cancelable as false;
//wonder if that is carried through rebirth?
this.setCancelable(false);
}
//One way for the client to attach in the beginning when the fragment is reborn.
//The reattachment is done through setFragmentDialogCallbacks
//This is a shortcut. Your compiler if enabled for lint may throw an error.
//You can use the newInstance pattern and setbundle (see the fragments chapter)
public ProgressDialogFragment(IFragmentDialogCallbacks infdc) {
this.fdc = infdc;
this.setCancelable(false);
}
/**
* This can get called multiple times each time the fragment is
* re-created. So storing the dialog reference in a local variable should be safe
*/
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Log.d(tag,"In onCreateDialog");
pd = new ProgressDialog(getActivity());
pd.setTitle("title");
pd.setMessage("In Progress...");
pd.setIndeterminate(false);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMax(15);
return pd;
}
//Called when the dialog is dismissed.I should tell my corresponding task
//to close or do the right thing! This is done through call back to fdc
//fdc: fragment dialog callbacks could be the Task, or Activity or the rootRADO
//See Listing 15-12 to see how FDC is implemented by the task
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
Log.d(tag,"Dialog dismissed");
if (fdc != null) {
fdc.onDismiss(this, dialog);
}
}
@Override
public void onCancel(DialogInterface dialog) {
super.onDismiss(dialog);
Log.d(tag,"Dialog cancelled");
if (fdc != null) {
fdc.onCancel(this, dialog);
}
}
//will be called by a client like the task
public void setProgress(int value) {
pd.setProgress(value);
}
}
清单 15-11 中的代码展示了如何将一个常规的非托管 ProgressDialog 包装在一个托管片段对话框中。我们扩展一个 DialogFragment 并覆盖它的 onCreateDialog() 以返回 ProgressDialog 对象。除了这个基本特性之外,我们还增加了监视进度对话框何时关闭或取消的功能。我们还在包装的类上提供了一个 setProgress() 方法来调用内部 ProgressDialog 上的 setProgress() 。您可以在可下载的项目(proandroid 5 _ Ch15 _ testasynctaskwithconfigchanges . zip)中看到 IFragmentDialogCallbacks 的源代码,因为它对于理解这个片段进度对话框并不重要。
现在让我们看看 AsyncTask 如何创建和控制这个片段进度对话框。为了帮助理解,清单 15-12 给出了异步任务的伪代码。有关完整的源代码,请参考可下载的项目。
清单 15-12 。使用片段进度对话框的 AsyncTask 的伪代码
//The following code is in MyLongTaskWithRADO.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can start this task through menu item: Flip Dialog with ADOs
public class MyLongTaskWithRADO extends AsyncTask<String,Integer,Integer>
implements IRetainedADO, IFragmentDialogCallbacks
{
//....other code
@Override public void onPreExecute() {
//....other code
//get the activity as it would have been set by the root RADO
Activity act = this.getActivity();
//Create the progress diaolg
ProgressDialogFragment pdf = new ProgressDialogFragment();
//the show method will add and commit the fragment dialog
pdf.show(act.getFragmentManager(), this.PROGRESS_DIALOG_FRAGMENT_TAG_NAME);
}
@Override public void onProgressUpdate() {
//if activity is available, get the fragment dialog from it
//call setProgress() on it
//otherwise ignore the progress
}
@Override public void onPostExecute() {
//if activity is in a good state
//dismiss the dialog and tell the root RADO to drop the pointer to the AsyncTask
//if not remember it through a flag to close it when you come back
}
@Override public void attach() {
//called when the activity is back
//check to see if you are done
//if so dismiss the dialog and remove yourself from the RADO
//if not continue to update the progress
}
}
因为这个 AsyncTask 实现了保留的活动相关对象( IRetainedADO )的思想,它知道活动何时可用,何时不可用。它还知道活动的状态,比如 UI 是否准备好了。尽管实现活动相关对象(ado)需要一些代码,但这并不是一个难懂的概念。出于篇幅考虑,我们将这个问题留给您来研究可下载的项目 pro Android 5 _ Ch15 _ testasynktaskwithconfigchanges。zip 看看这是怎么做到的。
这个清单 15-12 中的 AsyncTask 也接管了它的片段对话框的管理,这样它就像一个内聚的单元,从而不会因为这个 AsyncTask 的细节而污染主活动。清单 15-12 中的另一个关键细节是当 AsyncTask 结束时对话框关闭时会发生什么。此时,如果活动是隐藏的,或者由于旋转而不存在,那么在重新创建活动时关闭对话框是很重要的。为此, onPostExecute() 会记住 AsyncTask 的最后状态,无论它是已完成还是正在进行。这个 AsyncTask 然后等待 attach() 方法,当 UI 就绪活动被重新附加到这个 ADO 时,该方法被调用。一旦进入 attach() 方法, AsyncTask 就可以关闭片段对话框。
你可以下载名为 pro Android 5 _ Ch15 _ testasynctaskswithconfigchanges . zip 的项目,看看清单 15-12 中呈现的交互是如何完全实现的。
与使用保留片段相比,这种使用保留对象的特殊方法有点复杂。但它的优雅之处在于,使用 ADOs 的思想以更通用的形式解决了这个问题,不管它们是片段还是其他。我们在参考资料部分提供了概述这一想法并提供背景的链接。至此,让我们把注意力转向我们的第二个解决方案中推荐的保留片段的想法。
使用保留片段和片段对话框
在第二个解决方案中,我们将坚持使用片段对话框,但是我们将使用无头保留片段,而不是简单的保留对象。Android 不赞成保留对象,而支持保留片段。在 Android 中,保留的对象只是一个对象,没有跟踪活动状态的内置功能。(这就是为什么我们必须在顶层发明 ADOs 的框架。)随着 Android 后续版本中片段的引入,这一缺陷不复存在。尽管片段紧密地编织在 UI 的结构中,但是它们也可以在没有 UI 的情况下存在。这些被称为无头片段。除了能够跟踪活动的状态,片段也可以被保留,就像保留的对象一样。
概述保留片段的方法
这个解决方案中的方法是使用一个无头的保留片段作为锚,在活动和异步任务之间进行通信。这种方法的主要方面如下:
- 继续使用片段进度对话框,就像之前的解决方案一样。
- 让活动创建一个无头保留片段,该片段保存一个指向 AsyncTask 的指针。这个无头的保留片段取代了前面解决方案中的保留对象。作为一个保留的片段,当用一个新的指针重新创建活动时,片段对象仍然存在。然后,AsyncTask 总是依赖保留的片段来检索最新的活动指针。
- AsyncTask 依赖于被告知活动状态的无头保留片段来完成前面解决方案中指出的所有测试用例。
探索相应的关键代码片段
在之前的解决方案中,我们已经向您展示了片段对话框的代码。由于我们在这个解决方案中继续使用同一个对象,我们将关注保留片段,以及 AsyncTask 如何通过保留片段使用片段对话框。
在我们在下载中提供的示例程序(proandroid 5 _ Ch15 _ testasynctaskwithconfigchanges . zip)中,我们将保留片段称为 AsyncTesterFragment 。清单 15-13 显示了这个类的伪代码,它展示了,除了别的以外,是什么使这个类成为一个无头片段。
清单 15-13 。无头片段的伪代码
//The following code is in AsyncTesterFragment.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can start this task through menu item: Flip Dialog with Fragment
public class AsyncTesterFragment
扩展片段(或从片段派生的另一个对象){
//No need to override the key onCreateView() method
//which otherwise would have returned a view loaded from a layout.
//Thus having no View makes this fragment a headless fragment
//Use this name to register with the activity
public static String FRAGMENT_NAME="AsyncTesterRetainedFragment";
//Local variable for the asynctask. You can use a menu to start work on this task
//Nullify this reference when the asynctask finishes
MyLongTaskWithFragmentDialog taskReference;
//Have an init method to help with inheritance
public void init(arg1, arge2, etc) {
super.init(arg1,...); //if there is one
setArguments(....); //or pass the bundle to the super init
}
public static AsyncTesterFragment newInstance(arg1, arg2, ...){
AsyncTesterFragment f = new AsyncTesterFragment();
f.init(arg1,arg2,...);
}
//have more static methods to create the fragment, locate the fragment etc.
}
清单 15-13 中的代码有三点值得一提。由于没有覆盖 onCreateView() ,这个片段变成了一个无头片段。因为片段是使用默认构造函数重新创建的,所以我们遵循了 newInstance() 模式,并扩展了该模式以使用 init() 方法,这些方法可以是虚拟的,也可以是继承的。如果您在更深层次中扩展片段类,后一种方法是有用的。
清单 15-14 显示了这个 AsyncTesterFragment 对象上的一个静态方法,它可以创建这个片段,让它保持它的状态,然后用活动注册它。
清单 15-14 。将片段注册为保留片段
//The following code is in AsyncTesterFragment.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can start this task through menu item: Flip Dialog with Fragment
public static AsyncTesterFragment createRetainedAsyncTesterFragment(Activity act) {
AsyncTesterFragment frag = AsyncTesterFragment.newInstance();
frag.setRetainInstance(true);
FragmentManager fm = act.getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(frag, AsyncTesterFragment.FRAGMENT_TAG);
ft.commit();
return frag;
}
一旦这个保留的片段在活动中可用,它可以在任何时候被检索,并被要求启动一个 AsyncTask 。清单 15-15 显示了 AsyncTask 的伪代码,它能够与这个保留的片段交互来控制片段对话框
清单 15-15 。通过保留片段使用片段对话的异步任务
//The following code is in MyLongTaskWithFragment.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can start this task through menu item: Flip Dialog with Fragment
public class MyLongTaskWithFragment extends AsyncTask<String,Integer,Integer> {
//...other code
//The following reference passed in and set from the constructor
AsyncTesterFragment retainedFragment;
//....other code
@Override protected void onPreExecute() {
....other code
//get the activity from the retained fragment
Activity act = retainedFragment.getActivity();
//Create the progress dialog
ProgressDialogFragment pdf = new ProgressDialogFragment();
//the show method will add and commit the fragment dialog
pdf.show(act.getFragmentManager(), this.PROGRESS_DIALOG_FRAGMENT_TAG_NAME);
}
@Override protected void onProgressUpdate() {
//if activity is available, get the fragment dialog from it, call setProgress() on it
//otherwise ignore the progress
}
@Override protected void onPostExecute() {
//if activity is in a good state
//dismiss the dialog and tell the root RADO to drop the pointer to the AsyncTask
//if not remember it through a flag to close it when you come back
}
@Override public void attach() {
//called when the activity is back. check to see if this task is done
//if so dismiss the dialog and remove yourself from the retained fragment
//if not continue to update the progress
}
@Override protected Integer doInBackground(String...strings)
{
//Do the actual work here which occurs on a separate thread
}
}
清单 15-15 中的 AsyncTask 的行为很像使用保留对象的 AsyncTask。一旦这个任务知道了如何从保留的片段中访问进度对话框片段,设置进度就非常简单了。和以前一样,这个任务也需要知道活动何时被重新附加,以防任务提前完成。如果发生这种情况, AsyncTask 需要记住这一点,并在重新挂接时关闭对话框。清单 15-15 中的伪代码满足了前面列出的所有测试条件。
这就结束了我们的第二个解决方案。现在让我们转到第三个解决方案,我们将使用进度条而不是进度对话框来显示一个 AsyncTask 的进度。
使用保留的片段和进度条
关于 progress dialog()的 Android SDK 文档建议我们在许多场景中使用 ProgressBar 作为更好的实践。据称的原因是进度条不太打扰,因为它允许与活动的其他区域进行交互。像进度对话框一样,进度条的持续时间可以是不确定的,也可以是固定的。也可以是连续旋转的圆,也可以是单杠。您可以通过查找进度条的文档找到这些模式。清单 15-16 给出了一个布局文件中进度条样式的快速纲要。
清单 15-16 。在布局文件中设置进度条样式的不同方法
//The following code is in spb_show_progressbars_activity_layout.xml
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can see these progress bars through menu item: Show Progress bars
<!-- A regular progress bar - A large spinning circle -->
<ProgressBar
android:id="@+id/tpb_progressBar1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/background_light"/>
<!-- Small spinning circle -->
<ProgressBar
android:id="@+id/tpb_progressBar4"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/background_light"/>
<!-- Horizontal indefinite Progress bar: a line -->
<ProgressBar
android:id="@+id/tpb_progressBar3"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
/>
<!-- Horizontal fixed duration Progress bar: a line -->
<ProgressBar
android:id="@+id/tpb_progressBar3"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="false"
android:max="50"
android:progress="10"
/>
图 15-3 显示了清单 15-16 中显示的进度条布局在加载到活动中时的样子。每种类型的进度条都有标签来指示其模式或行为。(使用菜单项显示进度条从项目下载 proandroid 5 _ Ch15 _ testasynktaskswithconfigchanges . zip .调用该视图)
图 15-3 。安卓进度条示例
概述 ProgressBar 方法
通过进度条报告异步任务进度的方法类似于之前使用保留的无头片段和片段进度对话框的方法。
- 与前面的解决方案一样,让活动创建一个无头保留片段,该片段保存一个指向 AsyncTask 的指针。
- 在活动布局中嵌入进度条。 AsyncTask 会通过无头保留片段到达这个进度条。
- AsyncTask 依赖于被告知活动状态的无头保留片段来完成前面指出的所有测试用例。
遍历相应的关键代码片段
让我们浏览一下让这个解决方案工作所需的关键代码片段。让我们从局部变量开始, AsyncTask 持有这些变量来与保留的片段和活动进行交互(清单 15-17 )。
清单 15-17。AsyncTask 的局部变量使用 ProgressBar
//The following code is in MyLongTaskWithProgressBar.java
//(ProAndroid5_Ch15_TestAsyncTaskWithConfigChanges.zip)
//You can start this task through menu item: Test ProgressBar
public class MyLongTaskWithProgressBar
extends AsyncTask<String,Integer,Integer>
implements IWorkerObject
{
public String tag = null; //Debug tag
private MonitoredFragment retainedFragment; //Reference to the retained fragment
int curProgress = 0; //To track current progress
....
清单 15-18 显示了 AsyncTask 启动时如何初始化进度条。
清单 15-18 。初始化进度条
//Part of MyLongTaskWithProgressBar.java
private void showProgressBar() {
Activity act = retainedFragment.getActivity();
ProgressBar pb = (ProgressBar) act.findViewById(R.id.tpb_progressBar1);
pb.setProgress(0);
pb.setMax(15);
pb.setVisibility(View.VISIBLE);
}
清单 15-19 显示了 AsyncTask 在定位后如何在进度条上设置进度。
清单 15-19 。在进度条上设置进度
//Part of MyLongTaskWithProgressBar.java
private void setProgressOnProgressBar(int i) {
this.curProgress = i;
ProgressBar pbar = getProgressBar();
if (pbar == null) {
Log.d(tag, "Activity is not available to set progress");
return;
}
pbar.setProgress(i);
}
定位活动的方法 getProgressBar() 相当简单;您只需使用 find() 方法来定位进度条视图。如果活动由于设备旋转而不可用,则进度条引用将为空,我们将忽略设置进度。清单 15-20 显示了 AsyncTask 如何关闭进度条。
清单 15-20 。异步任务完成时关闭进度条
//Part of MyLongTaskWithProgressBar.java
private void closeProgressBar(){
ProgressBar pbar = getProgressBar();
if (pbar == null) {
Log.d(tag, "Sorry progress bar is null to close it!");
return;
}
//Dismiss the dialog
pbar.setVisibility(View.GONE);
detachFromParent();
}
一旦 ProgresBar 从视图中移除,清单 15-20 中的代码通知保留的片段,它可以释放 AsyncTask 指针,如果它持有它的话。根据保留的片段如何保存这个指针,这个步骤可能需要,也可能不需要。但是告诉父类它不再需要保留它不再需要的引用是一个好的做法。因此,清单 15-21 展示了 AsyncTask 如何通知父进程它不再需要持有一个指向 AsyncTask 的指针。
清单 15-21 。像保留片段一样,通知客户端 AsyncTask 的完成
//To tell the called object that I, the AsyncTask, have finished
//The Activity or retained fragment can act as a client to this AsyncTask
//AsyncTask is imagined to be a WorkerObject and hence understands the IWorkerObjectClient
//MyLongTaskWithProgressBar implements IWorkerObject
//AsyncTesterFragment implements the IWorkerObjectClient
//Code below is taken from MyLongTaskWithProgressBar.java
//This implements the IWorkerObject contract
IWorkerObjectClient client = null;
int workerObjectPassbackIdentifier = -1;
public void registerClient(IWorkerObjectClient woc,
int inWorkerObjectPassbackIdentifier) {
client = woc;
this.workerObjectPassbackIdentifier = inWorkerObjectPassbackIdentifier;
}
private void detachFromParent() {
if (client == null) {
Log.e(tag,"You have failed to register a client.");
return;
}
//client is available
client.done(this,workerObjectPassbackIdentifier);
}
利用 ProgressBar 解决方案解决关键差异
当我们使用进度条而不是进度对话框时,有一些意想不到的差异你必须知道。
最初,在布局文件中,进度条的可见性被设置为消失,从而表示异步任务甚至还没有开始的状态。一旦 AsyncTask 开始,它会将可见性设置为可见,并随后设置进度。然而,当重新创建活动时,活动的状态管理要求从 onCreate() 方法出来的控件是可见的。因为在布局中可见性被设置为消失,所以该活动不会恢复进度条状态,并且当设备旋转时您将看不到进度条。因此, AsyncTask 需要接管这个进度条状态管理的控制权,并在活动被重新附加时正确地重新初始化它。清单 15-22 展示了我们如何在 AsyncTask 代码中实现这一点。
清单 15-22 。从 AsyncTask 管理 ProgressBar 状态
//Taken from MyLongTaskWithProgressBar.java
//On activity start
public void onStart(Activity act) {
//dismiss dialog if needed
if (bDoneFlag == true) {
Log.d(tag,"On my start I notice I was done earlier");
closeProgressBar();
return;
}
Log.d(tag,"I am reattached. I am not done");
setProgressBarRightOnReattach();
}
private void setProgressBarRightOnReattach() {
ProgressBar pb = getProgressBar();
pb.setMax(15);
pb.setProgress(curProgress);
pb.setVisibility(View.VISIBLE);
}
清单 15-22 中的 onStart() 方法由 AsyncTask 上的保留片段调用,此时活动被重新附加到保留片段,并且该片段检测到活动的 UI 已经准备好被使用。
使用进度条的另一个区别是后退按钮的行为。与进度对话框不同,对于活动,您可能希望允许后退按钮。由于“后退”按钮完全删除了该活动,您可能希望借此机会取消该任务。清单 15-23 中的 releaseResources() 方法被保留的片段调用,当它通过监视 onDestroy() 方法中的 isFinishing() 标志检测到活动不会返回时。
清单 15-23 。取消活动返回的异步任务
//Taken from MyLongTaskWithProgressBar.java
public void releaseResources() {
cancel(true); //cancel the task
detachFromParent(); //remove myself
}
本章后半部分概述的所有三种解决方案都将正确显示一个 AsyncTask 的进度。SDK 推荐的方法是使用进度条作为正确的 UI 组件来显示进度。我们对于只需要一两秒钟的快速任务的偏好是使用进度条。对于一个需要更长时间的任务——并且你不希望用户扰乱 UI 的状态——那么使用 ProgressDialog 和一个 headless retained 片段。当您的解决方案需要深层次的对象时,无论您是通过保留的片段还是通过保留的对象来使用 ADO 框架,使用 ADO 框架都会很方便。您可以在可下载的项目 pro Android 5 _ Ch15 _ testasynctaskswithconfigchanges . zip 中看到这里概述的所有解决方案的完整实现。
如果 AsyncTask 正在更新和改变状态,还需要进一步考虑。如果是这种情况,您可能希望使用后台服务,以便在进程被回收并在以后重新启动时可以重新启动它。这里介绍的方法对于快速到中等水平的阅读是足够的,因为您希望用户等待。但是,对于较长时间的读取或写入,您可能希望采用基于服务的解决方案。
参考
以下参考资料将帮助您了解本章中讨论的主题的更多信息:
developer . Android . com/reference/Android/OS/async task . html:明确记录 AsyncTask 行为的关键资源。- :再看一个乖巧的 AsyncTask 。
- :我们在准备本章时收集的关于 AsyncTask 的研究笔记。
- :Android 在其 API 中经常使用 Java 泛型。这个 URL 记录了 Java 泛型的一些基础知识,可以帮助您入门。
- :正如本章所展示的,要权威性地使用 AsyncTask 你需要了解很多关于活动生命周期、片段、它们的生命周期、无头片段、配置变更、片段对话框、AsyncTask、ADOs 等等。这个网址上有许多关注这些领域的文章。
- :ADO 是一种抽象,我们的一位作者支持它作为处理配置变更的便捷工具。这个 URL 记录了 ado 是什么以及如何使用它们,并且还提供了一个初步的实现。
- :这个 URL 记录了使用 ProgressBar 的背景、有用的 URL、代码片段和有用的提示。
- :这个 URL 对配置发生变化时的活动生命周期有很好的研究。
- :要写出在设备旋转时运行良好的程序是相当困难的。这个 URL 概述了一些基本的测试案例,您必须成功运行这些案例来验证 AsyncTask。
- 【http://www.androidbook.com/item/4673】:这个 URL 建议使用一种增强的模式来构造继承片段。
- :了解一个片段,包括一个留存的片段,最好的方法就是用心研究它的回调。这个 URL 提供了片段的所有重要回调的文档样本代码。
- :了解一个活动生命周期的最好方法是努力研究它的回调。这个 URL 提供了所有重要活动回调的文档样本代码。
- :这个 URL 概述了我们对片段对话框的研究。
- :这本书的可下载项目列表在这个 URL。对于这一章,寻找一个名为 proandroid 5 _ Ch15 _ testasynctask . zip 和 proandroid 5 _ Ch15 _ testasynctaskswithconfigchanges 的 zip 文件。后一个 zip 文件实现了行为良好的 AsyncTask 的三个解决方案。
摘要
在这一章中,除了介绍 AsyncTask 之外,我们还介绍了进度对话框、进度条、无头保留片段和 ado。阅读这一章,你不仅理解了 AsyncTask ,还能运用你对活动生命周期的理解和对片段的深刻理解。我们还记录了一组关键的测试案例,一个表现良好的 Android 应用必须满足这些案例。**