Android中AsyncTask使用詳解

孫群發表於2016-02-19

在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的使用過程。

佈局檔案如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button android:id="@+id/btnDownload"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="開始下載" />

    <TextView android:id="@+id/textView"
        android:layout_below="@id/btnDownload"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

介面上有一個“開始下載”的按鈕,點選該按鈕即可通過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, Progress, Result>
    //在此例中,Params泛型是String型別,Progress泛型是Object型別,Result泛型是Long型別
    private class DownloadTask extends AsyncTask<String, Object, Long> {
        @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("<title>");
                if(startIndex > 0){
                    startIndex += 7;
                    int endIndex = respone.indexOf("</title>");
                    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);
        }
    }
}

點選下載按鈕後,介面如下所示:
這裡寫圖片描述

控制檯輸出如下所示:
這裡寫圖片描述

下面對以上程式碼進行一下說明。

  1. 我們在MainActivity中定義了內部類DownloadTask,DownloadTask繼承自AsyncTask,在該例中,Params泛型是String型別,Progress泛型是Object型別,Result泛型是Long型別。

  2. 我們定義了一個Url字串陣列,將該陣列傳遞給AsyncTask的execute方法,用於非同步執行task。

  3. 在執行了downloadTask.execute(urls)之後,AsyncTask會自動回撥onPreExecute方法,在該方法中我們將textView設定為“開始下載…”幾個字,告知使用者即將執行下載操作。通過控制檯輸出我們也可以看出該方法是在主執行緒中執行的。

  4. 在執行了onPreExecute方法之後,AsyncTask會回撥doInBackground方法,該方法中的輸入引數是String型別的不定長陣列,此處的String就對應著Params泛型型別,我們在該方法中遍歷Url陣列,依次下載對應的檔案,當我們下載完一個檔案,就相當於我們階段性地完成了一部分任務,我們就通過呼叫publishProgress方法將階段性處理結果釋出出去。在此例中我們將階段性的處理結果定義為Object型別,即Progress泛型型別。通過控制檯輸出我們可以看出doInBackground方法是執行在新的工作執行緒”AsyncTask #1”中的,AsyncTask的工作執行緒都是以”AsyncTask #”然後加上數字作為名字。當所有檔案下載完成後,我們就可以通過totalSize返回所有下載的位元組數,返回值型別為Long,對應著AsyncTask中的Result泛型型別。

  5. 在doInBackground方法中,每當下載完一個檔案,我們就會呼叫publishProgress方法釋出階段性結果,之後AsyncTask會回撥onProgressUpdate方法,在此例中,onProgressUpdate的引數為Object型別,對應著AsyncTask中的Progress泛型型別。通過控制檯輸出我們可以發現,該方法是在主執行緒中呼叫的,在該方法中我們會通過textView更新UI,告知使用者哪個檔案下載完成了,這樣使用者體驗相對友好。

  6. 在整個doInBackground方法執行完畢後,AsyncTask就會回撥onPostExecute方法,在該方法中我們再次通過textView更新UI告知使用者全部下載任務完成了。

  7. 在通過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<Params, Progress, Result> 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的使用有所幫助!

相關閱讀:
我的Android博文整理彙總
原始碼解析Android中AsyncTask的工作原理
Android中Handler的使用
Android新執行緒中更新主執行緒UI中的View方法彙總

相關文章