经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
学习安卓开发[5] - HTTP、后台任务以及与UI线程的交互
来源:cnblogs  作者:zhixin9001  时间:2019/2/11 9:08:16  对本文有异议

在上一篇学习安卓开发[4] - 使用隐式Intent启动短信、联系人、相机应用中了解了在调用其它应用的功能时隐式Intent的使用,本次基于一个图片浏览APP的开发,记录使用AsyncTask在后台执行HTTP任务以获取图片URL,然后使用HandlerThread动态下载和显示图片

  • HTTP
    • 请求数据
    • 解析Json数据
  • AsyncTask
    • 主线程与后台线程
    • 后台线程的启动与结果返回
  • HandlerThread
    • AsyncTask不适用于批量下载图片
    • ThreadHandler的启动和注销
    • 创建并发送消息
    • 处理消息并返回结果

HTTP

请求数据

这里使用java.net.HttpURLConnection来执行HTTP请求,GET请求的基本用法如下,默认执行的就是GET,所以可以省略connection.setRequestMethod("GET"),connection.getInputStream()取得InputStream后,再循环执行read()方法将数据从流中取出、写入ByteArrayOutputStream中,然后通过ByteArrayOutputStream.toByteArray返回为Byte数组格式,最后转换为String。网上还有一种方法是使用BufferedReader.readLine()来逐行读取输入缓冲区的数据并写入StringBuilder。对于POST方法,可以使用getOutputStream()来写入参数。

  1. public byte[] getUrlBytes(String urlSpec) throws IOException {
  2. URL url = new URL(urlSpec);
  3. HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  4. try {
  5. ByteArrayOutputStream out = new ByteArrayOutputStream();
  6. InputStream in = connection.getInputStream();
  7. if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
  8. throw new IOException(connection.getResponseMessage() +
  9. "with" + urlSpec);
  10. }
  11. int bytesRead = 0;
  12. byte[] buffer = new byte[1024];
  13. while ((bytesRead = in.read(buffer)) > 0) {
  14. out.write(buffer, 0, bytesRead);
  15. }
  16. out.close();
  17. return out.toByteArray();
  18. } finally {
  19. connection.disconnect();
  20. }
  21. }
  22. public String getUrlString(String urlSpec) throws IOException {
  23. return new String(getUrlBytes(urlSpec));
  24. }
解析Json数据

url为百度的图片接口,返回json格式数据,所以将API返回的json字符串转换为JSONObject,然后遍历json数组,将其转换为指定的对象。

  1. ...
  2. String url = "http://image.baidu.com/channel/listjson?pn=0&rn=25&tag1=明星&ie=utf8";
  3. String jsonString = getUrlString(url);
  4. JSONObject jsonBody = new JSONObject(jsonString);
  5. parseItems(items, jsonBody);
  6. ...
  7. private void parseItems(List<GalleryItem> items, JSONObject jsonObject) throws IOException, JSONException {
  8. JSONArray photoJsonArray = jsonObject.getJSONArray("data");
  9. for (int i = 0; i < photoJsonArray.length() - 1; i++) {
  10. JSONObject photoJsonObject = photoJsonArray.getJSONObject(i);
  11. if (!photoJsonObject.has("id")) {
  12. continue;
  13. }
  14. GalleryItem item = new GalleryItem();
  15. item.setId(photoJsonObject.getString("id"));
  16. item.setCaption(photoJsonObject.getString("desc"));
  17. item.setUrl(photoJsonObject.getString("image_url"));
  18. items.add(item);
  19. }
  20. }

AsyncTask

主线程与后台线程

HTTP相关的代码准备好了,但无法在Fragment类中被直接调用。因为网络操作通常比较耗时,如果在主线程(UI线程)中直接操作,会导致界面无响应的现象发生。所以Android系统禁止任何主线程的网络连接行为,否则会报NewworkOnMainThreadException。
主线程不同于普通的线程,后者在完成预定的任务后便会终止,但主线程则处于无限循环的状态,以等待用户或系统的触发事件。

后台线程的启动与结果返回

至于网络操作,正确的做法是创建一个后台线程,在这个线程中进行。AsyncTask提供了使用后台线程的简便方法。代码如下:

  1. private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> {
  2. @Override
  3. protected List<GalleryItem> doInBackground(Void... voids) {
  4. List<GalleryItem> items = new FlickrFetchr().fetchItems();
  5. return items;
  6. }
  7. @Override
  8. protected void onPostExecute(List<GalleryItem> galleryItems) {
  9. mItems = galleryItems;
  10. setupAdapter();
  11. }
  12. }

重写了AsyncTask的doInBackground方法和onPostExecute方法,另外还有两个方法可重写,它们的作用分别是:

  • onPreExecute(), 在后台操作开始前被UI线程调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条,或者一些控件的实例化,这个方法可以不用实现。
  • doInBackground(Params...), 将在onPreExecute 方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台处理工作。可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。
  • onProgressUpdate(Progress...),在publishProgress方法被调用后,UI 线程将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。
  • onPostExecute(Result), 在doInBackground 执行完成后,onPostExecute 方法将被UI 线程调用,后台的计算结果将通过该方法传递到UI 线程,并且在界面上展示给用户
  • onCancelled(),在用户取消线程操作的时候调用。在主线程中调用onCancelled()的时候调用

AsyncTask的三个泛型参数就是对应doInBackground(Params...)、onProgressUpdate(Progress...)、onPostExecute(Result)的,这里设置为

  1. AsyncTask<Void, Void, List<GalleryItem>>

所以线程完成后返回的结果类型为List

  1. @Override
  2. public void onCreate(@Nullable Bundle savedInstanceState) {
  3. ...
  4. new FetchItemsTask().execute();
  5. }

HandlerThread

AsyncTask不适用于批量下载图片

前面通过AsyncTask创建的后台线程获取到了所有图片的URL信息,接下来需要下载这些图片并显示到RecyclerView。但如果要在doInBackGround中直接下载这些图片则是不合理的,这是因为:

  • 图片下载比较耗时,如果要下载的图片较多,需要等这些图片都下载成功后才去更新UI,体验很差。
  • 下载的图片还涉及到保存的问题,数量较大的图片不宜直接存放在内存,而且如果要实现无限滚动来显示图片,内存很快就会耗尽
    所以对于类似这种重复且数量较大、耗时较长的任务来说,AsyncView便不再适合了。
    换一种实现方式,既然用RecyclerView显示图片,在加载每个Holder时,单独下载对应的图片,这样便不会存在前面的问题了,于是该是HandlerThread登场的时候了,HandlerThread使用消息队列工作,这种使用消息队列的线程也叫消息循环,消息队列由线程和looper组成,looper对象管理着线程的消息队列,会循环检查队列上是否有新消息。
    创建继承了HandlerThread的ThumbnailDownloader:
  1. public class ThumbnailDownloader<T> extends HandlerThread

这里T设置为之后ThumbnailDownloader的使用者,即PhotoHolder。

ThreadHandler的启动和注销

在Fragment创建时启动线程:

  1. @Override
  2. public void onCreate(@Nullable Bundle savedInstanceState) {
  3. ...
  4. mThumbnailDownloader.start();
  5. mThumbnailDownloader.getLooper();
  6. ...
  7. }

在Fragment销毁时终止线程:

  1. @Override
  2. public void onDestroy() {
  3. super.onDestroy();
  4. mThumbnailDownloader.quit();
  5. }

这一步是必要的,否则即使Fragment已被销毁,线程也会一直运行下去。

创建并发送消息

先了解一下Message和Handler

Message

给消息队列发送的就是Message类的实例,Message类用户需要定义这几个变量:

  • what, 用户自定义的int型消息标识代码
  • obj,随消息发送的对象
  • target, 处理消息的handler
    target是一个handler类实例,创建的message会自动与一个Handler关联,message待处理时,handler对象负责触发消息事件

    Handler

    handler是处理message的target,也是创建和发布message的接口。而looper拥有message对象的收件箱,所以handler总是引用着looper,在looper上发布或处理消息。handler与looper为多对一关系;looper拥有整个message队列,为一对多关系;多个message可引用同一个handler,为多对一关系。

    使用Handler
    调用Handler.obtainMessage方法创建消息,而不是手动创建,obtainMessage会从公共回收池中获取消息,这样做可以避免反复创建新的message对象,更加高效。获取到message,随后调用sendToTarget()将其发送给它的handler,handler会将这个message放置在looper消息队列的尾部。这些操作在queueThumbnail中完成:
  1. public void queueThumbnail(T target, String url) {
  2. Log.i(TAG, "Got a URL: " + url);
  3. if (url == null) {
  4. mRequestMap.remove(target);
  5. } else {
  6. mRequestMap.put(target, url);
  7. mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
  8. .sendToTarget();
  9. }
  10. }

然后在RecyclerView的Adapter绑定holder的时候,调用queueThumbnail,将图片url发送给后台线程。

  1. public class PhotoAdapter extends RecyclerView.Adapter<PhotoHolder> {
  2. ...
  3. @Override
  4. public void onBindViewHolder(PhotoHolder holder, int position) {
  5. ...
  6. mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
  7. }

但后台线程的消息队列存放的不是url,而是对应的Holder,url存放在ConcurrentMap型的mRequestMap中,ConcurrentMap是一种线程安全的Map结构。存放了holder对对应url的map关系,这样在消息队列中处理某个holder时,可以从mRequestMap拿到它的url。

  1. private ConcurrentMap<T, String> mRequestMap
处理消息并返回结果
消息的处理

具体处理消息的动作通过重写Handler.handleMessage方法实现。onLooperPrepared在Looper首次检查消息队列之前调用,所以在此可以实例化handler并重写handleMessage。下载图片的实现在handleRequest方法中,将请求API拿到的byte[]数据转换成bitmap。

  1. public class ThumbnailDownloader<T> extends HandlerThread {
  2. ...
  3. @Override
  4. protected void onLooperPrepared() {
  5. mRequestHandler = new Handler() {
  6. @Override
  7. public void handleMessage(Message msg) {
  8. if (msg.what == MESSAGE_DOWNLOAD) {
  9. T target = (T) msg.obj;
  10. Log.i(TAG, "Get a request for URL: " + mRequestMap.get(target));
  11. handleRequest(target);
  12. }
  13. }
  14. };
  15. }
  16. private void handleRequest(final T target) {
  17. try {
  18. final String url = mRequestMap.get(target);
  19. if (url == null) {
  20. return;
  21. }
  22. byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
  23. final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
  24. Log.i(TAG, "Bitmap created");
  25. mResponseHandler.post(new Runnable() {
  26. @Override
  27. public void run() {
  28. if(mRequestMap.get(target)!=url||mHasQuit){
  29. return;
  30. }
  31. mRequestMap.remove(target);
  32. mThumbnailDownloadListener.onThumbnailDownload(target,bitmap);
  33. }
  34. });
  35. } catch (IOException ioe) {
  36. Log.e(TAG, "Error downloading image", ioe);
  37. }
  38. }
结果的返回

下载得到的Bitmap需要返回给UI线程的holder以显示到屏幕。如何做呢?UI线程也是一个拥有handler和looper的消息循环。所以要返回结果给UI线程,就可以反过来,从后台线程使用主线程的handler。
那么,后台线程首先需要持有UI线程的handler:

  1. public class PhotoGalleryFragment extends Fragment {
  2. @Override
  3. public void onCreate(@Nullable Bundle savedInstanceState) {
  4. ...
  5. Handler responseHandler = new Handler();
  6. mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);
  7. ...
  8. }

ThumbnailDownloader的构造函数中接收UI线程的handler。图片下载完成后就要向UI线程发布message了,可以通过Handler.post(Runnable)进行,重写Runable.run()方法,不让halder处理消息,而是在这里触发ThumbnailDownloadListener。

  1. public class ThumbnailDownloader<T> extends HandlerThread {
  2. ...
  3. public interface ThumbnailDownloadListener<T>{
  4. void onThumbnailDownload(T target, Bitmap thumbnail);
  5. }
  6. public void setThumbnailDownloadListener(ThumbnailDownloadListener<T> listener){
  7. mThumbnailDownloadListener=listener;
  8. }
  9. public ThumbnailDownloader(Handler responseHandler) {
  10. super(TAG);
  11. mResponseHandler=responseHandler;
  12. }
  13. private void handleRequest(final T target) {
  14. ...
  15. mResponseHandler.post(new Runnable() {
  16. @Override
  17. public void run() {
  18. if(mRequestMap.get(target)!=url||mHasQuit){
  19. return;
  20. }
  21. mRequestMap.remove(target);
  22. mThumbnailDownloadListener.onThumbnailDownload(target,bitmap);
  23. }
  24. });
  25. ...
  26. }
  27. }

mThumbnailDownloadListener被触发后,UI线程的注册方法就会将后台返回的图片绑定到其Holder。

  1. public class PhotoGalleryFragment extends Fragment {
  2. @Override
  3. public void onCreate(@Nullable Bundle savedInstanceState) {
  4. ...
  5. mThumbnailDownloader.setThumbnailDownloadListener(
  6. new ThumbnailDownloader.ThumbnailDownloadListener<PhotoHolder>() {
  7. @Override
  8. public void onThumbnailDownload(PhotoHolder target, Bitmap thumbnail) {
  9. Drawable drawable = new BitmapDrawable(getResources(), thumbnail);
  10. target.bindDrawable(drawable);
  11. }
  12. }
  13. );
  14. ...
  15. }

如此,后台任务的执行与返回就完成了。

原文链接:http://www.cnblogs.com/zhixin9001/p/10349167.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号