關於Android中工作者執行緒的思考
本文系2015 北京 GDG Devfest分享內容整理。
在Android中,我們或多或少使用了工作者執行緒,比如Thread,AsyncTask,HandlerThread,甚至是自己建立的執行緒池,使用工作者執行緒我們可以將耗時的操作從主執行緒中移走。然而在Android系統中為什麼存在工作者執行緒呢,常用的工作者執行緒有哪些不易察覺的問題呢,關於工作者執行緒有哪些優化的方面呢,本文將一一解答這些問題。
工作者執行緒的存在原因
- 因為Android的UI單執行緒模型,所有的UI相關的操作都需要在主執行緒(UI執行緒)執行
- Android中各大元件的生命週期回撥都是位於主執行緒中,使得主執行緒的職責更重
- 如果不使用工作者執行緒為主執行緒分擔耗時的任務,會造成應用卡頓,嚴重時可能出現ANR(Application Not Responding),即程式未響應。
因而,在Android中使用工作者執行緒顯得勢在必行,如一開始提到那樣,在Android中工作者執行緒有很多,接下來我們將圍繞AsyncTask,HandlerThread等深入研究。
AsyncTask
AsyncTask是Android框架提供給開發者的一個輔助類,使用該類我們可以輕鬆的處理非同步執行緒與主執行緒的互動,由於其便捷性,在Android工程中,AsyncTask被廣泛使用。然而AsyncTask並非一個完美的方案,使用它往往會存在一些問題。接下來將逐一列舉AsyncTask不容易被開發者察覺的問題。
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回收,因而就出現了記憶體洩露問題。
關於如何避免記憶體洩露,我們可以使用靜態內部類 + 弱引用的形式解決。
cancel的問題
AsyncTask作為任務,是支援呼叫者取消任務的,即允許我們使用AsyncTask.canncel()方法取消提交的任務。然而其實cancel並非真正的起作用。
首先,我們看一下cancel方法:
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()
才可以。
序列帶來的問題
Android團隊關於AsyncTask執行策略進行了多次修改,修改大致如下:
- 自最初引入到Donut(1.6)之前,任務序列執行
- 從Donut到GINGERBREAD_MR1(2.3.4),任務被修改成了並行執行
- 從HONEYCOMB(3.0)至今,任務恢復至序列,但可以設定
executeOnExecutor()
實現並行執行。
然而AsyncTask的序列實際執行起來是這樣的邏輯
- 由序列執行器控制任務的初始分發
- 並行執行器一次執行單個任務,並啟動下一個
在AsyncTask中,併發執行器實際為ThreadPoolExecutor的例項,其CORE_POOL_SIZE為當前裝置CPU數量+1,MAXIMUM_POOL_SIZE值為CPU數量的2倍 + 1。
以一個四核手機為例,當我們持續呼叫AsyncTask任務過程中
- 在AsyncTask執行緒數量小於CORE_POOL_SIZE(5個)時,會啟動新的執行緒處理任務,不重用之前空閒的執行緒
- 當數量超過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); }
在本例中,我們可以按照如下規則提交任務
- post 提交優先順序一般的任務
- postAtFrontOfQueue 將優先順序較高的任務加入到佇列前端
- postAtTime 指定時間提交任務
- postDelayed 延後提交優先順序較低的任務
上面的輕量級任務處理器利用HandlerThread的單一執行緒 + 任務佇列的形式,可以處理類似本地IO(檔案或資料庫讀取)的輕量級任務。在具體的處理場景下,可以參考如下做法:
- 對於本地IO讀取,並顯示到介面,建議使用postAtFrontOfQueue
- 對於本地IO寫入,不需要通知介面,建議使用postDelayed
- 一般操作,可以使用post
執行緒優先順序調整
在Android應用中,將耗時任務放入非同步執行緒是一個不錯的選擇,那麼為非同步執行緒調整應有的優先順序則是一件錦上添花的事情。眾所周知,執行緒的並行通過CPU的時間片切換實現,對執行緒優先順序調整,最主要的策略就是降低非同步執行緒的優先順序,從而使得主執行緒獲得更多的CPU資源。
Android中的執行緒優先順序和Linux系統程式優先順序有些類似,其值都是從-20至19。其中Android中,開發者可以控制的優先順序有:
THREAD_PRIORITY_DEFAULT
,預設的執行緒優先順序,值為0THREAD_PRIORITY_LOWEST
,最低的執行緒級別,值為19THREAD_PRIORITY_BACKGROUND
後臺執行緒建議設定這個優先順序,值為10THREAD_PRIORITY_MORE_FAVORABLE
相對THREAD_PRIORITY_DEFAULT
稍微優先,值為-1THREAD_PRIORITY_LESS_FAVORABLE
相對THREAD_PRIORITY_DEFAULT
稍微落後一些,值為1
為執行緒設定優先順序也比較簡單,通用的做法是在run方法體的開始部分加入下列程式碼
android.os.Process.setThreadPriority(priority);
通常設定優先順序的規則如下:
- 一般的工作者執行緒,設定成
THREAD_PRIORITY_BACKGROUND
- 對於優先順序很低的執行緒,可以設定
THREAD_PRIORITY_LOWEST
- 其他特殊需求,視業務應用具體的優先順序
總結
在Android中工作者執行緒如此普遍,然而潛在的問題也不可避免,建議在開發者使用工作者執行緒時,從工作者執行緒的數量和優先順序等方面進行審視,做到較為合理的使用。
相關文章
- 關於Java併發多執行緒的一點思考Java執行緒
- 面試中關於多執行緒同步,你必須要思考的問題面試執行緒
- Android中的執行緒池Android執行緒
- 對於es執行緒池使用的思考執行緒
- Android優化幀動畫過程中的多執行緒模型思考Android優化動畫執行緒模型
- Android中的執行緒通訊Android執行緒
- Android JNI 中的執行緒操作Android執行緒
- Android中的多程式、多執行緒Android執行緒
- python多執行緒中:如何關閉執行緒?Python執行緒
- 關於redis單執行緒的分析Redis執行緒
- 關於執行緒設計的感受執行緒
- Android多執行緒之執行緒池Android執行緒
- 關於執行緒池的面試題執行緒面試題
- 關於執行緒的幾個函式執行緒函式
- Android程式框架:執行緒與執行緒池Android框架執行緒
- Android執行緒池Android執行緒
- Android小知識-Java多執行緒相關(執行緒間通訊)上篇AndroidJava執行緒
- Android中的多執行緒斷點續傳Android執行緒斷點
- 走進Java Android 的執行緒世界(二)執行緒池JavaAndroid執行緒
- java基礎 關於執行緒安全Java執行緒
- Java 關於執行緒的一些使用Java執行緒
- python關於執行緒的一點介紹Python執行緒
- java 多執行緒(關於Thread的講解)Java執行緒thread
- 關於js執行緒問題的解讀JS執行緒
- 關於c#多執行緒中的幾個訊號量C#執行緒
- 關於Numba的執行緒實現的說明執行緒
- 解惑Android的post()方法究竟執行在哪個執行緒中Android執行緒
- Android中後臺的服務和多執行緒Android執行緒
- android程式和執行緒Android執行緒
- 關於多執行緒的兩種實現方式執行緒
- 多執行緒的執行緒狀態及相關操作執行緒
- [Android] 關於 Model 層的幾點思考(一)Android
- 03.關於執行緒你必須知道的8個問題(中)執行緒
- Java多執行緒-完成Android開發中的某些需求Java執行緒Android
- Android 中的執行緒有哪些,原理與各自特點Android執行緒
- Android執行緒池的原理以及專案中實踐Android執行緒
- javascript執行緒及與執行緒有關的效能優化JavaScript執行緒優化
- 有個關於多執行緒的識別問題執行緒
- 關於redis的幾件小事(二)redis執行緒模型Redis執行緒模型