Android 實踐之異步線程AsyncTask

爲何要引入AsyncTask?

在Android程序開始運行的時候會單獨啓動一個進程,默認情況下所有這個程序操作都在這個進程中進行。一個Android程序默認情況下只有一個進程,但一個進程中可以有多個線程。

在這些線程中,有一個線程叫做UI線程(也叫Main Thread),除了UI線程外的線程都叫子線程(Worker Thread)。UI線程主要負責控制UI界面的顯示、更新、交互等。因此,UI線程中的操作延遲越短越好(流暢)。把一些耗時的操作(網絡請求、數據庫操作、邏輯計算等)放到單獨的線程,可以避免主線程阻塞。

Android給我們提供了一種輕量級的異步任務類AsyncTask。該類中實現異步操作,並提供接口反饋當前異步執行結果及進度,這些接口中有直接運行在主線程中的(如 onPostExecute,onPreExecute等)。

使用的優點:

簡單,快捷
過程可控
使用的缺點:

使用多個異步操作並需要進行Ui變更時,就變得複雜起來.
AsyncTask的三種泛型類型

Params 啓動任務執行的輸入參數,比如HTTP請求的URL。
Progress 後臺任務執行的百分比。
Result 後臺執行任務最終返回的結果,比如String。
使用過AsyncTask 的同學都知道一個異步加載數據最少要重寫以下這兩個方法:

doInBackground(Params…) 後臺執行,比較耗時的操作都可以放在這裏。注意這裏不能直接操作UI。此方法在後臺線程執行,完成任務的主要工作,通常需要較長的時間。在執行過程中可以調用publicProgress(Progress…)來更新任務的進度。
onPostExecute(Result) 相當於Handler 處理UI的方式,在這裏面可以使用在doInBackground 得到的結果處理操作UI。 此方法在主線程執行,任務執行的結果作爲此方法的參數返回
有必要的話你還得重寫以下這三個方法,但不是必須的:

onProgressUpdate(Progress…) 可以使用進度條增加用戶體驗度。 此方法在主線程執行,用於顯示任務執行的進度。
onPreExecute() 這裏是最終用戶調用Excute時的接口,當任務執行之前開始調用此方法,可以在這裏顯示進度對話框。
onCancelled() 用戶調用取消時,要做的操作
AsyncTask的侷限性

AsyncTask的優點在於執行完後臺任務後可以很方便的更新UI,然而使用它存在着諸多的限制。先拋開內存泄漏問題,使用AsyncTask主要存在以下侷限性:

在Android 4.1版本之前,AsyncTask類必須在主線程中加載,這意味着對AsyncTask類的第一次訪問必須發生在主線程中;在Android 4.1以及以上版本則不存在這一限制,因爲ActivityThread(代表了主線程)的main方法中會自動加載AsyncTask
AsyncTask對象必須在主線程中創建
AsyncTask對象的execute方法必須在主線程中調用
一個AsyncTask對象只能調用一次execute方法
AsyncTask的工作原理

首先,讓我們來看一下AsyncTask類的構造器都做了些什麼:

 1 public AsyncTask() {
 2         mWorker = new WorkerRunnable<Params, Result>() {
 3             public Result call() throws Exception {
 4                 mTaskInvoked.set(true);
 5 
 6                 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
 7                 //noinspection unchecked
 8                 Result result = doInBackground(mParams);
 9                 Binder.flushPendingCommands();
10                 return postResult(result);
11             }
12         };
13 
14         mFuture = new FutureTask<Result>(mWorker) {
15             @Override
16             protected void done() {
17                 try {
18                     postResultIfNotInvoked(get());
19                 } catch (InterruptedException e) {
20                     android.util.Log.w(LOG_TAG, e);
21                 } catch (ExecutionException e) {
22                     throw new RuntimeException("An error occurred while executing doInBackground()",
23                             e.getCause());
24                 } catch (CancellationException e) {
25                     postResultIfNotInvoked(null);
26                 }
27             }
28         };
29     }

在第2行到第12行,初始化了mWorker,它是一個派生自WorkRunnable類的對象。WorkRunnable是一個抽象類,它實現了Callable接口。我們再來看一下第4行開始的call方法的定義,首先將mTaskInvoked設爲true表示當前任務已被調用過,然後在第6行設置線程的優先級。在第8行我們可以看到,調用了AsyncTask對象的doInBackground方法開始執行我們所定義的後臺任務,並獲取返回結果存入result中。最後將任務返回結果傳遞給postResult方法。關於postResult方法我們會在下文進行分析。由此我們可以知道,實際上AsyncTask的成員mWorker包含了AyncTask最終要執行的任務(即mWorker的call方法)。

接下來讓我們看看對mFuture的初始化。我們可以看到mFuture是一個FutureTask的直接子類(匿名內部類)的對象,在FutureTask的構造方法中我們傳入了mWorker作爲參數。我們使用的是FutureTask的這個構造方法:

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

也就是說,mFuture是一個封裝了我們的後臺任務的FutureTask對象,FutureTask類實現了FutureRunnable接口,通過這個接口可以方便的取消後臺任務以及獲取後臺任務的執行結果,具體介紹請看這裏:Java併發編程:Callable、Future和FutureTask。

從上面的分析我們知道了,當mWorker中定義的call方法被執行時,doInBackground就會開始執行,我們定義的後臺任務也就真正開始了。那麼這個call方法什麼時候會被調用呢?我們可以看到經過層層封裝,實際上是mFuture對象封裝了call方法,當mFuture對象被提交到AsyncTask包含的線程池執行時,call方法就會被調用,我們定義的後臺任務也就開始執行了。下面我們來看一下mFuture是什麼時候被提交到線程池執行的。

首先來看一下execute方法的源碼:

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

我們可以看到它接收的參數是Params類型的參數,這個參數會一路傳遞到doInBackground方法中。execute方法僅僅是調用了executeOnExecutor方法,並將executeOnExecutor方法的返回值作爲自己的返回值。我們注意到,傳入了sDefaultExecutor作爲executeOnExecutor方法的參數,那麼sDefaultExecutor是什麼呢?簡單的說,它是AsyncTask的默認執行器(線程池)。AsyncTask可以以串行(一個接一個的執行)或並行(一併執行)兩種方式來執行後臺任務,在Android3.0及以後的版本中,默認的執行方式是串行。這個sDefaultExecutor就代表了默認的串行執行器(線程池)。也就是說我們平常在AsyncTask對象上調用execute方法,使用的是串行方式來執行後臺任務。關於線程池更加詳細的介紹與分析請見:深入理解Java之線程池

我們再來看一下executeOnExecutor方法都做了些什麼:

 1 public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
 2             Params... params) {
 3         if (mStatus != Status.PENDING) {
 4             switch (mStatus) {
 5                 case RUNNING:
 6                     throw new IllegalStateException("Cannot execute task:"
 7                             + " the task is already running.");
 8                 case FINISHED:
 9                     throw new IllegalStateException("Cannot execute task:"
10                             + " the task has already been executed "
11                             + "(a task can be executed only once)");
12             }
13         }
14 
15         mStatus = Status.RUNNING;
16 
17         onPreExecute();
18 
19         mWorker.mParams = params;
20         exec.execute(mFuture);
21 
22         return this;
23     }

從以上代碼的第4行到第12行我們可以知道,當AsyncTask對象的當前狀態爲RUNNING或FINISHED時,調用execute方法會拋出異常,這意味着不能對正在執行任務的AsyncTask對象或是已經執行完任務的AsyncTask對象調用execute方法,這也就解釋了我們上面提到的侷限中的最後一條。

接着我們看到第17行存在一個對onPreExecute方法的調用,這表示了在執行後臺任務前確實會調用onPreExecute方法。

在第19行,把我們傳入的execute方法的params參數賦值給了mWorker的mParams成員變量;而後在第20行調用了exec的execute方法,並傳入了mFuture作爲參數。exec就是我們傳進來的sDefaultExecutor。那麼接下來我們看看sDefaultExecutor究竟是什麼。在AsyncTask類的源碼中,我們可以看到這句:

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
sDefaultExecutor被賦值爲SERIAL_EXECUTOR,那麼我們來看一下SERIAL_EXECUTOR:

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
現在,我們知道了實際上sDefaultExecutor是一個SerialExecutor對象,我們來看一下SerialExecutor類的源碼:

 1 private static class SerialExecutor implements Executor {
 2         final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
 3         Runnable mActive;
 4 
 5         public synchronized void execute(final Runnable r) {
 6             mTasks.offer(new Runnable() {
 7                 public void run() {
 8                     try {
 9                         r.run();
10                     } finally {
11                         scheduleNext();
12                     }
13                 }
14             });
15             if (mActive == null) {
16                 scheduleNext();
17             }
18         }
19 
20         protected synchronized void scheduleNext() {
21             if ((mActive = mTasks.poll()) != null) {
22                 THREAD_POOL_EXECUTOR.execute(mActive);
23             }
24         }
25     }

我們來看一下execute方法的實現。mTasks代表了SerialExecutor這個串行線程池的任務緩存隊列,在第6行,我們用offer方法向任務緩存隊列中添加一個任務,任務的內容如第7行到第13行的run方法定義所示。我們可以看到,run方法中:第9行調用了mFuture(第5行的參數r就是我們傳入的mFuture)的run方法,而mFuture的run方法內部會調用mWorker的call方法,然後就會調用doInBackground方法,我們的後臺任務也就開始執行了。那麼我們提交到任務緩存隊列中的任務什麼時候會被執行呢?我們接着往下看。

首先我們看到第三行定義了一個Runnable變量mActive,它代表了當前正在執行的AsyncTask對象。第15行判斷mActive是否爲null,若爲null,就調用scheduleNext方法。如第20行到24行所示,在scheduleNext方法中,若緩存隊列非空,則調用THREAD_POOL_EXECUTOR.execute方法執行從緩存隊列中取出的任務,這時我們的後臺任務便開始你真正執行了。

通過以上的分析,我們可以知道SerialExecutor所完成的工作主要是把任務加到任務緩存隊列中,而真正執行任務的是THREAD_POOL_EXECUTOR。我們來看下THREAD_POOL_EXECUTOR是什麼:

public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
從上面的代碼我們可以知道,它是一個線程池對象。根據AsyncTask的源碼,我們可以獲取它的各項參數如下:

 1 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
 2 private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
 3 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
 4 private static final int KEEP_ALIVE = 1;
 5 
 6 private static final ThreadFactory sThreadFactory = new ThreadFactory() {
 7     private final AtomicInteger mCount = new AtomicInteger(1);
 8 
 9     public Thread newThread(Runnable r) {
10         return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
11     }
12 };
13 
14 private static final BlockingQueue<Runnable> sPoolWorkQueue =
15             new LinkedBlockingQueue<Runnable>(128);

由以上代碼我們可以知道:

corePoolSize爲CPU數加一;
maximumPoolSize爲CPU數的二倍加一;
存活時間爲1秒;
任務緩存隊列爲LinkedBlockingQueue。
現在,我們已經瞭解到了從我們調用AsyncTask對象的execute方法開始知道後臺任務執行完都發生了什麼。現在讓我們回過頭來看一看之前提到的postResult方法的源碼:

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

在以上源碼中,先調用了getHandler方法獲取AsyncTask對象內部包含的sHandler,然後通過它發送了一個MESSAGE_POST_RESULT消息。我們來看看sHandler的相關代碼:

 1 private static final InternalHandler sHandler = new InternalHandler();
 2 
 3 private static class InternalHandler extends Handler {
 4         public InternalHandler() {
 5             super(Looper.getMainLooper());
 6         }
 7 
 8         @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
 9         @Override
10         public void handleMessage(Message msg) {
11             AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
12             switch (msg.what) {
13                 case MESSAGE_POST_RESULT:
14                     // There is only one result
15                     result.mTask.finish(result.mData[0]);
16                     break;
17                 case MESSAGE_POST_PROGRESS:
18                     result.mTask.onProgressUpdate(result.mData);
19                     break;
20             }
21         }
22 }

從以上代碼中我們可以看到,sHandler是一個靜態的Handler對象。我們知道創建Handler對象時需要當前線程的Looper,所以我們爲了以後能夠通過sHandler將執行環境從後臺線程切換到主線程(即在主線程中執行handleMessage方法),我們必須使用主線程的Looper,因此必須在主線程中創建sHandler。這也就解釋了爲什麼必須在主線程中加載AsyncTask類,是爲了完成sHandler這個靜態成員的初始化工作。

在以上代碼第10行開始的handleMessage方法中,我們可以看到,當sHandler收到MESSAGE_POST_RESULT方法後,會調用finish方法,finish方法的源碼如下:

1 private void finish(Result result) {
2         if (isCancelled()) {
3             onCancelled(result);
4         } else {
5             onPostExecute(result);
6         }
7         mStatus = Status.FINISHED;
8 }

在第2行,會通過調用isCancelled方法判斷AsyncTask任務是否被取消,若取消了則調用onCancelled方法,否則調用onPostExecute方法;在第7行,把mStatus設爲FINISHED,表示當前AsyncTask對象已經執行完畢。

經過了以上的分析,我們大概瞭解了AsyncTask的內部運行邏輯,知道了它默認使用串行方式執行任務。那麼如何讓它以並行的方式執行任務呢? 閱讀了以上的代碼後,我們不難得到結論,只需調用executeOnExecutor方法,並傳入THREAD_POOL_EXECUTOR作爲其線程池即可。

項目應用

public void login ( ) {
    AsyncTask<Void, String, Void> login = new AsyncTask<Void, String, Void>() {
        @Nullable
        protected Void doInBackground(Void... arg0) {
            try {
                socket = new Socket(host, Integer.valueOf(port));                               //連接服務器
                writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));  //創建寫緩衝
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));    //創建讀緩衝
                writer.write(account+psw);
                writer.flush();
                String line;
                line=reader.readLine();
                if (line!=null&&line.equals("Ca/ok")) {
                    publishProgress("@link_success");
                } else if(line!=null&&line.equals("Ca/ok no")){
                    publishProgress("@link_another");
                }else{
                    publishProgress("@link_defeat");
                }
                while((line=reader.readLine())!="Cb/ok"){
                    publishProgress(line);
                }
            } catch (IOException e) {
                publishProgress("@link_error");
                e.printStackTrace();
            }
            return null;
        }
        protected void onProgressUpdate(String... values) {
            if(values[0].equals("@link_error")) {
                Toast.makeText(LoginActivity.this, "連接錯誤", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xffcc0000));
            }else if(values[0].equals("@link_success")){
                Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xff99cc00));
                progressBar_state.setProgress(100);
            } else if(values[0].equals("@link_defeat")){
                Toast.makeText(LoginActivity.this, "登錄失敗", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xffffbb33));
                progressBar_state.setProgress(50);
            }
            else if(values[0].equals("@link_another")){
                Toast.makeText(LoginActivity.this, "重複登錄", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xffffbb33));
                progressBar_state.setProgress(50);
            }else {
                Locale local=new Locale("CHINESE");
                SimpleDateFormat formatter = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss",local);
                Date curDate = new Date(System.currentTimeMillis());//獲取當前時間
                String str = formatter.format(curDate);
                str=str.substring(12);
                Toast.makeText(LoginActivity.this, "["+str+"] Receive:"+values[0], Toast.LENGTH_SHORT).show();
                super.onProgressUpdate(values);
            }
        }
    };
    login.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

public void logout( ) {
    AsyncTask<Void, String, Void> logout = new AsyncTask<Void, String, Void>() {
        @Nullable
        protected Void doInBackground(Void... arg0) {
            try {
                writer.write("Cb\n");
                writer.flush();
                String line;
                line=reader.readLine();
                if (line != null && line.equals("Cb/ok")) {
                    socket.close();
                    publishProgress("@logout_success");
                } else {
                    publishProgress("@logout_defeat");
                }
            } catch (IOException e) {
                publishProgress("@logout_error");
                e.printStackTrace();
            }
            return null;
        }
////////////////////////////////////////////////////////////
        protected void onProgressUpdate(String... values) {
            if (values[0].equals("@logout_success")) {
                Toast.makeText(LoginActivity.this, "退出成功", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xffffbb33));
                progressBar_state.setProgress(50);
            } else if (values[0].equals("@logout_error")) {
                Toast.makeText(LoginActivity.this, "退出失敗", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(LoginActivity.this, "退出錯誤", Toast.LENGTH_SHORT).show();
                progressBar_state.setProgressTintList(ColorStateList.valueOf(0xffcc0000));
            }
        }
    };
    logout.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

此項目使用AsyncTask來進行異步無阻塞的網絡連接,網絡處理部分放在doInBackground()方法裏面,對處理信息以字符串的的形式,通過publishProgress()方法返回,然後在onProgressUpdate()方法裏針對返回信息更改UI。

項目目的是開啓一個異步線程進行登錄服務器,服務器響應是否可以連接,連接成功後可一直讀取服務器端發送的數據指令。退出登錄時需給服務器發送斷開連接的請求,並等待服務器的回覆後斷開連接。

項目最初嘗試使用AsyncTask的executy()方法執行線程,因其默認以串行方式運行,當需要在連接之後一直接受服務器發送的指令時,無法在主線程裏響應註銷登錄的線程操作。這時便適合使用executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)方法進行並行操作。

發佈了34 篇原創文章 · 獲贊 21 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章