关于Android中工作者线程的思考
本文系2015 北京 GDG Devfest分享內(nèi)容整理。
在Android中,我們或多或少使用了工作者線程,比如Thread,AsyncTask,HandlerThread,甚至是自己創(chuàng)建的線程池,使用工作者線程我們可以將耗時(shí)的操作從主線程中移走。然而在Android系統(tǒng)中為什么存在工作者線程呢,常用的工作者線程有哪些不易察覺的問題呢,關(guān)于工作者線程有哪些優(yōu)化的方面呢,本文將一一解答這些問題。
工作者線程的存在原因
- 因?yàn)锳ndroid的UI單線程模型,所有的UI相關(guān)的操作都需要在主線程(UI線程)執(zhí)行
- Android中各大組件的生命周期回調(diào)都是位于主線程中,使得主線程的職責(zé)更重
- 如果不使用工作者線程為主線程分擔(dān)耗時(shí)的任務(wù),會(huì)造成應(yīng)用卡頓,嚴(yán)重時(shí)可能出現(xiàn)ANR(Application Not Responding),即程序未響應(yīng)。
因而,在Android中使用工作者線程顯得勢(shì)在必行,如一開始提到那樣,在Android中工作者線程有很多,接下來我們將圍繞AsyncTask,HandlerThread等深入研究。
AsyncTask
AsyncTask是Android框架提供給開發(fā)者的一個(gè)輔助類,使用該類我們可以輕松的處理異步線程與主線程的交互,由于其便捷性,在Android工程中,AsyncTask被廣泛使用。然而AsyncTask并非一個(gè)完美的方案,使用它往往會(huì)存在一些問題。接下來將逐一列舉AsyncTask不容易被開發(fā)者察覺的問題。
AsyncTask與內(nèi)存泄露
內(nèi)存泄露是Android開發(fā)中常見的問題,只要開發(fā)者稍有不慎就有可能導(dǎo)致程序產(chǎn)生內(nèi)存泄露,嚴(yán)重時(shí)甚至可能導(dǎo)致OOM(OutOfMemory,即內(nèi)存溢出錯(cuò)誤)。AsyncTask也不例外,也有可能造成內(nèi)存泄露。
以一個(gè)簡(jiǎn)單的場(chǎng)景為例:
在Activity中,通常我們這樣使用AsyncTask
上述代碼使用的匿名內(nèi)存類創(chuàng)建AsyncTask實(shí)例,然而在Java中,非靜態(tài)內(nèi)存類會(huì)隱式持有外部類的實(shí)例引用,上面例子AsyncTask創(chuàng)建于Activity中,因而會(huì)隱式持有Activity的實(shí)例引用。
而在AsyncTask內(nèi)部實(shí)現(xiàn)中,mFuture同樣使用匿名內(nèi)部類創(chuàng)建對(duì)象,而mFuture會(huì)作為執(zhí)行任務(wù)加入到任務(wù)執(zhí)行器中。
private final WorkerRunnable<Params, Result> mWorker; public AsyncTask() {mFuture = new FutureTask<Result>(mWorker) {@Overrideprotected void done() {//some code}}; }而mFuture加入任務(wù)執(zhí)行器,實(shí)際上是放入了一個(gè)靜態(tài)成員變量SERIAL_EXECUTOR指向的對(duì)象SerialExecutor的一個(gè)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 coder.run();}});} }當(dāng)任務(wù)處于排隊(duì)狀態(tài),則Activity實(shí)例引用被靜態(tài)常量SERIAL_EXECUTOR 間接持有。
在通常情況下,當(dāng)設(shè)備發(fā)生屏幕旋轉(zhuǎn)事件,當(dāng)前的Activity被銷毀,新的Activity被創(chuàng)建,以此完成對(duì)布局的重新加載。
而本例中,當(dāng)屏幕旋轉(zhuǎn)時(shí),處于排隊(duì)的AsyncTask由于其對(duì)Activity實(shí)例的引用關(guān)系,導(dǎo)致這個(gè)Activity不能被銷毀,其對(duì)應(yīng)的內(nèi)存不能被GC回收,因而就出現(xiàn)了內(nèi)存泄露問題。
關(guān)于如何避免內(nèi)存泄露,我們可以使用靜態(tài)內(nèi)部類 + 弱引用的形式解決。
cancel的問題
AsyncTask作為任務(wù),是支持調(diào)用者取消任務(wù)的,即允許我們使用AsyncTask.canncel()方法取消提交的任務(wù)。然而其實(shí)cancel并非真正的起作用。
首先,我們看一下cancel方法:
public final boolean cancel(boolean mayInterruptIfRunning) {mCancelled.set(true);return mFuture.cancel(mayInterruptIfRunning); }cancel方法接受一個(gè)boolean類型的參數(shù),名稱為mayInterruptIfRunning,意思是是否可以打斷正在執(zhí)行的任務(wù)。
當(dāng)我們調(diào)用cancel(false),不打斷正在執(zhí)行的任務(wù),對(duì)應(yīng)的結(jié)果是
- 處于doInBackground中的任務(wù)不受影響,繼續(xù)執(zhí)行
- 任務(wù)結(jié)束時(shí)不會(huì)去調(diào)用onPostExecute方法,而是執(zhí)行onCancelled方法
當(dāng)我們調(diào)用cancel(true),表示打斷正在執(zhí)行的任務(wù),會(huì)出現(xiàn)如下情況:
- 如果doInBackground方法處于阻塞狀態(tài),如調(diào)用Thread.sleep,wait等方法,則會(huì)拋出InterruptedException。
- 對(duì)于某些情況下,有可能無法打斷正在執(zhí)行的任務(wù)
如下,就是一個(gè)cancel方法無法打斷正在執(zhí)行的任務(wù)的例子
AsyncTask<String,Void,Void> task = new AsyncTask<String, Void, Void>() {@Overrideprotected 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任務(wù)執(zhí)行task.cancel(true); } catch (InterruptedException e) {e.printStackTrace(); }上面的例子,如果想要使cancel正常工作需要在循環(huán)中,需要在循環(huán)條件里面同時(shí)檢測(cè)isCancelled()才可以。
串行帶來的問題
Android團(tuán)隊(duì)關(guān)于AsyncTask執(zhí)行策略進(jìn)行了多次修改,修改大致如下:
- 自最初引入到Donut(1.6)之前,任務(wù)串行執(zhí)行
- 從Donut到GINGERBREAD_MR1(2.3.4),任務(wù)被修改成了并行執(zhí)行
- 從HONEYCOMB(3.0)至今,任務(wù)恢復(fù)至串行,但可以設(shè)置executeOnExecutor()實(shí)現(xiàn)并行執(zhí)行。
然而AsyncTask的串行實(shí)際執(zhí)行起來是這樣的邏輯
- 由串行執(zhí)行器控制任務(wù)的初始分發(fā)
- 并行執(zhí)行器一次執(zhí)行單個(gè)任務(wù),并啟動(dòng)下一個(gè)
在AsyncTask中,并發(fā)執(zhí)行器實(shí)際為ThreadPoolExecutor的實(shí)例,其CORE_POOL_SIZE為當(dāng)前設(shè)備CPU數(shù)量+1,MAXIMUM_POOL_SIZE值為CPU數(shù)量的2倍 + 1。
以一個(gè)四核手機(jī)為例,當(dāng)我們持續(xù)調(diào)用AsyncTask任務(wù)過程中
- 在AsyncTask線程數(shù)量小于CORE_POOL_SIZE(5個(gè))時(shí),會(huì)啟動(dòng)新的線程處理任務(wù),不重用之前空閑的線程
- 當(dāng)數(shù)量超過CORE_POOL_SIZE(5個(gè)),才開始重用之前的線程處理任務(wù)
但是由于AsyncTask屬于默認(rèn)線性執(zhí)行任務(wù),導(dǎo)致并發(fā)執(zhí)行器總是處于某一個(gè)線程工作的狀態(tài),因而造成了ThreadPool中其他線程的浪費(fèi)。同時(shí)由于AsyncTask中并不存在allowCoreThreadTimeOut(boolean)的調(diào)用,所以ThreadPool中的核心線程即使處于空閑狀態(tài)也不會(huì)銷毀掉。
Executors
Executors是Java API中一個(gè)快速創(chuàng)建線程池的工具類,然而在它里面也是存在問題的。
以Executors中獲取一個(gè)固定大小的線程池方法為例
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }在上面代碼實(shí)現(xiàn)中,CORE_POOL_SIZE和MAXIMUM_POOL_SIZE都是同樣的值,如果把nThreads當(dāng)成核心線程數(shù),則無法保證最大并發(fā),而如果當(dāng)做最大并發(fā)線程數(shù),則會(huì)造成線程的浪費(fèi)。因而Executors這樣的API導(dǎo)致了我們無法在最大并發(fā)數(shù)和線程節(jié)省上做到平衡。
為了達(dá)到最大并發(fā)數(shù)和線程節(jié)省的平衡,建議自行創(chuàng)建ThreadPoolExecutor,根據(jù)業(yè)務(wù)和設(shè)備信息確定CORE_POOL_SIZE和MAXIMUM_POOL_SIZE的合理值。
HandlerThread
HandlerThread是Android中提供特殊的線程類,使用這個(gè)類我們可以輕松創(chuàng)建一個(gè)帶有Looper的線程,同時(shí)利用Looper我們可以結(jié)合Handler實(shí)現(xiàn)任務(wù)的控制與調(diào)度。以Handler的post方法為例,我們可以封裝一個(gè)輕量級(jí)的任務(wù)處理器
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); }在本例中,我們可以按照如下規(guī)則提交任務(wù)
- post 提交優(yōu)先級(jí)一般的任務(wù)
- postAtFrontOfQueue 將優(yōu)先級(jí)較高的任務(wù)加入到隊(duì)列前端
- postAtTime 指定時(shí)間提交任務(wù)
- postDelayed 延后提交優(yōu)先級(jí)較低的任務(wù)
上面的輕量級(jí)任務(wù)處理器利用HandlerThread的單一線程 + 任務(wù)隊(duì)列的形式,可以處理類似本地IO(文件或數(shù)據(jù)庫讀取)的輕量級(jí)任務(wù)。在具體的處理場(chǎng)景下,可以參考如下做法:
- 對(duì)于本地IO讀取,并顯示到界面,建議使用postAtFrontOfQueue
- 對(duì)于本地IO寫入,不需要通知界面,建議使用postDelayed
- 一般操作,可以使用post
線程優(yōu)先級(jí)調(diào)整
在Android應(yīng)用中,將耗時(shí)任務(wù)放入異步線程是一個(gè)不錯(cuò)的選擇,那么為異步線程調(diào)整應(yīng)有的優(yōu)先級(jí)則是一件錦上添花的事情。眾所周知,線程的并行通過CPU的時(shí)間片切換實(shí)現(xiàn),對(duì)線程優(yōu)先級(jí)調(diào)整,最主要的策略就是降低異步線程的優(yōu)先級(jí),從而使得主線程獲得更多的CPU資源。
Android中的線程優(yōu)先級(jí)和Linux系統(tǒng)進(jìn)程優(yōu)先級(jí)有些類似,其值都是從-20至19。其中Android中,開發(fā)者可以控制的優(yōu)先級(jí)有:
- THREAD_PRIORITY_DEFAULT,默認(rèn)的線程優(yōu)先級(jí),值為0
- THREAD_PRIORITY_LOWEST,最低的線程級(jí)別,值為19
- THREAD_PRIORITY_BACKGROUND?后臺(tái)線程建議設(shè)置這個(gè)優(yōu)先級(jí),值為10
- THREAD_PRIORITY_MORE_FAVORABLE?相對(duì)THREAD_PRIORITY_DEFAULT稍微優(yōu)先,值為-1
- THREAD_PRIORITY_LESS_FAVORABLE?相對(duì)THREAD_PRIORITY_DEFAULT稍微落后一些,值為1
為線程設(shè)置優(yōu)先級(jí)也比較簡(jiǎn)單,通用的做法是在run方法體的開始部分加入下列代碼
android.os.Process.setThreadPriority(priority);通常設(shè)置優(yōu)先級(jí)的規(guī)則如下:
- 一般的工作者線程,設(shè)置成THREAD_PRIORITY_BACKGROUND
- 對(duì)于優(yōu)先級(jí)很低的線程,可以設(shè)置THREAD_PRIORITY_LOWEST
- 其他特殊需求,視業(yè)務(wù)應(yīng)用具體的優(yōu)先級(jí)
總結(jié)
在Android中工作者線程如此普遍,然而潛在的問題也不可避免,建議在開發(fā)者使用工作者線程時(shí),從工作者線程的數(shù)量和優(yōu)先級(jí)等方面進(jìn)行審視,做到較為合理的使用。
原文:http://www.infoq.com/cn/articles/android-worker-thread
總結(jié)
以上是生活随笔為你收集整理的关于Android中工作者线程的思考的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JAndFix: 基于Java实现的An
- 下一篇: Android插件化原理解析——Hook