學習安卓開發[5] - HTTP、後臺任務以及與UI執行緒的互動
在上一篇學習安卓開發[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()來寫入引數。
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)); }
解析Json資料
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); } }
AsyncTask
主執行緒與後臺執行緒
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方法,另外還有兩個方法可重寫,它們的作用分別是:
- 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)的,這裡設定為
AsyncTask<Void, Void, List<GalleryItem>>
所以執行緒完成後返回的結果型別為List<GalleryItem>。
後臺執行緒的啟動可以在Fragment建立的時候執行:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { ... new FetchItemsTask().execute(); }
HandlerThread
AsyncTask不適用於批量下載圖片
前面通過AsyncTask建立的後臺執行緒獲取到了所有圖片的URL資訊,接下來需要下載這些圖片並顯示到RecyclerView。但如果要在doInBackGround中直接下載這些圖片則是不合理的,這是因為:
- 圖片下載比較耗時,如果要下載的圖片較多,需要等這些圖片都下載成功後才去更新UI,體驗很差。
-
下載的圖片還涉及到儲存的問題,數量較大的圖片不宜直接存放在記憶體,而且如果要實現無限滾動來顯示圖片,記憶體很快就會耗盡
所以對於類似這種重複且數量較大、耗時較長的任務來說,AsyncView便不再適合了。
換一種實現方式,既然用RecyclerView顯示圖片,在載入每個Holder時,單獨下載對應的圖片,這樣便不會存在前面的問題了,於是該是HandlerThread登場的時候了,HandlerThread使用訊息佇列工作,這種使用訊息佇列的執行緒也叫訊息迴圈,訊息佇列由執行緒和looper組成,looper物件管理著執行緒的訊息佇列,會迴圈檢查佇列上是否有新訊息。
建立繼承了HandlerThread的ThumbnailDownloader:
public class ThumbnailDownloader<T> extends HandlerThread
這裡T設定為之後ThumbnailDownloader的使用者,即PhotoHolder。
ThreadHandler的啟動和登出
在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類的例項,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中完成:
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); } } ); ... }
如此,後臺任務的執行與返回就完成了。