關于Android中工作者線程的思考
作者:網絡轉載 發布時間:[ 2015/12/18 14:25:31 ] 推薦標簽:移動測試
在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);
}
相關推薦

最新發布
性能測試之測試環境搭建的方法
2020/7/21 15:39:32軟件測試是從什么時候開始被企業所重視的呢?
2020/7/17 9:09:11Android自動化測試框架有哪些?有什么用途?
2020/7/17 9:03:50什么樣的項目適合做自動化?自動化測試人員應具備怎樣的能力?
2020/7/17 8:57:06幾大市面主流性能測試工具測評
2020/7/17 8:52:11RPA機器人能夠快速響應企業需求,是怎么做到的?
2020/7/17 8:48:05Bug可以真正消滅嗎?為什么?
2020/7/17 8:43:03軟件測試基本概念是怎么來的?軟件測試生命周期的形成歷經了什么?
2020/7/16 9:11:10