Android AsyncTask 詳解

zYinux發表於2018-10-27

Android AsyncTask 詳解

內容劃分

  • AsyncTask簡介
  • 簡單使用
  • 繁雜部分和原始碼淺析
  • 一些坑的地方

AsyncTask簡介

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.

這是Google Android 開發文件上關於AsyncTask的介紹,大概意思是AsyncTask設計為一個對於Thread和Handle的輔助類,主要讓開發者方便的使用UI Thread和後臺Thread的操作( 比如在後臺執行緒下載檔案,同時要在UI執行緒更新下載進度 )。同時這不是一個通用的多執行緒程式設計框架,他被設計為用於能夠在 最多幾秒的時間內返回結果的任務。

簡單使用

這裡我們模擬一個後臺下載一些檔案,並在使用者介面顯示一個ProgressDialog來顯示下載進度的功能。

/**
 * author: zyinux
 * date: on 2018/10/26
 */
public class DownloadTask extends AsyncTask<String,Integer,Boolean> {

    ProgressDialog progressDialog;

    Context context;

    public DownloadTask(Context context) {
        this.context=context;

    }

    @Override
    protected void onPreExecute() {
        progressDialog=new ProgressDialog(context);
        progressDialog.setMax(100);
    // progressDialog.setCancelable(false);
    //注意這裡我將上一行程式碼註釋掉,使得dialog能夠被取消,至於為什麼這麼做後面解釋
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(String... strings) {
        int pro=0;

        while (true){
            pro++;
            publishProgress(pro);
            if (pro>=100){
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        progressDialog.setProgress(values[0]);
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        progressDialog.dismiss();
        if (aBoolean){
            Toast.makeText(context,"下載成功",Toast.LENGTH_SHORT).show();
        }else {
            Toast.makeText(context,"下載失敗",Toast.LENGTH_SHORT).show();
        }
    }
}
複製程式碼

下面是Activity中呼叫的主要程式碼

new DownloadTask(this).execute("testurl");
//使用非常簡單,new 之後執行execute傳入執行的引數即可
複製程式碼

執行效果如圖

Android AsyncTask 詳解
很普通的效果,下面來分析下上面的程式碼 首先是

public class DownloadTask extends AsyncTask<String,Integer,Boolean>
複製程式碼

這一行泛型尖括號裡的三個型別,具體對應三個

  • Params 執行時傳送給任務的引數的型別
  • Progress 後臺執行過程進度的型別
  • Result 執行結果返回值的型別 當不需要這些引數的時候可以設定為<Void,Void,Void> 接著是實現的幾個主要方法
protected void onPreExecute() {}
protected Boolean doInBackground(String... strings) {}
protected void onProgressUpdate(Integer... values) {}
protected void onPostExecute(Boolean aBoolean) { }
複製程式碼
  • onPreExecute 方法執行在UI執行緒,會在做後臺任務之前呼叫,可以在這裡執行一些初始化操作,例如上面的顯示Dialog
  • doInBackground 改方法執行在後臺執行緒,任務中的耗時操作都應該在這裡執行,AsyncTask內部維持了一個執行緒池,來對該方法呼叫執行優化。該方法的引數型別就是上面設定的 Params ,也就是執行呼叫程式碼中execute裡傳遞來的引數。在該方法內部可以呼叫publishProgress方法來傳遞當前的進度。
  • onProgressUpdate 在publishProgress方法後,系統會呼叫該方法,該方法執行在UI Thread,所以可以在這裡做UI更新的操作,比如更新ProgressDialog的進度。這裡傳遞的引數的型別就是上文裡的 Progress。
  • onPostExecute 在doInBackground方法執行完成後會執行該方法,同樣執行在UI Thread。而傳入的引數就是doInBackground方法的返回值,該型別由上文的Result指定。

繁雜部分和原始碼淺析

上面基本講解了AsyncTask的使用方法了。細心的小夥伴可能注意到我上面的這兩句程式碼

// progressDialog.setCancelable(false);
//注意這裡我將上一行程式碼註釋掉,使得dialog能夠被取消,至於為什麼這麼做後面解釋
複製程式碼

現在來解釋這裡這麼寫的原因,假設我們執行app,並執行DownloadTask,這時候螢幕上彈出一個進度框,目前為止一切都沒有問題。這個時候我們點選螢幕的其他地方,進度框會被取消掉,接著我們再次執行DownloadTask,小夥伴們猜猜現在會發生什麼?

由於不太方便錄屏和傳gif圖,我這裡就簡單說下會發生的事情:進度框會再度彈出,這沒什麼問題,但是進度條會停留在0%不動,直到一段時間之後彈出Toast顯示下載完成,接著進度條開始慢慢增加,當達到百分之百時再次彈出Toast提示下載完成。

###為什麼會這樣? 首先我們知道,取消dialog並不會取消掉AsyncTask,所以再次執行DownloadTask時,相當於此時有兩個AsyncTask任務在執行。這就引出了一個問題,多個AsyncTask執行時是序列還是並行?

序列還是並行?

先說答案,預設是序列的,為什麼,我們來看原始碼。 當執行 new DownloadTask(this).execute("testurl"); 後:

    @MainThread
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    接著繼續看

    @MainThread
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        //看這裡我們直到onPreExecute方法一定在doInBackground方法之前呼叫,並且是在UI Thread
        onPreExecute();
        /**
        *具體執行方法在這裡 我們直到這個exec就是上一步傳進來的sDefaultExecutor 
        *這是一個用來管理執行緒池的框架
        */
        exec.execute(mFuture);
        return this;
    }

    初始化的地方,重點這裡初始化為static final 是一個類靜態變數,是類例項共享的
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();    

    繼續看這個SerialExecutor
    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

    將任務儲存在ArrayDeque中,這是一個FIFO的佇列,最後執行這個佇列中的每一個任務。所以當執行多個AsyncTask時,他們是序列執行的。
複製程式碼
上面說了這時一般情況,那麼特殊情況呢?
複製程式碼
DownloadTask task=new DownloadTask(this);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
複製程式碼

這時候多個任務就不是一定排隊按順序執行了,具體執行順序要看系統對執行緒的排程了,小夥伴們可以自己測試一下看看。不過一般不推薦這麼使用,除非你有特殊需求。

一些坑的地方

關於cancel方法

public final boolean cancel(boolean mayInterruptIfRunning) {}
複製程式碼

傳入的參數列示當前任務執行時是否可以取消。但是當你的doInBackground方法中執行一個迴圈或者一個IO流讀寫任務,即使你傳入了true,改方法也無法取消這個任務的執行。區別在於呼叫這個方法後,doInBackground執行完成時會呼叫onCancelled方法,而不是onPostExecute方法,所以cancel無法保證任務能夠被取消

記憶體洩漏

上面的示列程式碼從Activity中傳入了一個context。而AsyncTask的生命週期和Activity是無關的,那麼當Activity被finish後,AsyncTask依然存在,而他持有著Activity的引用導致Activity無法被垃圾回收。 同時如果使用了非靜態匿名內部類來實現AsyncTask,由於Java內部類的特點,他同樣會持有當前Activity的引用造成記憶體洩漏。

相關文章