1. 程式人生 > >AsyncTask原始碼分析及其常見記憶體洩露

AsyncTask原始碼分析及其常見記憶體洩露

原理:

在使用AsyncTask時,一般會繼承AsyncTask並重寫doInBackground方法,onPostExecute方法,在doInBackground方法中做耗時操作,在onPostExecute方法中更新UI。常見的洩露的場景是,當Activity onDestroy方法回撥後,AsyncTask的方法沒有執行完成,或者是在doInBackground方法中,或者是在onPostExecute方法中,而AsyncTask持有Activity的引用(一般是非靜態內部類持有外部類的引用和匿名記憶體類持有外部類的引用兩種形式),導致Activity無法及時回收,從而導致記憶體洩露。

AsyncTask的設計其實是對Handler+Thread的封裝,AsyncTask的執行是通過呼叫execute方法,執行背後是有一個執行緒池。

private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;

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<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);

// AsyncTask的執行預設是靠sDefaultExecutor的排程
@MainThread    
public final AsyncTask<Params, Progress, Result> execute(Params... params) {                    
    return executeOnExecutor(sDefaultExecutor, params);    
}

sDefaultExecutor在AsyncTask中是以常量形式存在,所有AsyncTask的例項公用一個sDefaultExecutor,在AsyncTask叫SERIAL_EXECUTOR,翻譯過來是線性執行器的意思,其實就是化並行為序列的意思。

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        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物件,如果一次性啟動了多個任務,首先第一個Task執行execute方法時,呼叫ArrayDeque的offer將傳入的Runnable物件新增至佇列尾部,然後判斷mActive是否為null,第一次執行時為null,會呼叫scheduleNext方法,在scheduleNext方法中賦值mActive,通過THREAD_POOL_EXECUTOR排程,之後再有新的任務被執行時,同樣會呼叫offer方法將傳入的Runnable物件新增至佇列的尾部,但此時mActive不在為null,於是不會執行scheduleNext方法,也就是說不會得到立即執行,那什麼時候會執行呢?看finally中,同樣會呼叫scheduleNext方法,也就是說,當此Task執行完成後,會去執行下一個Task,SerialExecutor模仿的是單一執行緒池的效果,如果我們快速地啟動了很多工,同一時刻只會有一個執行緒正在執行,其餘的均處於等待狀態

假設使用者開啟某個頁面,而此頁面有Task在執行,再開啟另外一個頁面,這個頁面還有Task需要執行,這個時候很可能會出現卡一個的情況,不是硬體配置差,而是軟體質量差導致的。

修復:

1: cancel + isCancelled

2:建議在修復方案1的基礎上將AsyncTask作為靜態內部類存在(與Handler處理方式相似),避免內部類的this$0持有外部類的引用但不推薦只修改AsyncTask為靜態內部類的方案,雖然不是洩露了,但沒有根本上解決問題~

3:如果AsyncTask中需要使用Context,建議使用weakreference

cancel方法可能不會得到立即執行,在介面呼叫處也有如下說明:

    /**
     * <p>Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when <tt>cancel</tt> is called,
     * this task should never run. If the task has already started,
     * then the <tt>mayInterruptIfRunning</tt> parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.</p>
     *
     * <p>Calling this method will result in {@link #onCancelled(Object)} being
     * invoked on the UI thread after {@link #doInBackground(Object[])}
     * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
     * is never invoked. After invoking this method, you should check the
     * value returned by {@link #isCancelled()} periodically from
     * {@link #doInBackground(Object[])} to finish the task as early as
     * possible.</p>
     *
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *        task should be interrupted; otherwise, in-progress tasks are allowed
     *        to complete.
     *
     * @return <tt>false</tt> if the task could not be cancelled,
     *         typically because it has already completed normally;
     *         <tt>true</tt> otherwise
     *
     * @see #isCancelled()
     * @see #onCancelled(Object)
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }

如果任務沒有執行且cancel方法被呼叫,那麼任務會被立即取消且確保不會被執行,當任務已經啟動了,mayInterruptIfRunning引數決定是否嘗試去停止Task。呼叫cancel方法能確保onPostdExecute方法不會被執行,執行了cancel方法,不會立即終止任務,會等doInBackground方法執行完成後返回,然後定期通過呼叫isCancelled方法檢查task狀態儘早的結束task。意思是,AsyncTask不會立即結束一個正在執行的執行緒,呼叫cancel方法只是給AsyncTask設定了"cancelled"狀態,並不是停止Task,那麼有人說是不是由mayInterruptIfRunning引數來控制?其實mayInterruptIfRunning只是執行執行緒的interrupt方法,並不是真正的中斷執行緒,而是通知執行緒應該中斷了~

簡單說下執行緒的interrupt:

1,如果執行緒處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼執行緒將立即退出被阻塞狀態,並丟擲一個InterruptedException例外。
2,如果執行緒處於正常活動狀態,那麼會將該執行緒的中斷標誌設定為 true(isInterrupted() == true)僅此而已。被設定中斷標誌的執行緒將繼續正常執行,不受影響。

真正決定任務取消的是需要手動呼叫isCancelled方法check task狀態,因此推薦的修復方案是在手動呼叫cancel方法的同時,能呼叫inCancelled方法檢測task狀態:

@Override
protected Integer doInBackground(Void... mgs) {
// Task被取消了,馬上退出
if(isCancelled()) return null;
.......
// Task被取消了,馬上退出

if(isCancelled()) return null;
}
...