Android 非同步載入——AsyncTask詳談

iamxiarui_發表於2018-07-02

##寫在前面 昨天在看Android中的快取機制,順帶把非同步載入任務複習了一遍,尤其是AsyncTask。由於我在一般情況下使用較多的就是Handler方式執行非同步任務,很少用到AsyncTask,所以現在總結一下加深自己的印象,並分享出來與大家交流。後幾天會發一些關於Android中的快取機制。 在本文書寫過程中,我借鑑的資料有《第一行程式碼》、《Android開發藝術探索》、慕課網的一期課程以及相關的博文,連結在文末註明。下面是本文的目錄,比較簡單。

  • 非同步訊息處理機制
    • 組成部分
    • 基本流程
  • 初識AsyncTask
    • 基本介紹
    • 執行順序
    • 注意事項
  • AsyncTask的使用
    • 載入網路圖片
    • 模擬進度條
    • AsyncTask和Handler兩種非同步方式區別
  • 資料來源
  • 專案原始碼

##非同步訊息處理機制

非同步訊息處理機制是Android開發中的基礎知識。為了方便後面的AsyncTask的理解,我就先簡單說一下這個機制,但不作為本文的重點。

###組成部分 Android中的非同步訊息主要是由四個部分組成:

  • Message:訊息
  • MessageQueue:訊息佇列
  • Handler:訊息處理者
  • Looper:訊息迴圈者

下面我們依次來看:

####1、Message

Message就是非同步訊息中需要傳遞的訊息。它會在內部攜帶一些少量的資訊,用於不同執行緒之間交換資料。它本身有四個欄位,用來處理不同型別的資訊:

  • what:訊息的一些標誌,可以根據不同的標誌做不同的處理
  • obj:欄位為Object型別,這個不用多解釋了,物件中的大Boss
  • arg1和arg2:整型型別,可以存放一些標誌位或不易混淆的整型值

####2、MessageQueue

知道了Message就很好理解這個了,這是一個訊息佇列。佇列中存放著大量的Message,這些Message都是沒有被處理的,所以放在佇列中等待處理。值得注意的是,每個執行緒中有且僅有一個MessageQueue物件。

####3、Handler 這個我們很熟悉,不好翻譯,一般用來處理訊息。我們就叫它訊息處理者吧。它是處理機制中一個比較重要的角色,是用來傳送和處理訊息的。主要是兩個方法:

  • sendMessage(Message msg):傳送訊息
  • handleMessage(Message msg):處理訊息

也就是說經過Handler傳送的訊息最終會傳遞到handleMessage中,進而被處理。

####4、Looper Looper不是很好理解。《第一行程式碼》中將其看作MessageQueue的管家,比較抽象。而《Android開發藝術探索》中將其定義成訊息迴圈者,這個比較符合邏輯。也就是說,當執行緒中的Looper呼叫自身的loop()方法後,它會進入到一個無限迴圈中,迴圈監測MessageQueue中是否存在訊息,如有存在一條未處理的訊息,Looper就會將其取出,傳遞給Handler的handleMessage方法,讓其處理。

這裡我們需要注意兩點:

  • 每個執行緒中有且僅有一個Looper物件。
  • 新new出來的Thread是沒有Looper的,也就是隻有UI執行緒預設有Looper。

###基本流程

明白了上述這些基本定義後,我們來看一下非同步訊息處理機制的大體流程。二話不說,先上圖。

非同步訊息處理流程圖

從圖中我們可以看出,大體流程如下:

  1. 首先我們需要在主執行緒中建立一個Handler物件,並且需要重寫handleMessage方法;
  2. 其次在子執行緒中需要與主執行緒進行通訊的時候,就通過Message物件攜帶一些資訊,並通過Handler物件傳送出去。
  3. 傳送出去後該訊息會進入MessageQueue等待處理。
  4. 這個時候Looper就起作用了,它一直迴圈迴圈終於監測到佇列中有一條待處理的訊息,立即取出並分發到handleMessage方法,這樣主執行緒就收到這條訊息並開始處理。

關於這個機制我就簡單介紹到這,如果讀者對這個機制又不懂的地方可以Google相關資料,也有大量優秀博文。文末也有相關博文連結,不再贅述。

##初識AsyncTask

###基本介紹 AsyncTask是一種輕量級的非同步任務類,它與Handler一樣,可以在後臺執行任務。並可以把執行任務的過程及結果返回給主執行緒。這樣主執行緒就可以做一些更新UI的操作。

它的本質是一個抽象的泛型類,所以我們在使用的時候需要繼承這個AsyncTask類:

public abstract class AsyncTask<Params,Progress,Result>

可以看到,類提供了三個引數:

  • Params:任務啟動時需要輸入的引數型別
  • Progress:後臺任務執行中返回的進度值的型別
  • Result:後臺執行任務完成後返回結果的型別

同時,它有四個核心方法,我們依次來看:

  1. onPreExecute():該方法在主執行緒中執行,通常使用者完成一些準備和初始化的操作;
  2. doInBackground(Params... params):在子執行緒中執行,執行非同步任務的關鍵方法,必須重寫。此方法中可以呼叫publishProgress方法來更新任務進度,而publishProgress會呼叫onProgressUpdate方法來進行進度更新。注意此方法需要返回值給onPostExecute方法;
  3. onProgressUpdate(Progress...values):在主執行緒中執行,用於任務進度的更新;
  4. onPostExecute(Result result):在主執行緒中執行,該方法的引數是doInBackground的返回值。

當然還有其他方法,但是不怎麼常用,這裡不再介紹(其實我也不瞭解)。

當你自定義一個CustomTask繼承AsyncTask複寫這幾個方法後,就需要開啟任務,開啟方式為:

new CustomTask().execute();

當然在這裡我沒有寫引數,在實際開發中要注意填入相應的引數。

###執行順序

當我們自定義Task並複寫這四個核心方法後,來看看這幾個方法的執行順序。測試程式碼如下:

/**
 * 自定義非同步任務類
 */
class CustomTask extends AsyncTask<Void, Void, Void> {

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        Log.w("Task", " ----> onPreExecute");
    }

    @Override
    protected Void doInBackground(Void... params) {
        publishProgress();
        Log.w("Task", " ----> doInBackground");
        return null;
    }

    @Override
    protected void onProgressUpdate(Void... values) {
        super.onProgressUpdate(values);
        Log.w("Task", " ----> onProgressUpdate");
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        Log.w("Task", " ----> onPostExecute");
    }
}
複製程式碼

來看Log日誌:

執行順序Log日誌

可以看出執行順序依次是:

onPreExecute -> doInBackground -> onProgressUpdate -> onPostExecute

###注意事項

基本瞭解了AsyncTask後,我們需要知道一些注意事項和使用過程中的一些條件限制。

  • 在四個核心方法中,只有doInBackground方法執行在子執行緒,其他方法都執行在主執行緒;
  • AsyncTask類必須在主執行緒中載入,也就是第一次訪問AsyncTask必須發生在主執行緒;
  • AsyncTask物件必須在主執行緒中建立;
  • AsyncTask的execute方法必須在主執行緒中呼叫;
  • 四個核心方法只能系統自動呼叫,而不能主動呼叫;

##AsyncTask的使用

好了,看到這裡相信你已經對AsyncTask有了一個基本的認識了,現在我們就通過兩個例項在看AsyncTask在專案中的具體使用。

###載入網路圖片

首先來看第一個Demo,通過圖片的URL地址載入網路圖片。已經是網路圖片了,當然是耗時任務,所以我們需要開啟非同步任務。當然可以開啟子執行緒來獲取,但是這裡我們採取AsyncTask這樣的方式。完成效果如下:

載入網路圖片

來看程式碼,程式碼比較簡單,註釋也寫的很清晰,就不再解釋了:

/**
 * 非同步任務載入網路圖片
 */
public class NetPicActivity extends AppCompatActivity {

    private ImageView netPicImage;
    private ProgressBar netPicProgressBar;
    private static String picUrl = "http://www.iamxiarui.com/wp-content/uploads/2016/05/桌布.jpg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_net_pic);
        netPicImage = (ImageView) findViewById(R.id.iv_netpic);
        netPicProgressBar = (ProgressBar) findViewById(R.id.pb_netpic);
        //通過呼叫execute方法開始處理非同步任務.相當於執行緒中的start方法.
        new NetAsyncTask().execute(picUrl);
    }

    /**
     * 自定義網路請求非同步任務
     */
    class NetAsyncTask extends AsyncTask<String, Void, Bitmap> {

        /**
         * onPreExecute用於非同步處理前的操作
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //此處將progressBar設定為可見.
            netPicProgressBar.setVisibility(View.VISIBLE);
        }

        /**
         * 在doInBackground方法中進行非同步任務的處理
         *
         * @param params 引數為URL
         * @return Bitmap物件
         */
        @Override
        protected Bitmap doInBackground(String... params) {
            //獲取傳進來的引數
            String url = params[0];
            Bitmap bitmap = null;
            URLConnection connection;
            InputStream is;
            try {
                connection = new URL(url).openConnection();
                is = connection.getInputStream();
                //為了更清楚的看到載入圖片的等待操作,將執行緒休眠3秒鐘
                Thread.sleep(3000);
                BufferedInputStream bis = new BufferedInputStream(is);
                //通過decodeStream方法解析輸入流
                bitmap = BitmapFactory.decodeStream(bis);
                is.close();
                bis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        /**
         * onPostExecute用於UI的更新.此方法的引數為doInBackground方法返回的值
         *
         * @param bitmap 網路圖片
         */
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            //隱藏progressBar
            netPicProgressBar.setVisibility(View.GONE);
            //更新imageView
            netPicImage.setImageBitmap(bitmap);
        }
    }
}
複製程式碼

對了,一定要記得新增網路許可權,不然沒有效果。

###模擬進度條

載入網路圖片的時候,我們沒有用到onProgressUpdate方法,現在就模擬一個進度條載入的Demo,效果如下:

模擬進度條

來看程式碼,這裡我們模擬了進度條的載入,並在doInBackground中呼叫了publishProgress方法,讓進度條更新,具體程式碼如下:

/**
 * 用非同步任務模擬進度條的更新
 */
public class ProgressActivity extends AppCompatActivity {
    private ProgressBar mainProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_progress);

        mainProgressBar = (ProgressBar) findViewById(R.id.pb_main);

        //開啟非同步任務
        new PbAsyncTask().execute();
    }

    /**
     * 自定義非同步任務類
     */
    class PbAsyncTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            //使用for迴圈來模擬進度條的進度.
            for (int i = 0; i < 100; i++) {
                //呼叫publishProgress方法將自動觸發onProgressUpdate方法來進行進度條的更新.
                publishProgress(i);
                try {
                    //通過執行緒休眠模擬耗時操作
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //通過publishProgress方法傳過來的值進行進度條的更新.
            mainProgressBar.setProgress(values[0]);
        }
    }

}
複製程式碼

注意,這裡我們來看下面的圖,我們可以發現當第一次進度條沒有全部完成時,返回重新點選開啟進度條。此時進度條並沒有開始執行,而是等待一段時間後,才開始更新進度。

進度條異常

為什麼呢?因為AsyncTask是基於執行緒池進行實現的,當一個執行緒沒有結束時,後面的執行緒是不能執行的。也就是說必須等到第一個task的for迴圈結束後,才能執行第二個task。

那麼如何解決呢?我們知道,當我們點選BACK鍵時會呼叫Activity的onPause()方法,所以我們可以在Activity的onPause()方法中將正在執行的task標記為cancel狀態,在doInBackground方法中進行非同步處理時判斷是否是cancel狀態來決定是否取消之前的task。

更改後的程式碼如下:

/**
 * 用非同步任務模擬進度條的更新
 */
public class ProgressActivity extends AppCompatActivity {
    private ProgressBar mainProgressBar;
    private PbAsyncTask pbAsyncTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_progress);

        mainProgressBar = (ProgressBar) findViewById(R.id.pb_main);

        //開啟非同步任務
        pbAsyncTask = new PbAsyncTask();
        pbAsyncTask.execute();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (pbAsyncTask != null && pbAsyncTask.getStatus() == AsyncTask.Status.RUNNING) {
            //cancel方法只是將對應的AsyncTask標記為cancelt狀態,並不是真正的取消執行緒的執行.
            pbAsyncTask.cancel(true);
        }
    }

    /**
     * 自定義非同步任務類
     */
    class PbAsyncTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            //使用for迴圈來模擬進度條的進度.
            for (int i = 0; i < 100; i++) {
                //如果task是cancel狀態,則終止for迴圈,以進行下個task的執行.
                if (isCancelled()) {
                    break;
                }
                //呼叫publishProgress方法將自動觸發onProgressUpdate方法來進行進度條的更新.
                publishProgress(i);
                try {
                    //通過執行緒休眠模擬耗時操作
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //通過publishProgress方法傳過來的值進行進度條的更新.
            mainProgressBar.setProgress(values[0]);
        }
    }

}
複製程式碼

效果如下:

進度條異常解決

已經完美解決了這個問題,在這裡我們要注意,cancel方法只是將對應的AsyncTask標記為cancel狀態,並不是真正的取消執行緒的執行,想要真正取消執行緒,還是需要在doInBackground方法中停止。

##AsyncTask和Handler兩種非同步方式區別

已經看完兩個Demo了,應該對AsyncTask有了很深的理解了。但是想到這裡不得不提出一個疑問,這看起來不就是開啟一個子執行緒嘛,有啥不同的。其實AsyncTask和Handler兩種非同步方式還真有很大的區別。

首先來看AsyncTask,它是Android提供的輕量級的非同步類,可以直接繼承AsyncTask。在類中實現非同步操作,並提供介面來反饋當前非同步執行的程度,也就是所謂的進度更新,最後反饋執行的結果給UI主執行緒。這種方法使用起來簡單快捷,過程清晰明瞭而且便於控制。不足的是在在使用多個非同步操作和並需要進行Ui變更時,就變得複雜起來,程式碼也看起來比較臃腫。

其次是Handler,在本文開頭的時候,就說了非同步訊息處理機制。它是通過Handler, Looper, Message,Thread四個物件之間的聯絡來進行處理訊息的。這種方式在功能上比較清晰,有多個後臺任務的時候程式碼看起來比較有序。

所以說在實際開發過程中,根據需要來選擇非同步任務處理方式,就我個人而言,還是Handler方式用的比較多。

好了,本文基本技術了。由於我技術水平有限,如有錯誤或不同意見,歡迎指正與交流。

##優秀資料來源

慕課網 - Android必學-AsyncTask基礎

Android必學之AsyncTask - caobotao

Android訊息機制淺析 - xiasuhuei321

AsyncTask和Handler兩種非同步方式的實現和區別比較

##專案原始碼

Github - AsyncTaskDemo - IamXiaRui


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=699

相關文章