1. 程式人生 > >Android多執行緒-AsyncTask的使用和問題(取消,並行和序列,螢幕切換)

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方法被呼叫後,onPostExecuteonProgressUpdate方法都不會再呼叫了。而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就不會變化了。

其中一種解決方法

很簡單,加上這句就行了。

這裡寫圖片描述

這時螢幕怎麼切換都沒事

這裡寫圖片描述