1. 程式人生 > >Android中AsyncTask使用具體解釋

Android中AsyncTask使用具體解釋

pool 不同的 避免 創建 線程 觸發 contex uil text

在Android中我們能夠通過Thread+Handler實現多線程通信。一種經典的使用場景是:在新線程中進行耗時操作。當任務完畢後通過Handler向主線程發送Message。這樣主線程的Handler在收到該Message之後就能夠進行更新UI的操作。上述場景中須要分別在Thread和Handler中編寫代碼邏輯,為了使得代碼更加統一,我們能夠使用AsyncTask類。

AsyncTask是Android提供的一個助手類,它對Thread和Handler進行了封裝,方便我們使用。

Android之所以提供AsyncTask這個類,就是為了方便我們在後臺線程中運行操作。然後將結果發送給主線程,從而在主線程中進行UI更新等操作。在使用AsyncTask時,我們無需關註Thread和Handler,AsyncTask內部會對其進行管理,這樣我們就僅僅須要關註於我們的業務邏輯就可以。

AsyncTask有四個重要的回調方法,各自是:onPreExecute、doInBackground, onProgressUpdate 和 onPostExecute。

這四個方法會在AsyncTask的不同一時候期進行自己主動調用。我們僅僅須要實現這幾個方法的內部邏輯就可以。這四個方法的一些參數和返回值都是基於泛型的,並且泛型的類型還不一樣,所以在AsyncTask的使用中會遇到三種泛型參數:Params, Progress 和 Result,例如以下圖所看到的:
技術分享

  • Params表示用於AsyncTask運行任務的參數的類型
  • Progress表示在後臺線程處理的過程中,能夠階段性地公布結果的數據類型
  • Result表示任務所有完畢後所返回的數據類型

我們通過調用AsyncTask的execute()方法傳入參數並運行任務。然後AsyncTask會依次調用以下四個方法:

  • onPreExecute
    該方法的簽名例如以下所看到的:
    技術分享
    該方法有MainThread註解。表示該方法是運行在主線程中的。在AsyncTask運行了execute()方法後就會在UI線程上運行onPreExecute()方法,該方法在task真正運行前運行,我們通常能夠在該方法中顯示一個進度條。從而告知用戶後臺任務即將開始。

  • doInBackground
    該方法的簽名例如以下所看到的:
    技術分享
    該方法有WorkerThread

    註解,表示該方法是運行在單獨的工作線程中的。而不是運行在主線程中。doInBackground會在onPreExecute()方法運行完畢後馬上運行,該方法用於在工作線程中運行耗時任務,我們能夠在該方法中編寫我們須要在後臺線程中運行的邏輯代碼,因為是運行在工作線程中,所以該方法不會堵塞UI線程。

    該方法接收Params泛型參數,參數params是Params類型的不定長數組,該方法的返回值是Result泛型。因為doInBackgroud是抽象方法。我們在使用AsyncTask時必須重寫該方法。

    在doInBackground中運行的任務可能要分解為好多步驟。每完畢一步我們就能夠通過調用AsyncTask的publishProgress(Progress…)將階段性的處理結果公布出去。階段性處理結果是Progress泛型類型。當調用了publishProgress方法後,處理結果會被傳遞到UI線程中,並在UI線程中回調onProgressUpdate方法,以下會具體介紹。

    依據我們的具體須要,我們能夠在doInBackground中不調用publishProgress方法。當然也能夠在該方法中多次調用publishProgress方法。

    doInBackgroud方法的返回值表示後臺線程完畢任務之後的結果。

  • onProgressUpdate
    上面我們知道。當我們在doInBackground中調用publishProgress(Progress…)方法後。就會在UI線程上回調onProgressUpdate方法,該方法的方法簽名例如以下所看到的:
    技術分享
    該方法也具有MainThread註解,表示該方法是在主線程上被調用的。且傳入的參數是Progress泛型定義的不定長數組。

    假設在doInBackground中多次調用了publishProgress方法,那麽主線程就會多次回調onProgressUpdate方法。

  • onPostExecute
    該方法的簽名例如以下所看到的:
    技術分享
    該方法也具有MainThread註解,表示該方法是在主線程中被調用的。當doInBackgroud方法運行完畢後。就表示任務完畢了,doInBackgroud方法的返回值就會作為參數在主線程中傳入到onPostExecute方法中。這樣就能夠在主線程中依據任務的運行結果更新UI。

以下我們就以下載多個文件的演示樣例演示AsyncTask的使用過程。

布局文件例如以下所看到的:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button android:id="@+id/btnDownload"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="開始下載" />

    <TextView android:id="@+id/textView"
        android:layout_below="@id/btnDownload"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

界面上有一個“開始下載”的button,點擊該button就可以通過AsyncTask下載多個文件,相應的Java代碼例如以下所看到的:

package com.ispring.asynctask;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class MainActivity extends Activity implements Button.OnClickListener {

    TextView textView = null;
    Button btnDownload = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView)findViewById(R.id.textView);
        btnDownload = (Button)findViewById(R.id.btnDownload);
        Log.i("iSpring", "MainActivity -> onCreate, Thread name: " + Thread.currentThread().getName());
    }

    @Override
    public void onClick(View v) {
        //要下載的文件地址
        String[] urls = {
                "http://blog.csdn.net/iispring/article/details/47115879",
                "http://blog.csdn.net/iispring/article/details/47180325",
                "http://blog.csdn.net/iispring/article/details/47300819",
                "http://blog.csdn.net/iispring/article/details/47320407",
                "http://blog.csdn.net/iispring/article/details/47622705"
        };

        DownloadTask downloadTask = new DownloadTask();
        downloadTask.execute(urls);
    }

    //public abstract class AsyncTask<Params, Progress, Result>
    //在此例中。Params泛型是String類型,Progress泛型是Object類型,Result泛型是Long類型
    private class DownloadTask extends AsyncTask<String, Object, Long> {
        @Override
        protected void onPreExecute() {
            Log.i("iSpring", "DownloadTask -> onPreExecute, Thread name: " + Thread.currentThread().getName());
            super.onPreExecute();
            btnDownload.setEnabled(false);
            textView.setText("開始下載...");
        }

        @Override
        protected Long doInBackground(String... params) {
            Log.i("iSpring", "DownloadTask -> doInBackground, Thread name: " + Thread.currentThread().getName());
            //totalByte表示所有下載的文件的總字節數
            long totalByte = 0;
            //params是一個String數組
            for(String url: params){
                //遍歷Url數組,依次下載相應的文件
                Object[] result = downloadSingleFile(url);
                int byteCount = (int)result[0];
                totalByte += byteCount;
                //在下載完一個文件之後,我們就把階段性的處理結果公布出去
                publishProgress(result);
                //假設AsyncTask被調用了cancel()方法。那麽任務取消,跳出for循環
                if(isCancelled()){
                    break;
                }
            }
            //將總共下載的字節數作為結果返回
            return totalByte;
        }

        //下載文件後返回一個Object數組:下載文件的字節數以及下載的博客的名字
        private Object[] downloadSingleFile(String str){
            Object[] result = new Object[2];
            int byteCount = 0;
            String blogName = "";
            HttpURLConnection conn = null;
            try{
                URL url = new URL(str);
                conn = (HttpURLConnection)url.openConnection();
                InputStream is = conn.getInputStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buf = new byte[1024];
                int length = -1;
                while ((length = is.read(buf)) != -1) {
                    baos.write(buf, 0, length);
                    byteCount += length;
                }
                String respone = new String(baos.toByteArray(), "utf-8");
                int startIndex = respone.indexOf("<title>");
                if(startIndex > 0){
                    startIndex += 7;
                    int endIndex = respone.indexOf("</title>");
                    if(endIndex > startIndex){
                        //解析出博客中的標題
                        blogName = respone.substring(startIndex, endIndex);
                    }
                }
            }catch(MalformedURLException e){
                e.printStackTrace();
            }catch(IOException e){
                e.printStackTrace();
            }finally {
                if(conn != null){
                    conn.disconnect();
                }
            }
            result[0] = byteCount;
            result[1] = blogName;
            return result;
        }

        @Override
        protected void onProgressUpdate(Object... values) {
            Log.i("iSpring", "DownloadTask -> onProgressUpdate, Thread name: " + Thread.currentThread().getName());
            super.onProgressUpdate(values);
            int byteCount = (int)values[0];
            String blogName = (String)values[1];
            String text = textView.getText().toString();
            text += "\n博客《" + blogName + "》下載完畢。共" + byteCount + "字節";
            textView.setText(text);
        }

        @Override
        protected void onPostExecute(Long aLong) {
            Log.i("iSpring", "DownloadTask -> onPostExecute, Thread name: " + Thread.currentThread().getName());
            super.onPostExecute(aLong);
            String text = textView.getText().toString();
            text += "\n所有下載完畢,總共下載了" + aLong + "個字節";
            textView.setText(text);
            btnDownload.setEnabled(true);
        }

        @Override
        protected void onCancelled() {
            Log.i("iSpring", "DownloadTask -> onCancelled, Thread name: " + Thread.currentThread().getName());
            super.onCancelled();
            textView.setText("取消下載");
            btnDownload.setEnabled(true);
        }
    }
}

點擊下載button後,界面例如以下所看到的:
技術分享

控制臺輸出例如以下所看到的:
技術分享

以下對以上代碼進行一下說明。

  1. 我們在MainActivity中定義了內部類DownloadTask,DownloadTask繼承自AsyncTask。在該例中,Params泛型是String類型,Progress泛型是Object類型,Result泛型是Long類型。

  2. 我們定義了一個Url字符串數組。將該數組傳遞給AsyncTask的execute方法,用於異步運行task。

  3. 在運行了downloadTask.execute(urls)之後。AsyncTask會自己主動回調onPreExecute方法,在該方法中我們將textView設置為“開始下載…”幾個字,告知用戶即將運行下載操作。通過控制臺輸出我們也能夠看出該方法是在主線程中運行的。

  4. 在運行了onPreExecute方法之後。AsyncTask會回調doInBackground方法,該方法中的輸入參數是String類型的不定長數組,此處的String就相應著Params泛型類型,我們在該方法中遍歷Url數組。依次下載相應的文件,當我們下載完一個文件。就相當於我們階段性地完畢了一部分任務,我們就通過調用publishProgress方法將階段性處理結果公布出去。在此例中我們將階段性的處理結果定義為Object類型,即Progress泛型類型。通過控制臺輸出我們能夠看出doInBackground方法是運行在新的工作線程”AsyncTask #1”中的,AsyncTask的工作線程都是以”AsyncTask #”然後加上數字作為名字。

    當所有文件下載完畢後。我們就能夠通過totalSize返回所有下載的字節數,返回值類型為Long。相應著AsyncTask中的Result泛型類型。

  5. 在doInBackground方法中,每當下載完一個文件。我們就會調用publishProgress方法公布階段性結果,之後AsyncTask會回調onProgressUpdate方法。在此例中,onProgressUpdate的參數為Object類型,相應著AsyncTask中的Progress泛型類型。通過控制臺輸出我們能夠發現。該方法是在主線程中調用的,在該方法中我們會通過textView更新UI。告知用戶哪個文件下載完畢了,這樣用戶體驗相對友好。

  6. 在整個doInBackground方法運行完畢後,AsyncTask就會回調onPostExecute方法,在該方法中我們再次通過textView更新UI告知用戶所有下載任務完畢了。

  7. 在通過execute方法運行了異步任務之後,能夠通過AsyncTask的cancel方法取消任務,取消任務後AsyncTask會回調onCancelled方法,這樣不會再調用onPostExecute方法。

在使用Android的過程中,有以下幾點須要註意:

  • AsyncTask的實例必須在主線程中創建。

  • AsyncTask的execute方法必須在主線程中調用。

  • onPreExecute()、onPostExecute(Result),、doInBackground(Params…) 和 onProgressUpdate(Progress…)這四個方法都是回調方法,Android會自己主動調用。我們不應自己調用。

  • 對於一個AsyncTack的實例。僅僅能運行一次execute方法,在該實例上第二次運行execute方法時就會拋出異常。

通過上面的演示樣例,大家應該熟悉了AsyncTask的使用流程。我們上面提到,對於某個AsyncTask實例,僅僅能運行一次execute方法。假設我們想並行地運行多個任務怎麽辦呢?我們能夠考慮實例化多個AsyncTask實例,然後分別調用各個實例的execute方法,為了探究效果,我們將代碼更改例如以下所看到的:

public void onClick(View v) {
        //要下載的文件地址
        String[] urls = {
                "http://blog.csdn.net/iispring/article/details/47115879",
                "http://blog.csdn.net/iispring/article/details/47180325",
                "http://blog.csdn.net/iispring/article/details/47300819",
                "http://blog.csdn.net/iispring/article/details/47320407",
                "http://blog.csdn.net/iispring/article/details/47622705"
        };

        DownloadTask downloadTask1 = new DownloadTask();
        downloadTask1.execute(urls);

        DownloadTask downloadTask2 = new DownloadTask();
        downloadTask2.execute(urls);
    }

在單擊了button之後,我們實例化了兩個DownloadTask,並分別運行其execute方法。運行後界面例如以下所看到的:
技術分享

控制臺輸出例如以下所看到的:
技術分享

我們觀察一下控制臺的輸出結果,能夠發現對於downloadTask1,doInBackground方法是運行在線程“AsyncTask #1”中的;對於downloadTask2,doInBackground方法是運行在線程”AsyncTask #2”中的,此時我們可能會覺得太好了,兩個AsyncTask實例分別在不同的線程中運行,實現了並行處理。此處真的是並行運行的嗎?

我們自己觀察控制臺輸出就能夠發現,downloadTask1的doInBackground方法運行後,下載了五個文件,並五次觸發了onProgressUpdate,在這之後才運行downloadTask2的doInBackground方法。我們對照上面的GIF圖也能夠發現。在downloadTask1依照順序下載完五篇文章之後,downloadTask2才開始依照順序下載五篇文章。綜上所述,我們能夠知道。默認情況下假設創建了AsyncTask創建了多個實例。並同一時候運行實例的各個execute方法。那麽這些實例的execute方法並非並行運行的,是串行運行的,即在第一個實例的doInBackground完畢任務後,第二個實例的doInBackgroud方法才會開始運行,然後再運行第三個實例的doInBackground方法… 那麽你可能會問,不正確啊,上面downloadTask1是運行在”AsyncTask #1”線程中的,downloadTask2是運行在”AsyncTask #2”線程中的。這明明是兩個線程啊!

事實上AsyncTask為downloadTask1開辟了名為”AsyncTask #1”的工作線程,在其完畢了任務之後可能就銷毀了,然後AsyncTask又為downloadTask2開辟了名為”AsyncTask #2”的工作線程。

AsyncTask在最早的版本號中用一個單一的後臺線程串行運行多個AsyncTask實例的任務,從Android 1.6(DONUT)開始,AsyncTask用線程池並行運行異步任務,可是從Android 3.0(HONEYCOMB)開始為了避免並行運行導致的常見錯誤,AsyncTask又開始默認用單線程作為工作線程處理多個任務。

從Android 3.0開始AsyncTask添加了executeOnExecutor方法。用該方法能夠讓AsyncTask並行處理任務。該方法的方法簽名例如以下所看到的:

public final AsyncTask<Params, Progress, Result> executeOnExecutor (Executor exec, Params... params)

第一個參數表示exec是一個Executor對象,為了讓AsyncTask並行處理任務。通常情況下我們此處傳入AsyncTask.THREAD_POOL_EXECUTOR就可以,AsyncTask.THREAD_POOL_EXECUTOR是AsyncTask中內置的一個線程池對象。當然我們也能夠傳入我們自己實例化的線程池對象。

第二個參數params表示的是要運行的任務的參數。

通過executeOnExecutor方法並行運行任務的演示樣例代碼例如以下所看到的:

public void onClick(View v) {
        if(Build.VERSION.SDK_INT >= 11){
            String[] urls = {
                    "http://blog.csdn.net/iispring/article/details/47115879",
                    "http://blog.csdn.net/iispring/article/details/47180325",
                    "http://blog.csdn.net/iispring/article/details/47300819",
                    "http://blog.csdn.net/iispring/article/details/47320407",
                    "http://blog.csdn.net/iispring/article/details/47622705"
            };

            DownloadTask downloadTask1 = new DownloadTask();
            downloadTask1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, urls);

            DownloadTask downloadTask2 = new DownloadTask();
            downloadTask2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, urls);
        }
    }

我們實例化了兩個DownloadTask的實例。然後運行了這兩個實例的executeOnExecutor方法,並將AsyncTask.THREAD_POOL_EXECUTOR作為Executor傳入。二者都接收相同的Url數組作為任務運行的參數。

點擊下載button後,運行完的界面例如以下所看到的:
技術分享

控制臺輸出例如以下所看到的:
技術分享

通過控制臺的輸出結果我們能夠看到。在downloadTask1運行了doInBackground方法後,downloadTask2也馬上運行了doInBackground方法。並且通過程序運行完的UI界面能夠看到在一個DownloadTask實例下載了一篇文章之後。還有一個DownloadTask實例也馬上下載了一篇文章,兩個DownloadTask實例交叉按順序下載文件,能夠看出這兩個AsyncTask的實例是並行運行的。

假設大家想了解AsyncTask的工作原理。可參見還有一篇博文《源代碼解析Android中AsyncTask的工作原理》 。

希望本文對大家使用AsyncTask的使用有所幫助!

相關閱讀:
我的Android博文整理匯總
源代碼解析Android中AsyncTask的工作原理
Android中Handler的使用
Android新線程中更新主線程UI中的View方法匯總

Android中AsyncTask使用具體解釋