【1】關於Android中工作者線程的思考

本文系技術小黑屋 2015 北京 GDG Devfest分享內容整理。

一、Why

在Android中,我們或多或少使用了工作者線程,比如Thread,AsyncTask,HandlerThread,甚至是自己創建的線程池,使用工作者線程我們可以將耗時的操作從主線程中移走。首先思考以下幾個問題?

  1. 在Android系統中爲什麼存在工作者線程呢?
  2. 常用的工作者線程有哪些不易察覺的問題呢?
  3. 關於工作者線程有哪些優化的方面呢?

本文將一一解答這些問題。

二、Android 的 UI 單線程模型

工作者線程的存在原因:

  1. 因爲Android的UI單線程模型,所有的UI相關的操作都需要在主線程(UI線程)執行
  2. Android中各大組件的生命週期回調都是位於主線程中,使得主線程的職責更重
  3. 如果不使用工作者線程爲主線程分擔耗時的任務,會造成應用卡頓,嚴重時可能出現ANR(Application Not Responding), 即程序未響應。

關於 ANR 默認情況下,在android 中 Activity 的最長執行時間是5秒,BroadcastReceiver的最長執行時間則是10秒。

在開發Android 應用時必須遵守單線程模型的原則:

  1. 不要阻塞UI線程
  2. 確保只在UI線程中訪問Android UI工具包

因而,在Android中使用工作者線程顯得勢在必行,如一開始提到那樣,在Android中工作者線程有很多,接下來我們將圍繞 AsyncTask,HandlerThread 等深入研究。

三、AsyncTask異步任務

AsyncTask 是 Android 框架提供給開發者的一個輔助抽象類,使用該類我們可以輕鬆的處理異步線程與主線程的交互,由於其便捷性,在Android工程中,AsyncTask 被廣泛使用。

定義如下:

public abstract class AsyncTask<Params, Progress, Result> {

}

然而 AsyncTask 並非一個完美的方案,使用它往往會存在一些問題。接下來將逐一列舉 AsyncTask 不容易被開發者察覺的問題。

3.1、AsyncTask 與 內存泄露

內存泄露是Android開發中常見的問題,只要開發者稍有不慎就有可能導致程序產生內存泄露,嚴重時甚至可能導致OOM(OutOfMemory,即內存溢出錯誤)。AsyncTask 也不例外,也有可能造成內存泄露。

以一個簡單的場景爲例:

在Activity中,通常我們這樣使用AsyncTask:

//In Activity
new AsyncTask<String, Void, Void>() {

    @Override
    protected Void doInBackground(String... params) {
        //some code
        return null;
    }
}.execute("hello world");

上述代碼使用的匿名內存類創建AsyncTask實例,然而在Java中,非靜態內存類會隱式持有外部類的實例引用,上面例子AsyncTask創建於Activity中,因而會隱式持有Activity的實例引用。

而在AsyncTask內部實現中, mFuture 同樣使用匿名內部類創建對象,而mFuture 會作爲執行任務加入到任務執行器中。

private final WorkerRunnable<Params, Result> mWorker;
public AsyncTask() {
    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            //some code
        }
    };
}

而mFuture加入任務執行器,實際上是放入了一個靜態成員變量SERIAL_EXECUTOR指向的對象SerialExecutor的一個ArrayDeque類型的集合中。

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                //fake code
                r.run();
            }
        });
    }
}

當任務處於排隊狀態,則Activity實例引用被靜態常量SERIAL_EXECUTOR 間接持有。

在通常情況下,當設備發生屏幕旋轉事件,當前的Activity被銷燬,新的Activity被創建,以此完成對佈局的重新加載。

而本例中,當屏幕旋轉時,處於排隊的AsyncTask由於其對Activity實例的引用關係,導致這個Activity不能被銷燬,其對應的內存不能被GC回收,因而就出現了內存泄露問題。

關於如何避免內存泄露,我們可以使用靜態內部類 + 弱引用的形式解決。

3.2、AsyncTask.canncel() 的問題

AsyncTask作爲任務,是支持調用者取消任務的,即允許我們使用AsyncTask.canncel()方法取消提交的任務。然而其實cancel並非真正的起作用。

首先,我們看一下cancel方法:

// mayInterruptIfRunning <tt>true</tt> if the thread executing this, task should be interrupted; otherwise, in-progress tasks are allowed to complete.

public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

cancel 方法接受一個boolean 類型的參數,名稱爲mayInterruptIfRunning,意思是是否可以打斷正在執行的任務。

當我們調用cancel(false),不打斷正在執行的任務,對應的結果是:

  • 處於doInBackground中的任務不受影響,繼續執行
  • 任務結束時不會去調用onPostExecute方法,而是執行onCancelled方法

當我們調用cancel(true),表示打斷正在執行的任務,會出現如下情況:

  • 如果doInBackground方法處於阻塞狀態,如調用Thread.sleep,wait等方法,則會拋出InterruptedException。
  • 對於某些情況下,有可能無法打斷正在執行的任務。

如下,就是一個cancel方法無法打斷正在執行的任務的例子:

AsyncTask<String,Void,Void> task = new AsyncTask<String, Void, Void>() {

    @Override
    protected Void doInBackground(String... params) {
        boolean loop = true;
        while(loop) {
            Log.i(LOGTAG, "doInBackground after interrupting the loop");
        }
        return null;
    }
}


task.execute("hello world");
try {
    Thread.sleep(2000);//確保AsyncTask任務執行
    task.cancel(true);
} catch (InterruptedException e) {
    e.printStackTrace();
}

上面的例子,如果想要使cancel正常工作需要在循環中,需要在循環條件裏面同時檢測 isCancelled() 纔可以。

//  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.

// java.util.concurrent.atomic.AtomicBoolean  保證 mCancelled 只被初始化一次  
private final AtomicBoolean mCancelled = new AtomicBoolean();

public final boolean isCancelled() {
        return mCancelled.get();
    }

3.3、AsyncTask 串行帶來的問題

Android團隊關於AsyncTask執行策略進行了多次修改,修改大致如下:

  1. 自最初引入到Donut(1.6)之前,任務串行執行。
  2. 從Donut到GINGERBREAD_MR1(2.3.4),任務被修改成了並行執行。
  3. 從HONEYCOMB(3.0)至今,任務恢復至串行,但可以設置executeOnExecutor() 實現並行執行。

然而AsyncTask的串行實際執行起來是這樣的邏輯:

  1. 由串行執行器控制任務的初始分發;
  2. 並行執行器一次執行單個任務,並啓動下一個;

3.4、AsyncTask 中線程利用率的問題

在AsyncTask中,併發執行器實際爲 ThreadPoolExecutor 的實例,其CORE_POOL_SIZE爲當前設備CPU數量+1,MAXIMUM_POOL_SIZE值爲CPU數量的2倍 + 1。

ThreadPoolExecutor 和 SERIAL_EXECUTOR 初始化如下:

/*** An {@link Executor} that can be used to execute tasks in parallel.  【用於並行執行任務】*/ 
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**An {@link Executor} that executes tasks one at a time in serial 【用於默認串行執行任務】
     * order.  This serialization is global to a particular process. */
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
     private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

//SERIAL_EXECUTOR本質是在THREAD_POOL_EXECUTOR的基礎上添加一個mTasks的集合來保證任務的順序執行。
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();  //cpu數目
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;//核心線程數爲 cpu數目 + 1
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; // 最大併發數爲  cpu數目 * 2 + 1
    private static final int KEEP_ALIVE = 1;

線程池創建對象時各參數解釋:

  1. corePoolSize: 核心線程數,會一直存活,即使沒有任務,線程池也會維護線程的最少數量;
  2. maximumPoolSize: 線程池維護線程的最大數量;
  3. keepAliveTime: 線程池維護線程所允許的空閒時間,當線程空閒時間達到keepAliveTime,該線程會退出,直到線程數量等於corePoolSize。如果allowCoreThreadTimeout設置爲true,則所有線程均會退出直到線程數量爲0;
  4. unit: 線程池維護線程所允許的空閒時間的單位、可選參數值爲:TimeUnit中的幾個靜態屬性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS;
  5. workQueue: 線程池所使用的緩衝隊列,常用的是:java.util.concurrent.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue;

以一個四核手機爲例,當我們持續調用AsyncTask任務過程中:

  1. 在AsyncTask線程數量小於 CORE_POOL_SIZE(5個)時,會啓動新的線程處理任務,不重用之前空閒的線程
  2. 當數量超過CORE_POOL_SIZE(5個),纔開始重用之前的線程處理任務

但是由於AsyncTask 屬於默認線性執行任務,導致併發執行器總是處於某一個線程工作的狀態,因而造成了 ThreadPool 中其他線程的浪費。同時由於 AsyncTask中 並不存在allowCoreThreadTimeOut(boolean)的調用,所以 ThreadPool 中的核心線程即使處於空閒狀態也不會銷燬掉。

四、Executors

Executors是Java API中一個快速創建線程池的工具類,然而在它裏面也是存在問題的。

以Executors中獲取一個固定大小的線程池方法爲例:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, 
        TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

在上面代碼實現中,CORE_POOL_SIZE 和 MAXIMUM_POOL_SIZE都是同樣的值,如果把nThreads當成核心線程數,則無法保證最大併發,而如果當做最大併發線程數,則會造成線程的浪費。因而Executors這樣的API 導致了我們無法在最大併發數和線程節省上做到平衡。

爲了達到最大併發數和線程節省的平衡,建議自行創建ThreadPoolExecutor,根據業務和設備信息確定 CORE_POOL_SIZE 和MAXIMUM_POOL_SIZE 的合理值。

五、HandlerThread

HandlerThread 是 Android 中提供特殊的線程類,使用這個類我們可以輕鬆創建一個帶有 Looper 的線程,同時利用Looper我們可以結合Handler實現任務的控制與調度。以Handler的post方法爲例,我們可以封裝一個輕量級的任務處理器

private Handler mHandler;
private LightTaskManager() {
    HandlerThread workerThread = new HandlerThread("LightTaskThread");
    workerThread.start();
    mHandler = new Handler(workerThread.getLooper());
}

public void post(Runnable run) {
    mHandler.post(run);
}

public void postAtFrontOfQueue(Runnable runnable) {
    mHandler.postAtFrontOfQueue(runnable);
}

public void postDelayed(Runnable runnable, long delay) {
    mHandler.postDelayed(runnable, delay);
}

public void postAtTime(Runnable runnable, long time) {
    mHandler.postAtTime(runnable, time);
}

在本例中,我們可以按照如下規則提交任務:

  1. post 提交優先級一般的任務
  2. postAtFrontOfQueue 將優先級較高的任務加入到隊列前端
  3. postAtTime 指定時間提交任務
  4. postDelayed 延後提交優先級較低的任務

上面的輕量級任務處理器利用 HandlerThread 的單一線程 + 任務隊列的形式,可以處理類似本地IO(文件或數據庫讀取)的輕量級任務。在具體的處理場景下,可以參考如下做法:

  1. 對於本地IO讀取,並顯示到界面,建議使用postAtFrontOfQueue
  2. 對於本地IO寫入,不需要通知界面,建議使用postDelayed
  3. 一般操作,可以使用post

六、線程優先級調整

在Android應用中,將耗時任務放入異步線程是一個不錯的選擇,那麼爲異步線程調整應有的優先級則是一件錦上添花的事情。衆所周知,線程的並行通過CPU的時間片切換實現,對線程優先級調整,最主要的策略就是降低異步線程的優先級,從而使得主線程獲得更多的CPU資源。

Android中的線程優先級和Linux系統進程優先級有些類似,其值都是從-20至19。其中Android中,開發者可以控制的優先級有:

  • THREAD_PRIORITY_DEFAULT,默認的線程優先級,值爲0
  • THREAD_PRIORITY_LOWEST,最低的線程級別,值爲19
  • THREAD_PRIORITY_BACKGROUND 後臺線程建議設置這個優先級,值爲10
  • THREAD_PRIORITY_MORE_FAVORABLE 相對
  • THREAD_PRIORITY_DEFAULT稍微優先,值爲-1
  • THREAD_PRIORITY_LESS_FAVORABLE 相對
  • THREAD_PRIORITY_DEFAULT稍微落後一些,值爲1

爲線程設置優先級也比較簡單,通用的做法是在run方法體的開始部分加入下列代碼:

android.os.Process.setThreadPriority(priority);

通常設置優先級的規則如下:

一般的工作者線程,設置成THREAD_PRIORITY_BACKGROUND
對於優先級很低的線程,可以設置THREAD_PRIORITY_LOWEST
其他特殊需求,視業務應用具體的優先級。

七、總結

在Android中工作者線程如此普遍,然而潛在的問題也不可避免,建議在開發者使用工作者線程時,從工作者線程的數量和優先級等方面進行審視,做到較爲合理的使用。

致謝:
(1)、http://www.infoq.com/cn/articles/android-worker-thread
(2)、進程和線程
(3)、Android 單線程模型
(4)、譯文:Android中糟糕的AsyncTask
(5)、Android AsyncTask兩種線程池分析和總結
(6)、聊聊併發(三)——JAVA線程池的分析和使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章