Android多執行緒-AsyncTask的使用和問題(取消,並行和序列,螢幕切換)
AsyncTask是Android提供的一個執行非同步工作的類,內部其實是運用了執行緒池和Handler來進行非同步任務的執行和與主執行緒的互動。AsyncTask只是一個輔助類,適合執行時間短的非同步任務。
本文基於Android7.0的程式碼來說的。
示例
AsyncTask的使用方法是很簡單的。就做一個簡單的進度條。
佈局是這樣的:
裡面有一個進度條ProgressBar pb1
,開始按鈕Button btn1
,停止按鈕Button stop1
然後實現一個AsyncTask,通過構造方法接收一個ProgressBar和Button進行操作:
public class MyAsyncTask extends AsyncTask<String, Integer, String> {
private String TAG = this.getClass().getSimpleName();
Button btn;
ProgressBar pb;
public MyAsyncTask(Button btn, ProgressBar pb) {
this.btn = btn;
this.pb = pb;
}
@Override
protected String doInBackground (String... params) {
String result = "完成";
for (int i = 1; i <= 10; i++) {
try {
Log.i(TAG, "doInBackground: "+i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
publishProgress(i);
}
return result;
}
@Override
protected void onPreExecute() {
Log.i(TAG, "onPreExecute: 準備工作");
}
@Override
protected void onPostExecute(String s) {
btn.setText(s);
Log.i(TAG, "onPostExecute: 回撥");
}
@Override
protected void onProgressUpdate(Integer... values) {
pb.setProgress(values[0]);
}
}
然後給兩個按鈕新增點選事件:
MyAsyncTask task1
task1 = new MyAsyncTask(btn1, pb1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 開始1");
task1.execute();
}
});
stop1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 停止1");
task1.cancel(true);
}
});
1.構造引數
AsyncTask定義了三個泛型引數,在繼承的時候必須填寫。例如上面的AsyncTask<String, Integer, String>
在原始碼中定義是:
引數含義在下面的方法中會具體用到,先大致瞭解一下:
- Params 啟動任務的時候輸入的引數型別,一般都是String型別,如填個網址啥的。 上面示例中就是String型別。
- Progress 用來更新進度的型別。表示任務執行的進度的型別。 示例用的進度條,所以選擇Integer型別。
- Result 後臺任務執行完後返回的結果, 示例中返回的也是String型別。
2.重寫方法
要使用AsyncTask最少需要重寫方法doInBackground
。因為只有這個方法是抽象方法。
這個方法是在後臺執行緒執行的。可以看到使用的引數型別Params就是在構造時定義的第一個型別。而返回的型別Result就是定義的第三個型別。
初次之外一般為了對任務流程進行控制還會重寫下面幾個方法onPreExecute
,onPostExecute
,onProgressUpdate
。
下面幾個方法在AsyncTask中是空的,而且都要求在主執行緒中執行。
@MainThread
protected void onPreExecute() {
}
@MainThread
protected void onPostExecute(Result result) {
}
@MainThread
protected void onProgressUpdate(Progress... values) {
}
- onPreExecute() 在非同步任務開始前做的操作,
- onPostExecute(Result result) 後臺任務執行完後,通過這個方法能拿到任務返回的結果,進行處理。
- onProgressUpdate(Progress… values) 這個表示進度變化,引數型別是構造時的第二個型別Progress。進度應該是隨著任務的執行實時更新的,但是這個方法要在主執行緒中執行,而
doInBackground
是在子執行緒中執行,所以不能直接在doInBackground
中呼叫onProgressUpdate
方法,而是通過呼叫publishProgress(Progress... values)
來間接呼叫這個方法。
3.開始任務
AsyncTask的開始有下面三種方法:
execute(Params... params)
executeOnExecutor(Executor exec,Params... params)
execute(Runnable runnable)
execute(Params… params) 這個就是在示例中使用的開始任務的方式,傳入指定的引數,引數型別要和構造時定義的第一個引數型別Params一樣。引數可以為空的,那麼在方法
doInBackground(Params... params)
中的引數也是空的。使用預設的執行緒池執行任務,會按流程執行onPreExecute
,doInBackground(Params... params)
,onPostExecute(Result result)
等方法。executeOnExecutor(Executor exec,Params… params) 如果預設的執行緒池不能滿足你的要求,可以用這個方法用指定執行緒池來執行任務。流程跟上面是一樣的。
execute(Runnable runnable) 這個方法傳進來的是一個Runnable型別,方法中只有一行程式碼
sDefaultExecutor.execute(runnable)
就是用預設的執行緒池直接執行任務,就是使用執行緒池了,跟前面那些重寫的方法沒關係。
4.停止任務
要停止任務可以呼叫下面的方法:
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
下面是停止的演示:
取消也有一個回撥方法可以重寫,這裡加上,也是執行在主執行緒中:
@Override
protected void onCancelled() {
Log.i(TAG, "onCancelled: 取消任務");
btn.setText("取消了");
}
取消的問題
當cancel
方法被呼叫後,onPostExecute
和onProgressUpdate
方法都不會再呼叫了。而doInBackground
方法卻會一直執行下去,也就是後臺任務會繼續執行。
cancel(boolean mayInterruptIfRunning)
這個引數mayInterruptIfRunning
文件中表示是否應該立即終止doInBackground
中的任務。
然而實際用起來就不是那樣的了,無論我們傳的是true
還是false
,而AsyncTask的cancle
方法只是打上了一個取消的標記。並不是直接終止任務。如果是true
,則會呼叫一下後臺執行緒的interrupt
方法。
當呼叫了cancle
方法後,呼叫isCancelled
方法會返回true。在doInBackground
中應該呼叫isCancelled
來檢查當前任務是否被取消,以便及時終止任務。
AsyncTask設計成這樣就是為了方便更新主執行緒介面的,所以對使用者來說,在呼叫了cancle方法後,後臺的任務就不會在影響到主執行緒的介面變化了,因為後續的跟主執行緒互動的方法都不會再執行了。,也可以說是取消了。
而真的要及時取消doInBackground
的繼續執行則需要在這個方法中進行一些判斷。
不做處理
不做處理也就是在doInBackground
中不做判斷,像下面這樣。看一下輸出的日誌。當然介面的進度條都會停住,只要看doInBackground
有沒有在點選停止按鈕後停下來。
protected String doInBackground(String... params) {
String result = "完成";
for (int i = 1; i <= 10; i++) {
try {
Log.i(TAG, "doInBackground: "+i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
publishProgress(i);
}
return result;
}
- cancle(true):
- cancle(false):
可以看到區別是,當值為true的時候,後臺執行緒也會跑完。但是會呼叫子執行緒的interrupt
方法,而這個現在正在sleep,所以會引發InterruptedException
.
而值為false的時候,沒有任何變化,後臺執行緒繼續跑完。
做處理
1. 判斷isCancelled
在不同的執行節點判斷這個方法的值:
@Override
protected String doInBackground(String... params) {
String result = "完成";
for (int i = 1; i <= 10; i++) {
try {
if (isCancelled()){
Log.i(TAG, "doInBackground: 被標記停止了");
break;
}
Log.i(TAG, "doInBackground: "+i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
publishProgress(i);
}
return result;
}
這時呼叫cancle(false):
呼叫cancle(true)只是會多列印一個異常,一樣會停止。
2.抓異常
因為呼叫cancle(true)的時候有可能會丟擲異常,如這個例子中的InterruptedException
,因此可以通過異常捕捉來實現。
@Override
protected String doInBackground(String... params) {
String result = "完成";
for (int i = 1; i <= 10; i++) {
try {
Log.i(TAG, "doInBackground: "+i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
Log.i(TAG, "doInBackground: 捕捉到異常,退出");
break;
}
publishProgress(i);
}
return result;
}
這時呼叫cancle(true):
並行和序列
據說AsyncTask的任務是並行還是序列執行在不同Android版本有所變化,但是從API13開始,AsyncTask的任務執行都是序列的。
何為序列,比如有下面的介面:
有兩個task
private ProgressBar pb1;
private ProgressBar pb2;
private Button btn1;
private Button btn2;
private Button stop1;
private Button stop2;
private MyAsyncTask task1, task2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
task1 = new MyAsyncTask(btn1, pb1);
task2 = new MyAsyncTask(btn2, pb2);java
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 開始1");
task1.execute();
}
});
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 開始2");
task2.execute();
}
});
stop1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 停止1 ");
task1.cancel(true);
}
});
stop2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 停止2");
task2.cancel(false);
}
});
}
在上面的MyAstncTask中,後臺任務要執行10秒。
這裡為了區分,列印開始按鈕的名字來區分:
public class MyAsyncTask extends AsyncTask<String, Integer, String> {
private String TAG = this.getClass().getSimpleName();
Button btn;
ProgressBar pb;
String name;
public MyAsyncTask(Button btn, ProgressBar pb) {
this.btn = btn;
this.pb = pb;
name = btn.getText().toString();
}
@Override
protected String doInBackground(String... params) {
String result = "完成";
for (int i = 1; i <= 10; i++) {
try {
Log.i(TAG, "doInBackground: "+name+" "+i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
Log.i(TAG, "doInBackground: 捕捉到異常,退出");
break;
}
publishProgress(i);
}
return result;
}
@Override
protected void onPreExecute() {
Log.i(TAG, "onPreExecute: 準備工作 "+name);
}
@Override
protected void onPostExecute(String s) {
btn.setText(s);
Log.i(TAG, "onPostExecute: 回撥 "+name);
}
@Override
protected void onProgressUpdate(Integer... values) {
pb.setProgress(values[0]);
}
@Override
protected void onCancelled() {
Log.i(TAG, "onCancelled: 取消任務 "+name);
btn.setText("取消了");
}
}
在點選第一個開始按鈕之後點選第二個開始按鈕,效果:
列印日誌:
雖然點選了開始2,但是依然等第一個任務完成了才開始第二個任務。
想要讓任務並行執行怎麼辦呢?其實他之所以會序列執行任務,是因為內部預設的執行緒池中將任務進行了排隊,保證他們一個一個來。只要我們換個滿足要求的執行緒池來執行任務就行了。AstncTask內部就有一個執行緒池AsyncTask.THREAD_POOL_EXECUTOR
可以使用。當然,用Executors來建立也行。
然後將開始任務的execute(Params... params)
方法改為executeOnExecutor(Executor exec,Params... params)
.這裡用AsyncTask.THREAD_POOL_EXECUTOR
.
task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
效果:
日誌:
螢幕橫豎屏切換
使用AsyncTask的時候,在螢幕切換也會出現問題。
畫面是這樣的:
日誌是這樣的,動圖中也能看見:
雖然螢幕切換後,任務也在執行,也在不停地呼叫更新進度條的方法,最後也執行了onPostExecute
方法,但是介面上就是什麼變化都沒有。
因為在橫豎屏切換的時候,Activity會銷燬重建,所以AsyncTask所持有的引用就不是新建的Activity的控制元件了,新的Activity就不會變化了。
其中一種解決方法
很簡單,加上這句就行了。
這時螢幕怎麼切換都沒事