大話Android多執行緒(六) AsyncTask知識掃盲

Anlia發表於2018-02-16

版權宣告:本文為博主原創文章,未經博主允許不得轉載
原始碼:github.com/AnliaLee
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論

前言

本章我們將結合之前幾篇部落格,來研究研究多執行緒知識綜合應用程度很高的AsyncTask類(Android 7.0版本)

往期回顧
大話Android多執行緒(一) Thread和Runnable的聯絡和區別
大話Android多執行緒(二) synchronized使用解析
大話Android多執行緒(三) 執行緒間的通訊機制之Handler
大話Android多執行緒(四) Callable、Future和FutureTask
大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


AsyncTask簡介

通過之前幾篇部落格的學習和研究,我們知道了要將耗時的任務放到子執行緒中執行,然後使用Handler機制通知UI執行緒任務的結果並執行更新UI的操作。如果這些步驟都由我們自己動手去寫,勢必會讓程式碼顯得非常臃腫

Android給我們提供了一種輕量級的非同步任務類AsyncTask,該類實現了非同步操作,並提供相應的介面反饋非同步任務執行結果及進度,實現了從子執行緒執行任務到通知主執行緒更新UI的一條龍服務,大大減少了我們的開發工作。下面我們將從如何使用開始逐步揭開AsyncTask的神祕面紗


如何使用AsyncTask

我們以去快餐店點餐為例。我們將顧客點餐與取餐的行為放在主執行緒中(更新UI介面等操作),而服務人員在廚房配餐的行為放在子執行緒中進行(在後臺執行耗時操作

顧客不會關心服務人員在廚房是如何工作的,他們只關心點了什麼(配置引數並呼叫AsyncTask.execute進行提交)以及何時收到通知去取餐(在AsyncTask.onPostExecute回撥方法中接收後臺任務返回的結果,並執行相應的操作)。服務人員在配餐之前可以幫助顧客準備餐盤、紙巾等等,當然這些工作對顧客來說是可見的(通過重寫AsyncTask.onPreExecute回撥方法執行一些耗時任務之前的準備工作,該方法執行在主執行緒中)。準備工作完畢後,服務人員會通知廚房進行配餐,這部分工作對於顧客來說是不可見的(在AsyncTask.doInBackground方法中編寫執行耗時任務的程式碼,這些耗時任務執行在子執行緒中)。配餐完畢後,通知顧客來取餐(AsyncTask.doInBackground結束時會返回一個值,該值會傳遞到AsyncTask.onPostExecute中,證明耗時任務已經執行完畢)

整個流程簡單總結一下就是 開始任務execute) → 任務準備onPreExecute) → 執行任務doInBackground) → 反饋任務結果回到主執行緒執行相應操作onPostExecute

下面我們來看具體的程式碼(關於使用AsyncTask會導致記憶體洩漏的問題請看文末的補充,這裡的程式碼只是簡單實現就不多贅述了)

public class AsyncTaskTestActivity extends AppCompatActivity {
    TextView textShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task_test);
        textShow = (TextView) findViewById(R.id.text_show);
    }

    public void clickEvent(View view) {
    	switch (view.getId()) {
            case R.id.btn_start:
                List<String> list = new ArrayList<>();
                list.add("薯條");
                list.add("漢堡");
                list.add("可樂");
                new MyAsyncTask().execute(list);
                break;
    	}
    }

    public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            textShow.setText("餐盤準備好了,開始配餐...");
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            textShow.setText("配餐完畢," + s);
        }

        @Override
        protected String doInBackground(List<String>... params) {
            String foods = "已經配好的食物 —— ";
            try {
                for (String str : params[0]){
                    Thread.sleep(1000);//模擬配餐的時間
                    foods = foods + str + " ";
                    Log.e("白鬍子快餐店",foods);
                }
            }catch (Exception e){}
            return foods;
        }
    }
}
複製程式碼

執行效果如圖所示

大話Android多執行緒(六) AsyncTask知識掃盲

大話Android多執行緒(六) AsyncTask知識掃盲

我們來分析一下上述程式碼中的細節

使用AsyncTask首先得實現它的子類,我們先來看下抽象類AsyncTask的部分原始碼

public abstract class AsyncTask<Params, Progress, Result>
複製程式碼

這裡告訴我們如果要繼承AsyncTask,需配置3個泛型引數的具體型別,這3個引數的介紹如下

  • Params開始執行非同步任務時需傳入的引數型別,即AsyncTask.execute方法中要傳遞的引數型別。例如我們將Params設為String型別,那麼將呼叫 execute(String s) 方法開始任務,提交的引數s型別為String。另外此引數型別同樣和傳入AsyncTask.doInBackground方法的引數相對應

    ps:如果不需要傳遞任何引數,則可以將引數型別設為Void,那麼開始任務時只需要呼叫 execute() 即可,下同(返回引數同理)

  • Progress執行非同步任務過程中主執行緒傳遞的進度值的型別。我們可以在AsyncTask.doInBackground方法中呼叫publishProgress方法告知主執行緒當前耗時任務的執行進度,我們設定的進度值型別publishProgress方法要傳遞引數的型別
  • Result任務執行完畢後,返回的結果型別,即AsyncTask.doInBackground方法返回值的型別,也是AsyncTask.onPostExecute方法傳入引數的型別

結合之前的程式碼,在我們實現的AsyncTask子類中,Params設為List<String>型別,Progress設為String型別,Result設為String型別

public class MyAsyncTask extends AsyncTask<List<String>,String,String>
複製程式碼

那麼對應的我們在提交引數開始執行任務時,就需要傳入List<String>型別的引數了

List<String> list = new ArrayList<>();
list.add("薯條");
list.add("漢堡");
list.add("可樂");
new MyAsyncTask().execute(list);//這裡若傳入多個list,在doInBackground中按順序取參即可
複製程式碼

doInBackground中獲取我們提交的引數

protected String doInBackground(List<String>... params) {
	for (String str : params[0])//params[0]就是我們提交的第一個list
	...
	
	return foods;//String foods
}
複製程式碼

任務執行完畢後,返回一個String型別的值,該值即為onPostExecute方法的傳入引數

protected void onPostExecute(String s)//s就是我們需要的返回值
複製程式碼

好了,程式碼的細節分析完畢,下面我們來看看如何實現任務的進度更新功能

首先我們需要重寫AsyncTask.onProgressUpdate回撥方法,將更新進度UI的操作放在這裡面

public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
	//省略部分程式碼...
	@Override
	protected void onProgressUpdate(String... values) {
		super.onProgressUpdate(values);
		textShow.setText(values[0]);
	}
}
複製程式碼

然後在doInBackground方法中(子執行緒)呼叫publishProgress方法就可以把當前任務進度傳遞到onProgressUpdate中(主執行緒)了

@Override
protected String doInBackground(List<String>... params) {
	String foods = "已經配好的食物 —— ";
	try {
		for (String str : params[0]){
			Thread.sleep(1000);//模擬配餐的時間
			foods = foods + str + " ";
			publishProgress(foods);//同樣這裡可以傳遞多個String型別的引數
		}
		Thread.sleep(500);
	}catch (Exception e){}
	return foods;
}
複製程式碼

執行結果如下

大話Android多執行緒(六) AsyncTask知識掃盲

除了以上這些方法外,AsyncTask還提供了onCancelled回撥方法。當我們呼叫 cancel(true) 時,doInBackground方法將強制中斷任務並呼叫onCancelledonCancelled被呼叫時onPostExecute不會被呼叫),因此我們可以將取消任務的操作放在onCancelled中,例如

public class MyAsyncTask extends AsyncTask<List<String>,String,String>{
	//省略部分程式碼...
	@Override
	protected void onCancelled(String s) {
		super.onCancelled(s);
		textShow.setText("未完成配餐," + s);
	}

	@Override
	protected String doInBackground(List<String>... params) {
		String foods = "已經配好的食物 —— ";
		try {
			for (String str : params[0]){
				if(str.equals("可樂")){
					Thread.sleep(500);
					cancel(true);
					while (true){
						/**
						 * cancel方法只是簡單把標誌位改為true
						 * 最後使用Thread.interrupt去中斷執行緒執行
						 * 但這不能保證可以馬上停止任務
						 * 所以需使用isCancelled來判斷任務是否被取消
						 */
						if(isCancelled()){
							return foods;
						}
					}
				}
				...
			}
			Thread.sleep(500);
		}catch (Exception e){}
		return foods;
	}
}
複製程式碼

執行結果如下

大話Android多執行緒(六) AsyncTask知識掃盲

簡單總結一下這些AsyncTask中常用的方法

  • execute(Params... params):提交引數,開始任務
  • onPreExecute():執行任務之前的準備操作
  • doInBackground(Params... params):在子執行緒中執行任務,返回任務結果
  • onPostExecute(Result result):接收任務結果,在UI執行緒主執行緒)中執行相應操作
  • onProgressUpdate(Progress... values):在UI執行緒中執行更新進度的操作,配套的提交任務進度的方法為 publishProgress(Progress... values)
  • onCancelled(Result result):接收取消任務時的結果並執行相應的操作,配套的取消中斷任務的方法為 cancel(boolean mayInterruptIfRunning)

那麼這些方法在AsyncTask內部具體是怎麼運作的呢?下面我們就繼續深入探尋一番吧


AsyncTask內部工作流程

之前我們簡單地總結過一次流程:

開始任務execute) → 任務準備onPreExecute) → 執行任務doInBackground) → 反饋任務結果回到主執行緒執行相應操作onPostExecute

如果將這個流程繼續細化,則如下圖所示

大話Android多執行緒(六) AsyncTask知識掃盲

從圖中我們可以看到執行緒池ThreadPoolExecutor,負責執行緒間通訊的Handler等等。如果有看過我之前幾篇部落格或者瞭解過相關知識的童鞋應該很快就能在腦中描繪出AsyncTask整個工作流程的藍圖了。我們這裡就不一行行地分析每個方法的原始碼了,只是對照著上圖幫大家理清思路,這樣大家去看一些原始碼分析的部落格時就沒那麼頭疼了

首先從實現AsyncTask的子類說起,AsyncTask內部有3個狀態,它們封裝在Status列舉類中,分別是

  • Status.PENDING:在AsyncTask物件建立時就設定了狀態為PENDING,表示AsyncTask等待被使用,尚未開始執行任務
  • Status.RUNNING:提交引數開始任務後,狀態設定為RUNNING,表示AsyncTask處於執行任務的狀態,任務正在執行中
  • Status.FINISHED:任務完成後,狀態會設定成FINISHED

需要補充的是,這些狀態在整個任務生命週期中只會設定一次,何時設定狀態已在上圖用虛線標出

我們呼叫execute方法後,AsyncTask會繼續呼叫executeOnExecutor方法(在此方法中呼叫了onPreExecute,因此在建立子執行緒執行任務前就完成了準備操作)並傳入預設的任務執行者SERIAL_EXECUTORSerialExecutor),在SerialExecutor中維護著一個任務佇列並限制了任務必須單一依次執行。很多部落格將SerialExecutor說成是一個執行緒池,我個人並不贊同這一說法,因為實際上在SerialExecutor中完成建立執行緒、維護執行緒這些工作的是一個真正意義上的執行緒池(THREAD_POOL_EXECUTOR),因此最終提交任務的操作還是回到了執行緒池的老路子,呼叫ThreadPoolExecutor.execute方法將任務入列


Android 7.0版本的AsyncTask預設序列執行任務,那有什麼方法可以突破這一限制呢?答案是呼叫我們之前提到的executeOnExecutor方法

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params)
複製程式碼

我們可以跳過execute方法直接呼叫executeOnExecutor並傳入我們自定義的執行緒池,這樣就可以併發地執行多執行緒任務了


回到我們的工作流程,之前講到呼叫ThreadPoolExecutor.execute方法提交任務,提交的任務型別為CallableWorkerRunnable),AsyncTask在其call方法中呼叫doInBackground方法。也就是說,提交任務後,ThreadPoolExecutor建立了子執行緒,而子執行緒執行了doInBackground中的耗時任務

任務執行完畢後,按套路使用Handler傳送Message通知主執行緒耗時任務已經完成了(或呼叫publishProgress方法一樣可以讓Handler傳送訊息通知主執行緒執行更新進度的操作),之後的事件就是根據傳送Message的內容決定是執行onPostExecute(若設定了任務取消則執行onCancelled)還是onProgressUpdate方法了(上圖由於位置有限無法體現更新進度這一過程,原理實際上是一樣的)

那麼到這AsyncTask的內部工作流程我們已經基本過了一遍,如果想要更深入地瞭解原始碼實現的過程,這裡向大家推薦幾位前輩的部落格(由於他們部落格更新的時間不同,大家需注意比對各版本AsyncTask的差異)

Android AsyncTask 原始碼解析
Android 7.0 AsyncTask分析
以及 Android進階之光 一書中關於AsyncTask的講解


一些額外的補充

  1. AsyncTask不同版本執行緒池的區別
  2. AsyncTask的缺陷

    相關部落格推薦
    AsyncTask的缺陷和問題
    Android多執行緒-AsyncTask的使用和問題(取消,並行和序列,螢幕切換)

  3. Android實現弱引用AsyncTask,將記憶體洩漏置之度外

大話Android多執行緒系列到這就暫告一段落了,不知道大家看完這個系列之後收穫如何呢?如果覺得還行那就多多點贊,然後再買點橘子給博主吃。當然,我就吃兩個,剩下的都留給你們哈哈~

最後祝大家新春快樂"狗"到最後事事都旺旺~

相關文章