Android中AsyncTask的使用詳解
在Android中我們可以通過Thread+Handler實現多線程通訊,一種經典的使用情境是:在新線程中進行耗時操作,當任務完成後通過Handler向主線程發送Message,這樣主線程的Handler在收到該Message之後就可以進行更新UI的操作。上述情境中需要分別在Thread和Handler中編寫代碼邏輯,為了使得代碼更加統一,我們可以使用AsyncTask類。
AsyncTask是Android提供的一個助手類,它對Thread和Handler進行了封裝,方便我們使用。Android之所以提供AsyncTask這個類,就是為了方便我們在後台線程中執行操作,然後將結果發送給主線程,從而在主線程中進行UI更新等操作。在使用AsyncTask時,我們無需關注Thread和Handler,AsyncTask內部會對其進行管理,這樣我們就只需要關注於我們的商務邏輯即可。
AsyncTask有四個重要的回調方法,分別是:onPreExecute、doInBackground, onProgressUpdate 和 onPostExecute。這四個方法會在AsyncTask的不同時期進行自動調用,我們只需要實現這幾個方法的內部邏輯即可。這四個方法的一些參數和傳回值都是基於泛型的,而且泛型的類型還不一樣,所以在AsyncTask的使用中會遇到三種泛型參數:Params, Progress 和 Result,如所示:
Params表示用於AsyncTask執行任務的參數的類型 Progress表示在後台線程處理的過程中,可以階段性地發布結果的資料類型 Result表示任務全部完成後所返回的資料類型
我們通過調用AsyncTask的execute()方法傳入參數並執行任務,然後AsyncTask會依次調用以下四個方法:
onPreExecute
該方法的簽名如下所示:
該方法有MainThread註解,表示該方法是運行在主線程中的。在AsyncTask執行了execute()方法後就會在UI線程上執行onPreExecute()方法,該方法在task真正執行前運行,我們通常可以在該方法中顯示一個進度條,從而告知使用者背景工作即將開始。
doInBackground
該方法的簽名如下所示:
該方法有WorkerThread註解,表示該方法是運行在單獨的背景工作執行緒中的,而不是運行在主線程中。doInBackground會在onPreExecute()方法執行完成後立即執行,該方法用於在背景工作執行緒中執行耗時任務,我們可以在該方法中編寫我們需要在後台線程中啟動並執行邏輯代碼,由於是運行在背景工作執行緒中,所以該方法不會阻塞UI線程。該方法接收Params泛型參數,參數params是Params類型的不定長數組,該方法的傳回值是Result泛型,由於doInBackgroud是抽象方法,我們在使用AsyncTask時必須重寫該方法。在doInBackground中執行的任務可能要分解為好多步驟,每完成一步我們就可以通過調用AsyncTask的publishProgress(Progress…)將階段性的處理結果發布出去,階段性處理結果是Progress泛型型別。當調用了publishProgress方法後,處理結果會被傳遞到UI線程中,並在UI線程中回調onProgressUpdate方法,下面會詳細介紹。根據我們的具體需要,我們可以在doInBackground中不調用publishProgress方法,當然也可以在該方法中多次調用publishProgress方法。doInBackgroud方法的傳回值表示後台線程完成任務之後的結果。
onProgressUpdate
上面我們知道,當我們在doInBackground中調用publishProgress(Progress…)方法後,就會在UI線程上回調onProgressUpdate方法,該方法的方法簽名如下所示:
該方法也具有MainThread註解,表示該方法是在主線程上被調用的,且傳入的參數是Progress泛型定義的不定長數組。如果在doInBackground中多次調用了publishProgress方法,那麼主線程就會多次回調onProgressUpdate方法。
onPostExecute
該方法的簽名如下所示:
該方法也具有MainThread註解,表示該方法是在主線程中被調用的。當doInBackgroud方法執行完畢後,就表示任務完成了,doInBackgroud方法的傳回值就會作為參數在主線程中傳入到onPostExecute方法中,這樣就可以在主線程中根據任務的執行結果更新UI。
下面我們就以下載多個檔案的樣本示範AsyncTask的使用過程。
布局檔案如下所示:
介面上有一個“開始下載”的按鈕,點擊該按鈕即可通過AsyncTask下載多個檔案,對應的Java代碼如下所示:
package com.ispring.asynctask;import android.app.Activity;import android.os.AsyncTask;import android.os.Build;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.TextView;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;public class MainActivity extends Activity implements Button.OnClickListener { TextView textView = null; Button btnDownload = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.textView); btnDownload = (Button)findViewById(R.id.btnDownload); Log.i("iSpring", "MainActivity -> onCreate, Thread name: " + Thread.currentThread().getName()); } @Override public void onClick(View v) { //要下載的檔案地址 String[] urls = { "http://blog.csdn.net/iispring/article/details/47115879", "http://blog.csdn.net/iispring/article/details/47180325", "http://blog.csdn.net/iispring/article/details/47300819", "http://blog.csdn.net/iispring/article/details/47320407", "http://blog.csdn.net/iispring/article/details/47622705" }; DownloadTask downloadTask = new DownloadTask(); downloadTask.execute(urls); } //public abstract class AsyncTask //在此例中,Params泛型是String類型,Progress泛型是Object類型,Result泛型是Long類型 private class DownloadTask extends AsyncTask { @Override protected void onPreExecute() { Log.i("iSpring", "DownloadTask -> onPreExecute, Thread name: " + Thread.currentThread().getName()); super.onPreExecute(); btnDownload.setEnabled(false); textView.setText("開始下載..."); } @Override protected Long doInBackground(String... params) { Log.i("iSpring", "DownloadTask -> doInBackground, Thread name: " + Thread.currentThread().getName()); //totalByte表示所有下載的檔案的總位元組數 long totalByte = 0; //params是一個String數組 for(String url: params){ //遍曆Url數組,依次下載對應的檔案 Object[] result = downloadSingleFile(url); int byteCount = (int)result[0]; totalByte += byteCount; //在下載完一個檔案之後,我們就把階段性的處理結果發布出去 publishProgress(result); //如果AsyncTask被調用了cancel()方法,那麼任務取消,跳出for迴圈 if(isCancelled()){ break; } } //將總共下載的位元組數作為結果返回 return totalByte; } //下載檔案後返回一個Object數組:下載檔案的位元組數以及下載的部落格的名字 private Object[] downloadSingleFile(String str){ Object[] result = new Object[2]; int byteCount = 0; String blogName = ""; HttpURLConnection conn = null; try{ URL url = new URL(str); conn = (HttpURLConnection)url.openConnection(); InputStream is = conn.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int length = -1; while ((length = is.read(buf)) != -1) { baos.write(buf, 0, length); byteCount += length; } String respone = new String(baos.toByteArray(), "utf-8"); int startIndex = respone.indexOf(""); if(endIndex > startIndex){ //解析出部落格中的標題 blogName = respone.substring(startIndex, endIndex); } } }catch(MalformedURLException e){ e.printStackTrace(); }catch(IOException e){ e.printStackTrace(); }finally { if(conn != null){ conn.disconnect(); } } result[0] = byteCount; result[1] = blogName; return result; } @Override protected void onProgressUpdate(Object... values) { Log.i("iSpring", "DownloadTask -> onProgressUpdate, Thread name: " + Thread.currentThread().getName()); super.onProgressUpdate(values); int byteCount = (int)values[0]; String blogName = (String)values[1]; String text = textView.getText().toString(); text += "\n部落格《" + blogName + "》下載完成,共" + byteCount + "位元組"; textView.setText(text); } @Override protected void onPostExecute(Long aLong) { Log.i("iSpring", "DownloadTask -> onPostExecute, Thread name: " + Thread.currentThread().getName()); super.onPostExecute(aLong); String text = textView.getText().toString(); text += "\n全部下載完成,總共下載了" + aLong + "個位元組"; textView.setText(text); btnDownload.setEnabled(true); } @Override protected void onCancelled() { Log.i("iSpring", "DownloadTask -> onCancelled, Thread name: " + Thread.currentThread().getName()); super.onCancelled(); textView.setText("取消下載"); btnDownload.setEnabled(true); } }}
點擊下載按鈕後,介面如下所示:
控制台輸出如下所示:
下面對以上代碼進行一下說明。
我們在MainActivity中定義了內部類DownloadTask,DownloadTask繼承自AsyncTask,在該例中,Params泛型是String類型,Progress泛型是Object類型,Result泛型是Long類型。
我們定義了一個Url字串數組,將該數組傳遞給AsyncTask的execute方法,用於非同步執行task。
在執行了downloadTask.execute(urls)之後,AsyncTask會自動回調onPreExecute方法,在該方法中我們將textView設定為“開始下載…”幾個字,告知使用者即將執行下載操作。通過控制台輸出我們也可以看出該方法是在主線程中執行的。
在執行了onPreExecute方法之後,AsyncTask會回調doInBackground方法,該方法中的輸入參數是String類型的不定長數組,此處的String就對應著Params泛型型別,我們在該方法中遍曆Url數組,依次下載對應的檔案,當我們下載完一個檔案,就相當於我們階段性地完成了一部分任務,我們就通過調用publishProgress方法將階段性處理結果發布出去。在此例中我們將階段性的處理結果定義為Object類型,即Progress泛型型別。通過控制台輸出我們可以看出doInBackground方法是運行在新的背景工作執行緒”AsyncTask #1”中的,AsyncTask的背景工作執行緒都是以”AsyncTask #”然後加上數字作為名字。當所有檔案下載完成後,我們就可以通過totalSize返回所有下載的位元組數,傳回值類型為Long,對應著AsyncTask中的Result泛型型別。
在doInBackground方法中,每當下載完一個檔案,我們就會調用publishProgress方法發布階段性結果,之後AsyncTask會回調onProgressUpdate方法,在此例中,onProgressUpdate的參數為Object類型,對應著AsyncTask中的Progress泛型型別。通過控制台輸出我們可以發現,該方法是在主線程中調用的,在該方法中我們會通過textView更新UI,告知使用者哪個檔案下載完成了,這樣使用者體驗相對友好。
在整個doInBackground方法執行完畢後,AsyncTask就會回調onPostExecute方法,在該方法中我們再次通過textView更新UI告知使用者全部下載任務完成了。
在通過execute方法執行了非同步任務之後,可以通過AsyncTask的cancel方法取消任務,取消任務後AsyncTask會回調onCancelled方法,這樣不會再調用onPostExecute方法。
在使用Android的過程中,有以下幾點需要注意:
AsyncTask的執行個體必須在主線程中建立。
AsyncTask的execute方法必須在主線程中調用。
onPreExecute()、onPostExecute(Result),、doInBackground(Params…) 和 onProgressUpdate(Progress…)這四個方法都是回調方法,Android會自動調用,我們不應自己調用。
對於一個AsyncTack的執行個體,只能執行一次execute方法,在該執行個體上第二次執行execute方法時就會拋出異常。
通過上面的樣本,大家應該熟悉了AsyncTask的使用流程。我們上面提到,對於某個AsyncTask執行個體,只能執行一次execute方法,如果我們想並行地執行多個任務怎麼辦呢?我們可以考慮執行個體化多個AsyncTask執行個體,然後分別調用各個執行個體的execute方法,為了探究效果,我們將代碼更改如下所示:
public void onClick(View v) { //要下載的檔案地址 String[] urls = { "http://blog.csdn.net/iispring/article/details/47115879", "http://blog.csdn.net/iispring/article/details/47180325", "http://blog.csdn.net/iispring/article/details/47300819", "http://blog.csdn.net/iispring/article/details/47320407", "http://blog.csdn.net/iispring/article/details/47622705" }; DownloadTask downloadTask1 = new DownloadTask(); downloadTask1.execute(urls); DownloadTask downloadTask2 = new DownloadTask(); downloadTask2.execute(urls); }
在單擊了按鈕之後,我們執行個體化了兩個DownloadTask,並分別執行其execute方法,運行後介面如下所示:
控制台輸出如下所示:
我們觀察一下控制台的輸出結果,可以發現對於downloadTask1,doInBackground方法是運行線上程“AsyncTask #1”中的;對於downloadTask2,doInBackground方法是運行線上程”AsyncTask #2”中的,此時我們可能會認為太好了,兩個AsyncTask執行個體分別在不同的線程中運行,實現了平行處理。此處真的是並行啟動並執行嗎?
我們自己觀察控制台輸出就可以發現,downloadTask1的doInBackground方法執行後,下載了五個檔案,並五次觸發了onProgressUpdate,在這之後才執行downloadTask2的doInBackground方法。我們對比上面的GIF圖也可以發現,在downloadTask1按照順序下載完五篇文章之後,downloadTask2才開始按照順序下載五篇文章。綜上所述,我們可以知道,預設情況下如果建立了AsyncTask建立了多個執行個體,並同時執行執行個體的各個execute方法,那麼這些執行個體的execute方法並不是並存執行的,是串列執行的,即在第一個執行個體的doInBackground完成任務後,第二個執行個體的doInBackgroud方法才會開始執行,然後再執行第三個執行個體的doInBackground方法… 那麼你可能會問,不對啊,上面downloadTask1是運行在”AsyncTask #1”線程中的,downloadTask2是運行在”AsyncTask #2”線程中的,這明明是兩個線程啊!其實AsyncTask為downloadTask1開闢了名為”AsyncTask #1”的背景工作執行緒,在其完成了任務之後可能就銷毀了,然後AsyncTask又為downloadTask2開闢了名為”AsyncTask #2”的背景工作執行緒。
AsyncTask在最早的版本中用一個單一的後台線程串列執行多個AsyncTask執行個體的任務,從Android 1.6(DONUT)開始,AsyncTask用線程池並存執行非同步任務,但是從Android 3.0(HONEYCOMB)開始為了避免並存執行導致的常見錯誤,AsyncTask又開始預設用單線程作為背景工作執行緒處理多個任務。
從Android 3.0開始AsyncTask增加了executeOnExecutor方法,用該方法可以讓AsyncTask平行處理任務,該方法的方法簽名如下所示:
public final AsyncTask executeOnExecutor (Executor exec, Params... params)
第一個參數表示exec是一個Executor對象,為了讓AsyncTask平行處理任務,通常情況下我們此處傳入AsyncTask.THREAD_POOL_EXECUTOR即可,AsyncTask.THREAD_POOL_EXECUTOR是AsyncTask中內建的一個線程池對象,當然我們也可以傳入我們自己執行個體化的線程池對象。第二個參數params表示的是要執行的任務的參數。
通過executeOnExecutor方法並存執行任務的範例程式碼如下所示:
public void onClick(View v) { if(Build.VERSION.SDK_INT >= 11){ String[] urls = { "http://blog.csdn.net/iispring/article/details/47115879", "http://blog.csdn.net/iispring/article/details/47180325", "http://blog.csdn.net/iispring/article/details/47300819", "http://blog.csdn.net/iispring/article/details/47320407", "http://blog.csdn.net/iispring/article/details/47622705" }; DownloadTask downloadTask1 = new DownloadTask(); downloadTask1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, urls); DownloadTask downloadTask2 = new DownloadTask(); downloadTask2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, urls); } }
我們執行個體化了兩個DownloadTask的執行個體,然後執行了這兩個執行個體的executeOnExecutor方法,並將AsyncTask.THREAD_POOL_EXECUTOR作為Executor傳入,二者都接收同樣的Url數組作為任務執行的參數。
點擊下載按鈕後,運行完的介面如下所示:
控制台輸出如下所示:
通過控制台的輸出結果我們可以看到,在downloadTask1執行了doInBackground方法後,downloadTask2也立即執行了doInBackground方法。並且通過程式運行完的UI介面可以看到在一個DownloadTask執行個體下載了一篇文章之後,另一個DownloadTask執行個體也立即下載了一篇文章,兩個DownloadTask執行個體交叉按順序下載檔案,可以看出這兩個AsyncTask的執行個體是並存執行的。
如果大家想瞭解AsyncTask的工作原理,可參見另一篇博文《源碼解析Android中AsyncTask的工作原理》 。
希望本文對大家使用AsyncTask的使用有所協助!