Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> AsyncTask原理

AsyncTask原理

編輯:關於Android編程

為什麼要用AsyncTask

我們知道,Android應用的主線程(UI 線程,是線程不安全的,負責前台用戶界面的繪制以及響應用戶的操作)肩負著繪制用戶界面和及時響應用戶操作的重任,為了避免“用戶點擊按鈕後沒反應”這樣的糟糕用戶體驗,我們就要確保主線程時刻保持著較高的響應性,主線程不能夠運行需要占用大量CPU時間片的任務(如大量復雜的浮點運算,較大的磁盤IO操作,網絡socket等)。為了做到這一點,我們就要把耗時的任務移出主線程,那麼耗時的任務交給誰來完成呢?答案就是工作者線程。如果想要在子線程裡進行UI操作,讓工作者線程在後台執行一些比較耗時的任務,就需要借助Android的異步消息處理機制。不過為了更加方便我們在子線程中更新UI元素,Android中的工作者線程主要有AsyncTask、IntentService、HandlerThread,它們本質上都是對線程或線程池的封裝。Android從1.5版本就引入了一個AsyncTask類,使用它就可以非常靈活方便地從子線程切換到UI線程,我們本篇文章的主角也就正是它了。

總的來說,我們使用工作者線程是因為主線程已經有很多活要干了,累活就得交給別人干。AsyncTask是我們日常中廣泛使用的一種工作者線程,它的方便之處在於可以在後台任務執行完畢時根據返回結果相應的更新UI。AsyncTask很早就出現在Android的API裡了,所以我相信大多數朋友對它的用法都已經非常熟悉。不過今天我還是准備從AsyncTask的基本用法開始講起,然後我們再來一起分析下AsyncTask源碼,看看它是如何實現的,最後我會介紹一些關於AsyncTask你所不知道的秘密。

怎麼用AsyncTask

AsyncTask是對Handler與線程池的封裝。使用它的方便之處在於能夠更新用戶界面,當然這裡更新用戶界面的操作還是在主線程中完成的,但是由於AsyncTask內部包含一個Handler,所以可以發送消息給主線程讓它更新UI。另外,AsyncTask內還包含了一個線程池。使用線程池的主要原因是避免不必要的創建及銷毀線程的開銷。設想下面這樣一個場景:有100個只需要0.001ms就能執行完畢的任務,如果創建100個線程來執行這些任務,執行完任務的線程就進行銷毀。那麼創建與銷毀進程的開銷就很可能成為了影響性能的瓶頸。通過使用線程池,我們可以實現維護固定數量的線程,不管有多少任務,我們都始終讓線程池中的線程輪番上陣,這樣就避免了不必要的開銷。

在這裡簡單介紹下AsyncTask的使用方法,為後文對它的工作原理的研究做鋪墊,關於AsyncTask的詳細介紹大家可以參考官方文檔或是相關博文。

AsyncTask是一個抽象類,我們在使用時需要定義一個它的派生類並重寫相關方法。AsyncTask類的聲明如下:

public abstract class AsyncTask 

我們可以看到,AsyncTask是一個泛型類,它的三個類型參數的含義如下:

Params:在執行AsyncTask時需要傳入的參數,可用於在後台任務中使用。 Progress:後台任務執行時,如果需要在界面上顯示當前的進度,則使用這裡指定的泛型作為進度單位。 Result:當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值類型。

一個最簡單的自定義AsyncTask就可以寫成如下方式:

class DownloadTask extends AsyncTask {  
    ……  
} 

這裡我們把AsyncTask的第一個泛型參數指定為Void,表示在執行AsyncTask的時候不需要傳入參數給後台任務。第二個泛型參數指定為Integer,表示使用整型數據來作為進度顯示單位。第三個泛型參數指定為Boolean,則表示使用布爾型數據來反饋執行結果。

當然,目前我們自定義的DownloadTask還是一個空任務,並不能進行任何實際的操作,我們還需要去重寫AsyncTask中的幾個方法才能完成對任務的定制。

我們再來看一下AsyncTask類主要為我們提供了哪些方法:

onPreExecute() //此方法會在後台任務執行前被調用,用於進行一些准備工作,比如顯示一個進度條對話框等。
doInBackground(Params... params) //此方法中定義要執行的後台任務,在這個方法中可以調用publishProgress來更新任務進度(publishProgress內部會調用onProgressUpdate方法)
onProgressUpdate(Progress... values) //由publishProgress內部調用,表示任務進度更新
onPostExecute(Result result) //後台任務執行完畢後,此方法會被調用,參數即為後台任務的返回結果
onCancelled() //此方法會在後台任務被取消時被調用

以上方法中,除了doInBackground方法由AsyncTask內部線程池執行外,其余方法均在主線程中執行。

我們還是簡單介紹下AsyncTask一些使用示例。我們先新建一個類DemoAsyncTask繼承AsyncTask,因為AsyncTask是抽象類,其中doInBackground方法必須重寫。

private class DownloadTask extends AsyncTask {
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override  
    protected Boolean doInBackground(Void... params) {  
        try {  
            while (true) {  
                int downloadPercent = doDownload();  
                publishProgress(downloadPercent);  
                if (downloadPercent >= 100) {  
                    break;  
                }  
            }  
        } catch (Exception e) {  
            return false;  
        }  
        return true;  
    }   

     @Override  
    protected void onPostExecute(Boolean result) {  
        progressDialog.dismiss();  
        if (result) {  
            Toast.makeText(context, "下載成功", Toast.LENGTH_SHORT).show();  
        } else {  
            Toast.makeText(context, "下載失敗", Toast.LENGTH_SHORT).show();  
        }  
    }

    @Override  
    protected void onProgressUpdate(Integer... values) {  
        progressDialog.setMessage("當前下載進度:" + values[0] + "%");  
    } 

    @Override
    protected void onCancelled(Void aVoid) {
        super.onCancelled(aVoid);
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();
    }
}

DemoAsyncTask task = new DemoAsyncTask();
task.execute("demo test AsyncTask");
//task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, "test");
//myTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "test");

這裡我們模擬了一個下載任務,在doInBackground()方法中去執行具體的下載邏輯,在onProgressUpdate()方法中顯示當前的下載進度,在onPostExecute()方法中來提示任務的執行結果。如果想要啟動這個任務,只需要簡單地調用以下代碼即可:

new DownloadTask().execute();  

AsyncTask的局限性

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

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

接下來,我們從源碼的角度去探究一下AsyncTask的工作原理,並嘗試著搞清楚為什麼會存在以上局限性。

探索AsyncTask的工作原理

雖然AsyncTask這麼簡單好用,但你知道它是怎樣實現的嗎?那麼接下來,我們就來分析一下AsyncTask的源碼,對它的實現原理一探究竟。注意這裡我選用的是Android 4.0的源碼,如果你查看的是其它版本的源碼,可能會有一些出入。

從之前DownloadTask的代碼就可以看出,在啟動某一個任務之前,要先new出它的實例,因此,我們就先來看一看AsyncTask構造函數中的源碼,如下所示:

public AsyncTask() {  
    mWorker = new WorkerRunnable() {  
        public Result call() throws Exception {  
            mTaskInvoked.set(true);  
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
            return postResult(doInBackground(mParams));  
        }  
    };  
    mFuture = new FutureTask(mWorker) {  
        @Override  
        protected void done() {  
            try {  
                final Result result = get();  
                postResultIfNotInvoked(result);  
            } catch (InterruptedException e) {  
                android.util.Log.w(LOG_TAG, e);  
            } catch (ExecutionException e) {  
                throw new RuntimeException("An error occured while executing doInBackground()",  
                        e.getCause());  
            } catch (CancellationException e) {  
                postResultIfNotInvoked(null);  
            } catch (Throwable t) {  
                throw new RuntimeException("An error occured while executing "  
                        + "doInBackground()", t);  
            }  
        }  
    };  
}  

這段代碼雖然看起來有點長,但實際上並沒有任何具體的邏輯會得到執行,只是初始化了兩個變量,並在初始化mFuture的時候將mWorker作為參數傳入。在第2行到第8行,初始化了mWorker,它是一個派生自WorkRunnable類的對象。WorkRunnable是一個抽象類,它實現了Callable< Result>接口,Callback接口是JDK1.5加入的高級並發架包裡面的一個接口,它可以有一個泛型返回值。我們再來看一下第3行開始的call方法的定義,首先將mTaskInvoked設為true表示當前任務已被調用過,然後在第5行設置線程的優先級。在第6行我們可以看到,調用了AsyncTask對象的doInBackground方法開始執行我們所定義的後台任務,並獲取返回結果存入result中。最後將任務返回結果傳遞給postResult方法。關於postResult方法我們會在下文進行分析。由此我們可以知道,實際上AsyncTask的成員mWorker包含了AyncTask最終要執行的任務(即mWorker的call方法)。

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

public FutureTask(Callable 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()方法,因此現在我們來看一看execute()方法的源碼,如下所示:

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

簡單的有點過分了,只有一行代碼,僅是調用了executeOnExecutor()方法,我們可以看到它接收的參數是Params類型的參數,這個參數會一路傳遞到doInBackground方法中。execute方法僅僅是調用了executeOnExecutor方法,並將executeOnExecutor方法的返回值作為自己的返回值。我們注意到,傳入了sDefaultExecutor作為executeOnExecutor方法的參數,那麼sDefaultExecutor是什麼呢?簡單的說,它是AsyncTask的默認執行器(線程池)。AsyncTask可以以串行(一個接一個的執行)或並行(一並執行)兩種方式來執行後台任務,在Android3.0及以後的版本中,默認的執行方式是串行。這個sDefaultExecutor就代表了默認的串行執行器(線程池)。也就是說我們平常在AsyncTask對象上調用execute方法,使用的是串行方式來執行後台任務。那麼具體的邏輯就應該寫在這個方法裡了,快跟進去瞧一瞧:

public final AsyncTask executeOnExecutor(Executor exec,  
        Params... params) {  
    if (mStatus != Status.PENDING) {  
        switch (mStatus) {  
            case RUNNING:  
                throw new IllegalStateException("Cannot execute task:"  
                        + " the task is already running.");  
            case FINISHED:  
                throw new IllegalStateException("Cannot execute task:"  
                        + " the task has already been executed "  
                        + "(a task can be executed only once)");  
        }  
    }  
    mStatus = Status.RUNNING;  
    onPreExecute();  
    mWorker.mParams = params;  
    exec.execute(mFuture);  
    return this;  
} 

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

在第15行調用了onPreExecute()方法,因此證明了onPreExecute()方法會第一個得到執行。可是接下來的代碼就看不明白了,怎麼沒見到哪裡有調用doInBackground()方法呢?

接著我們看到,在第17行調用了Executor的execute()方法,並將前面初始化的mFuture對象傳了進去,那麼這個Executor對象又是什麼呢?查看上面的execute()方法,原來是傳入了一個sDefaultExecutor變量,接著找一下這個sDefaultExecutor變量是在哪裡定義的,源碼如下所示:

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();  
……  
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;  

可以看到,這裡先new出了一個SERIAL_EXECUTOR常量,然後將sDefaultExecutor的值賦值為這個常量,也就是說明,剛才在executeOnExecutor()方法中調用的execute()方法,其實也就是調用的SerialExecutor類中的execute()方法。那麼我們自然要去看看SerialExecutor的源碼了,如下所示:

private static class SerialExecutor implements Executor {  
    final ArrayDeque mTasks = new ArrayDeque();  
    Runnable mActive;  

    public synchronized void execute(final Runnable r) {  
        mTasks.offer(new Runnable() {  
            public void run() {  
                try {  
                    r.run();  
                } finally {  
                    scheduleNext();  
                }  
            }  
        });  
        if (mActive == null) {  
            scheduleNext();  
        }  
    }  

    protected synchronized void scheduleNext() {  
        if ((mActive = mTasks.poll()) != null) {  
            THREAD_POOL_EXECUTOR.execute(mActive);  
        }  
    }  
} 

我們來看一下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的源碼,我們可以獲取它的各項參數如下:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final BlockingQueue sPoolWorkQueue =
            new LinkedBlockingQueue(128);

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

corePoolSize為CPU數加一; maximumPoolSize為CPU數的二倍加一; 存活時間為1秒; 任務緩存隊列為LinkedBlockingQueue。

SerialExecutor類中也有一個execute()方法,這個方法裡的所有邏輯就是在子線程中執行的了,注意這個方法有一個Runnable參數,那麼目前這個參數的值是什麼呢?當然就是mFuture對象了,也就是說在第9行我們要調用的是FutureTask類的run()方法,而在這個方法裡又會去調用Sync內部類的innerRun()方法,因此我們直接來看innerRun()方法的源碼:

void innerRun() {  
    if (!compareAndSetState(READY, RUNNING))  
        return;  
    runner = Thread.currentThread();  
    if (getState() == RUNNING) { // recheck after setting thread  
        V result;  
        try {  
            result = callable.call();  
        } catch (Throwable ex) {  
            setException(ex);  
            return;  
        }  
        set(result);  
    } else {  
        releaseShared(0); // cancel  
    }  
}  

可以看到,在第8行調用了callable的call()方法,那麼這個callable對象是什麼呢?其實就是在初始化mFuture對象時傳入的mWorker對象了,此時調用的call()方法,也就是一開始在AsyncTask的構造函數中指定的,我們把它單獨拿出來看一下,代碼如下所示:

public Result call() throws Exception {  
    mTaskInvoked.set(true);  
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
    return postResult(doInBackground(mParams));  
}  

在postResult()方法的參數裡面,我們終於找到了doInBackground()方法的調用處,雖然經過了很多周轉,但目前的代碼仍然是運行在子線程當中的,所以這也就是為什麼我們可以在doInBackground()方法中去處理耗時的邏輯。接著將doInBackground()方法返回的結果傳遞給了postResult()方法,這個方法的源碼如下所示:

private Result postResult(Result result) {  
    Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,  
            new AsyncTaskResult(this, result));  
    message.sendToTarget();  
    return result;  
}  

如果你已經熟悉了異步消息處理機制,這段代碼對你來說一定非常簡單吧。這裡使用sHandler對象發出了一條消息,消息中攜帶了MESSAGE_POST_RESULT常量和一個表示任務執行結果的AsyncTaskResult對象。這個sHandler對象是InternalHandler類的一個實例,那麼稍後這條消息肯定會在InternalHandler的handleMessage()方法中被處理。InternalHandler的源碼如下所示:

private static class InternalHandler extends Handler {  
    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})  
    @Override  
    public void handleMessage(Message msg) {  
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;  
        switch (msg.what) {  
            case MESSAGE_POST_RESULT:  
                // There is only one result  
                result.mTask.finish(result.mData[0]);  
                break;  
            case MESSAGE_POST_PROGRESS:  
                result.mTask.onProgressUpdate(result.mData);  
                break;  
        }  
    }  
}  

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

這裡對消息的類型進行了判斷,如果這是一條MESSAGE_POST_RESULT消息,就會去執行finish()方法,如果這是一條MESSAGE_POST_PROGRESS消息,就會去執行onProgressUpdate()方法。那麼finish()方法的源碼如下所示:

private void finish(Result result) {  
    if (isCancelled()) {  
        onCancelled(result);  
    } else {  
        onPostExecute(result);  
    }  
    mStatus = Status.FINISHED;  
}

在第2行,會通過調用isCancelled方法判斷AsyncTask任務是否被取消,若取消了則調用onCancelled()方法,否則調用onPostExecute()方法;在第7行,把mStatus設為FINISHED,表示當前AsyncTask對象已經執行完畢,這樣當前任務的執行就全部結束了。

我們注意到,在剛才InternalHandler的handleMessage()方法裡,還有一種MESSAGE_POST_PROGRESS的消息類型,這種消息是用於當前進度的,調用的正是onProgressUpdate()方法,那麼什麼時候才會發出這樣一條消息呢?相信你已經猜到了,查看publishProgress()方法的源碼,如下所示:

protected final void publishProgress(Progress... values) {  
    if (!isCancelled()) {  
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS,  
                new AsyncTaskResult
(this, values)).sendToTarget(); } }

讀到這裡,非常清晰了吧!現在其實我們已經把AsyncTask整個執行任務的過程走完了,其中暴露給我們的那幾個回調方法也都走到了。正因如此,在doInBackground()方法中調用publishProgress()方法才可以從子線程切換到UI線程,從而完成對UI元素的更新操作。其實也沒有什麼神秘的,因為說到底,AsyncTask也是使用的異步消息處理機制,只是做了非常好的封裝而已。

現在我們回過頭來看,AsyncTask其實只是對JDK 1.5提供的高級並發特性,concurrent架包做的一個封裝,方便開發者來處理異步任務,當然裡面還有很多細節處理的方法值得大家學習,如任務執行進度的反饋,任務執行原子性的保證等,這些留給大家自己學習了。

相信你對AsyncTask中的每個回調方法的作用、原理、以及何時會被調用都已經搞明白了吧。

關於AsyncTask你所不知道的秘密

不得不說,剛才我們在分析SerialExecutor的時候,其實並沒有分析的很仔細,僅僅只是關注了它會調用mFuture中的run()方法,但是至於什麼時候會調用我們並沒有進一步地研究。其實SerialExecutor也是AsyncTask在3.0版本以後做了最主要的修改的地方,它在AsyncTask中是以常量的形式被使用的,因此在整個應用程序中的所有AsyncTask實例都會共用同一個SerialExecutor。下面我們就來對這個類進行更加詳細的分析,為了方便閱讀,我把它的代碼再貼出來一遍:

private static class SerialExecutor implements Executor {  
    final ArrayDeque mTasks = new ArrayDeque();  
    Runnable mActive;  

    public synchronized void execute(final Runnable r) {  
        mTasks.offer(new Runnable() {  
            public void run() {  
                try {  
                    r.run();  
                } finally {  
                    scheduleNext();  
                }  
            }  
        });  
        if (mActive == null) {  
            scheduleNext();  
        }  
    }  

    protected synchronized void scheduleNext() {  
        if ((mActive = mTasks.poll()) != null) {  
            THREAD_POOL_EXECUTOR.execute(mActive);  
        }  
    }  
}  

可以看到,SerialExecutor是使用ArrayDeque這個隊列來管理Runnable對象的,如果我們一次性啟動了很多個任務,首先在第一次運行execute()方法的時候,會調用ArrayDeque的offer()方法將傳入的Runnable對象添加到隊列的尾部,然後判斷mActive對象是不是等於null,第一次運行當然是等於null了,於是會調用scheduleNext()方法。在這個方法中會從隊列的頭部取值,並賦值給mActive對象,然後調用THREAD_POOL_EXECUTOR去執行取出的取出的Runnable對象。之後如何又有新的任務被執行,同樣還會調用offer()方法將傳入的Runnable添加到隊列的尾部,但是再去給mActive對象做非空檢查的時候就會發現mActive對象已經不再是null了,於是就不會再調用scheduleNext()方法。

那麼後面添加的任務豈不是永遠得不到處理了?當然不是,看一看offer()方法裡傳入的Runnable匿名類,這裡使用了一個try finally代碼塊,並在finally中調用了scheduleNext()方法,保證無論發生什麼情況,這個方法都會被調用。也就是說,每次當一個任務執行完畢後,下一個任務才會得到執行,SerialExecutor模仿的是單一線程池的效果,如果我們快速地啟動了很多任務,同一時刻只會有一個線程正在執行,其余的均處於等待狀態。Android照片牆應用實現,再多的圖片也不怕崩潰 這篇文章中例子的運行結果也證實了這個結論。

不過你可能還不知道,在Android 3.0之前是並沒有SerialExecutor這個類的,那個時候是直接在AsyncTask中構建了一個sExecutor常量,並對線程池總大小,同一時刻能夠運行的線程數做了規定,代碼如下所示:

private static final int CORE_POOL_SIZE = 5;  
private static final int MAXIMUM_POOL_SIZE = 128;  
private static final int KEEP_ALIVE = 10;  
……  
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,  
        MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);  

可以看到,這裡規定同一時刻能夠運行的線程數為5個,線程池總大小為128。也就是說當我們啟動了10個任務時,只有5個任務能夠立刻執行,另外的5個任務則需要等待,當有一個任務執行完畢後,第6個任務才會啟動,以此類推。而線程池中最大能存放的線程數是128個,當我們嘗試去添加第129個任務時,程序就會崩潰。

因此在3.0版本中AsyncTask的改動還是挺大的,在3.0之前的AsyncTask可以同時有5個任務在執行,而3.0之後的AsyncTask同時只能有1個任務在執行。為什麼升級之後可以同時執行的任務數反而變少了呢?這是因為更新後的AsyncTask已變得更加靈活,如果不想使用默認的線程池,還可以自由地進行配置。比如使用如下的代碼來啟動任務:

Executor exec = new ThreadPoolExecutor(15, 200, 10,  
        TimeUnit.SECONDS, new LinkedBlockingQueue());  
new DownloadTask().executeOnExecutor(exec); 

這樣就可以使用我們自定義的一個Executor來執行任務,而不是使用SerialExecutor。上述代碼的效果允許在同一時刻有15個任務正在執行,並且最多能夠存儲200個任務。

使用AsyncTask一點小技巧

我們以一個實例來說明,“點擊按鈕開始下載QQAndroid安裝包,然後顯示一個對話框來反饋下載進度”。我們先初始化一個對話框,由於要顯示進度,我們用Github上面一個能夠顯示百分比的進度條 NumberProgressbar,啟動任務的按鈕我們使用 circlebutton,一個有酷炫動畫的按鈕,Github上面有很多非常好的開源項目,當然炫酷的控件是其中一部分了,後面有機會,會去學習一些比較流行的控件它們的實現原理,今天就暫且拿來主義了~~。

1.先初始化進度條提示對話框。

builder = new AlertDialog.Builder(MainActivity.this);
LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
mDialogView = inflater.inflate(R.layout.progress_dialog_layout, null);
mNumberProgressBar = (NumberProgressBar)mDialogView.findViewById(R.id.number_progress_bar);
builder.setView(mDialogView);
mDialog = builder.create();

2.設置按鈕點擊事件。

findViewById(R.id.circle_btn).setOnClickListener(new View.OnClickListener(){
      @Override
      public void onClick(View v) {
          dismissDialog();
          mNumberProgressBar.setProgress(0);
          myTask = new MyAsyncTask();
          myTask.execute(qqDownloadUrl);
      }
  });

3.DownloadAsyncTask實現,有點長。

private class DownloadAsyncTask extends AsyncTask {

  @Override
  protected void onPreExecute() {
      super.onPreExecute();
      mDialog.show();
  }

  @Override
  protected void onPostExecute(String aVoid) {
      super.onPostExecute(aVoid);
      dismissDialog();
  }

  @Override
  protected void onProgressUpdate(Integer... values) {
      super.onProgressUpdate(values);
      mNumberProgressBar.setProgress(values[0]);
  }

  @Override
  protected void onCancelled(String aVoid) {
      super.onCancelled(aVoid);
      dismissDialog();
  }

  @Override
  protected void onCancelled() {
      super.onCancelled();
      dismissDialog();
  }

  @Override
  protected String doInBackground(String... params) {
      String urlStr = params[0];
      FileOutputStream output = null;
      try {
          URL url = new URL(urlStr);
          HttpURLConnection connection = (HttpURLConnection)url.openConnection();
          String qqApkFile = "qqApkFile";
          File file = new File(Environment.getExternalStorageDirectory() + "/" + qqApkFile);
          if (file.exists()) {
              file.delete();
          }
          file.createNewFile();
          InputStream input = connection.getInputStream();
          output = new FileOutputStream(file);
          int total = connection.getContentLength();
          if (total <= 0) {
              return null;
          }
          int plus = 0;
          int totalRead = 0;
          byte[] buffer = new byte[4*1024];
          while((plus = input.read(buffer)) != -1){
              output.write(buffer);
              totalRead += plus;
              publishProgress(totalRead * 100 / total);
              if (isCancelled()) {
                  break;
              }
          }
          output.flush();
      } catch (MalformedURLException e) {
          e.printStackTrace();
          if (output != null) {
              try {
                  output.close();
              } catch (IOException e2) {
                  e2.printStackTrace();
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          if (output != null) {
              try {
                  output.close();
              } catch (IOException e2) {
                  e2.printStackTrace();
              }
          }
      } finally {
          if (output != null) {
              try {
                  output.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      return null;
  }
}

這樣一個簡單的下載文件文件就基本實現了,到目前為止談不上技巧,但是現在我們有一個問題,就是如果我們的Activity正在後台執行一個任務,可能耗時較長,那用戶可能會點擊返回退出Activity或者退出App,那麼後台任務不會立即退出,如果AsyncTask內部有Activity中成員變量的引用,還會造成Activity的回收延時,造成一段時間內的內存洩露,所以我們需要加上下面的第四步處理。

4.onPause中判斷應用是否要退出,從而決定是否取消AsyncTask執行。

@Override
protected void onPause() {
  super.onPause();
  if (myTask != null && isFinishing()) {
      myTask.cancel(false);
  }
}

這樣我們的異步任務就會在Activity退出時,也隨之取消任務執行,順利被系統銷毀回收,第四步很多時候會被遺漏,而且一般也不會有什麼致命的問題,但是一旦出問題了,就很難排查,所以遵循編碼規范還是有必要的。

希望大家讀完能有所收獲O(∩_∩)O~

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved