在上一篇學習安卓開發[4] - 使用隱式Intent啓動短信、聯繫人、相機應用中瞭解了在調用其它應用的功能時隱式Intent的使用,本次基於一個圖片瀏覽APP的開發,記錄使用AsyncTask在後臺執行HTTP任務以獲取圖片URL,而後使用HandlerThread動態下載和顯示圖片java
這裏使用java.net.HttpURLConnection來執行HTTP請求,GET請求的基本用法以下,默認執行的就是GET,因此能夠省略connection.setRequestMethod("GET"),connection.getInputStream()取得InputStream後,再循環執行read()方法將數據從流中取出、寫入ByteArrayOutputStream中,而後經過ByteArrayOutputStream.toByteArray返回爲Byte數組格式,最後轉換爲String。網上還有一種方法是使用BufferedReader.readLine()來逐行讀取輸入緩衝區的數據並寫入StringBuilder。對於POST方法,可使用getOutputStream()來寫入參數。json
public byte[] getUrlBytes(String urlSpec) throws IOException { URL url = new URL(urlSpec); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = connection.getInputStream(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new IOException(connection.getResponseMessage() + "with" + urlSpec); } int bytesRead = 0; byte[] buffer = new byte[1024]; while ((bytesRead = in.read(buffer)) > 0) { out.write(buffer, 0, bytesRead); } out.close(); return out.toByteArray(); } finally { connection.disconnect(); } } public String getUrlString(String urlSpec) throws IOException { return new String(getUrlBytes(urlSpec)); }
url爲百度的圖片接口,返回json格式數據,因此將API返回的json字符串轉換爲JSONObject,而後遍歷json數組,將其轉換爲指定的對象。數組
... String url = "http://image.baidu.com/channel/listjson?pn=0&rn=25&tag1=明星&ie=utf8"; String jsonString = getUrlString(url); JSONObject jsonBody = new JSONObject(jsonString); parseItems(items, jsonBody); ... private void parseItems(List<GalleryItem> items, JSONObject jsonObject) throws IOException, JSONException { JSONArray photoJsonArray = jsonObject.getJSONArray("data"); for (int i = 0; i < photoJsonArray.length() - 1; i++) { JSONObject photoJsonObject = photoJsonArray.getJSONObject(i); if (!photoJsonObject.has("id")) { continue; } GalleryItem item = new GalleryItem(); item.setId(photoJsonObject.getString("id")); item.setCaption(photoJsonObject.getString("desc")); item.setUrl(photoJsonObject.getString("image_url")); items.add(item); } }
HTTP相關的代碼準備好了,但沒法在Fragment類中被直接調用。由於網絡操做一般比較耗時,若是在主線程(UI線程)中直接操做,會致使界面無響應的現象發生。因此Android系統禁止任何主線程的網絡鏈接行爲,不然會報NewworkOnMainThreadException。 主線程不一樣於普通的線程,後者在完成預約的任務後便會終止,但主線程則處於無限循環的狀態,以等待用戶或系統的觸發事件。安全
至於網絡操做,正確的作法是建立一個後臺線程,在這個線程中進行。AsyncTask提供了使用後臺線程的簡便方法。代碼以下:網絡
private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> { @Override protected List<GalleryItem> doInBackground(Void... voids) { List<GalleryItem> items = new FlickrFetchr().fetchItems(); return items; } @Override protected void onPostExecute(List<GalleryItem> galleryItems) { mItems = galleryItems; setupAdapter(); } }
重寫了AsyncTask的doInBackground方法和onPostExecute方法,另外還有兩個方法可重寫,它們的做用分別是:併發
AsyncTask的三個泛型參數就是對應doInBackground(Params...)、onProgressUpdate(Progress...)、onPostExecute(Result)的,這裏設置爲ide
AsyncTask<Void, Void, List<GalleryItem>>
因此線程完成後返回的結果類型爲List<GalleryItem>。 後臺線程的啓動能夠在Fragment建立的時候執行:函數
@Override public void onCreate(@Nullable Bundle savedInstanceState) { ... new FetchItemsTask().execute(); }
前面經過AsyncTask建立的後臺線程獲取到了全部圖片的URL信息,接下來須要下載這些圖片並顯示到RecyclerView。但若是要在doInBackGround中直接下載這些圖片則是不合理的,這是由於:oop
public class ThumbnailDownloader<T> extends HandlerThread
這裏T設置爲以後ThumbnailDownloader的使用者,即PhotoHolder。post
在Fragment建立時啓動線程:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { ... mThumbnailDownloader.start(); mThumbnailDownloader.getLooper(); ... }
在Fragment銷燬時終止線程:
@Override public void onDestroy() { super.onDestroy(); mThumbnailDownloader.quit(); }
這一步是必要的,不然即便Fragment已被銷燬,線程也會一直運行下去。
先了解一下Message和Handler
給消息隊列發送的就是Message類的實例,Message類用戶須要定義這幾個變量:
handler是處理message的target,也是建立和發佈message的接口。而looper擁有message對象的收件箱,因此handler老是引用着looper,在looper上發佈或處理消息。handler與looper爲多對一關係;looper擁有整個message隊列,爲一對多關係;多個message可引用同一個handler,爲多對一關係。
調用Handler.obtainMessage方法建立消息,而不是手動建立,obtainMessage會從公共回收池中獲取消息,這樣作能夠避免反覆建立新的message對象,更加高效。獲取到message,隨後調用sendToTarget()將其發送給它的handler,handler會將這個message放置在looper消息隊列的尾部。這些操做在queueThumbnail中完成:
public void queueThumbnail(T target, String url) { Log.i(TAG, "Got a URL: " + url); if (url == null) { mRequestMap.remove(target); } else { mRequestMap.put(target, url); mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD, target) .sendToTarget(); } }
而後在RecyclerView的Adapter綁定holder的時候,調用queueThumbnail,將圖片url發送給後臺線程。
public class PhotoAdapter extends RecyclerView.Adapter<PhotoHolder> { ... @Override public void onBindViewHolder(PhotoHolder holder, int position) { ... mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl()); }
但後臺線程的消息隊列存放的不是url,而是對應的Holder,url存放在ConcurrentMap型的mRequestMap中,ConcurrentMap是一種線程安全的Map結構。存放了holder對對應url的map關係,這樣在消息隊列中處理某個holder時,能夠從mRequestMap拿到它的url。
private ConcurrentMap<T, String> mRequestMap
具體處理消息的動做經過重寫Handler.handleMessage方法實現。onLooperPrepared在Looper首次檢查消息隊列以前調用,因此在此能夠實例化handler並重寫handleMessage。下載圖片的實如今handleRequest方法中,將請求API拿到的byte[]數據轉換成bitmap。
public class ThumbnailDownloader<T> extends HandlerThread { ... @Override protected void onLooperPrepared() { mRequestHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MESSAGE_DOWNLOAD) { T target = (T) msg.obj; Log.i(TAG, "Get a request for URL: " + mRequestMap.get(target)); handleRequest(target); } } }; } private void handleRequest(final T target) { try { final String url = mRequestMap.get(target); if (url == null) { return; } byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length); Log.i(TAG, "Bitmap created"); mResponseHandler.post(new Runnable() { @Override public void run() { if(mRequestMap.get(target)!=url||mHasQuit){ return; } mRequestMap.remove(target); mThumbnailDownloadListener.onThumbnailDownload(target,bitmap); } }); } catch (IOException ioe) { Log.e(TAG, "Error downloading image", ioe); } }
下載獲得的Bitmap須要返回給UI線程的holder以顯示到屏幕。如何作呢?UI線程也是一個擁有handler和looper的消息循環。因此要返回結果給UI線程,就能夠反過來,從後臺線程使用主線程的handler。 那麼,後臺線程首先須要持有UI線程的handler:
public class PhotoGalleryFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { ... Handler responseHandler = new Handler(); mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler); ... }
ThumbnailDownloader的構造函數中接收UI線程的handler。圖片下載完成後就要向UI線程發佈message了,能夠經過Handler.post(Runnable)進行,重寫Runable.run()方法,不讓halder處理消息,而是在這裏觸發ThumbnailDownloadListener。
public class ThumbnailDownloader<T> extends HandlerThread { ... public interface ThumbnailDownloadListener<T>{ void onThumbnailDownload(T target, Bitmap thumbnail); } public void setThumbnailDownloadListener(ThumbnailDownloadListener<T> listener){ mThumbnailDownloadListener=listener; } public ThumbnailDownloader(Handler responseHandler) { super(TAG); mResponseHandler=responseHandler; } private void handleRequest(final T target) { ... mResponseHandler.post(new Runnable() { @Override public void run() { if(mRequestMap.get(target)!=url||mHasQuit){ return; } mRequestMap.remove(target); mThumbnailDownloadListener.onThumbnailDownload(target,bitmap); } }); ... } }
mThumbnailDownloadListener被觸發後,UI線程的註冊方法就會將後臺返回的圖片綁定到其Holder。
public class PhotoGalleryFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { ... mThumbnailDownloader.setThumbnailDownloadListener( new ThumbnailDownloader.ThumbnailDownloadListener<PhotoHolder>() { @Override public void onThumbnailDownload(PhotoHolder target, Bitmap thumbnail) { Drawable drawable = new BitmapDrawable(getResources(), thumbnail); target.bindDrawable(drawable); } } ); ... }
如此,後臺任務的執行與返回就完成了。