Android多线程任务优化2:实现后台预读线程

导语:从上一篇《多线程任务的优化1:探讨AsyncTask的缺陷》我们了解到,使用AsyncTask有导致应用FC的风险,而且AsyncTask并不能满足我们一些特定的需求。下面我们介绍一种通过模仿AsyncTask的封装方式,实现一个后台预读数据的线程。

概述:在空闲时对获取成本较高的数据(如要读取本地或网络资源)进行预读是提高性能的有效手段。为了给用户带来更好的交互体验,提高响应性,很多网络应用(如新闻阅读类应用)都在启动的时候进行预读,把网络数据缓存到sdcard或者内存中。

例子:下面介绍一个实现预读的例子,打开应用之后会有一个欢迎界面,在打开欢迎界面的同时我们在后台启动预读线程,预读下一个Activity需要显示的数据,预读数据保存到一个静态的Hashmap中。

首先要编写我们的后台预读线程,遵循不重复造轮子的原则,我们对AsyncTask稍作修改就可以满足需求。下面是我们自定义的PreReadTask类代码:

PreReadTask.java 实现预读线程

  1. packagecom.zhuozhuo;
  2. importjava.util.concurrent.ExecutorService;
  3. importjava.util.concurrent.Executors;
  4. importjava.util.concurrent.PriorityBlockingQueue;
  5. importjava.util.concurrent.RejectedExecutionHandler;
  6. importjava.util.concurrent.SynchronousQueue;
  7. importjava.util.concurrent.ThreadPoolExecutor;
  8. importjava.util.concurrent.TimeUnit;
  9. importjava.util.concurrent.BlockingQueue;
  10. importjava.util.concurrent.LinkedBlockingQueue;
  11. importjava.util.concurrent.ThreadFactory;
  12. importjava.util.concurrent.Callable;
  13. importjava.util.concurrent.FutureTask;
  14. importjava.util.concurrent.ExecutionException;
  15. importjava.util.concurrent.TimeoutException;
  16. importjava.util.concurrent.CancellationException;
  17. importjava.util.concurrent.atomic.AtomicInteger;
  18. importandroid.content.SyncResult;
  19. importandroid.os.Process;
  20. importandroid.os.Handler;
  21. importandroid.os.Message;
  22. publicabstractclassPreReadTask<Params,Progress,Result>{
  23. privatestaticfinalStringLOG_TAG="FifoAsyncTask";
  24. //privatestaticfinalintCORE_POOL_SIZE=5;
  25. //privatestaticfinalintMAXIMUM_POOL_SIZE=5;
  26. //privatestaticfinalintKEEP_ALIVE=1;
  27. //privatestaticfinalBlockingQueue<Runnable>sWorkQueue=
  28. //newLinkedBlockingQueue<Runnable>();
  29. //
  30. privatestaticfinalThreadFactorysThreadFactory=newThreadFactory(){
  31. privatefinalAtomicIntegermCount=newAtomicInteger(1);
  32. publicThreadnewThread(Runnabler){
  33. returnnewThread(r,"PreReadTask#"+mCount.getAndIncrement());
  34. }
  35. };
  36. privatestaticfinalExecutorServicesExecutor=Executors.newSingleThreadExecutor(sThreadFactory);//只有一个工作线程的线程池
  37. privatestaticfinalintMESSAGE_POST_RESULT=0x1;
  38. privatestaticfinalintMESSAGE_POST_PROGRESS=0x2;
  39. privatestaticfinalintMESSAGE_POST_CANCEL=0x3;
  40. privatestaticfinalInternalHandlersHandler=newInternalHandler();
  41. privatefinalWorkerRunnable<Params,Result>mWorker;
  42. privatefinalFutureTask<Result>mFuture;
  43. privatevolatileStatusmStatus=Status.PENDING;
  44. /**
  45. *Indicatesthecurrentstatusofthetask.Eachstatuswillbesetonlyonce
  46. *duringthelifetimeofatask.
  47. */
  48. publicenumStatus{
  49. /**
  50. *Indicatesthatthetaskhasnotbeenexecutedyet.
  51. */
  52. PENDING,
  53. /**
  54. *Indicatesthatthetaskisrunning.
  55. */
  56. RUNNING,
  57. /**
  58. *Indicatesthat{@linkFifoAsyncTask#onPostExecute}hasfinished.
  59. */
  60. FINISHED,
  61. }
  62. /**
  63. *Createsanewasynchronoustask.ThisconstructormustbeinvokedontheUIthread.
  64. */
  65. publicPreReadTask(){
  66. mWorker=newWorkerRunnable<Params,Result>(){
  67. publicResultcall()throwsException{
  68. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  69. returndoInBackground(mParams);
  70. }
  71. };
  72. mFuture=newFutureTask<Result>(mWorker){
  73. @Override
  74. protectedvoiddone(){
  75. Messagemessage;
  76. Resultresult=null;
  77. try{
  78. result=get();
  79. }catch(InterruptedExceptione){
  80. android.util.Log.w(LOG_TAG,e);
  81. }catch(ExecutionExceptione){
  82. thrownewRuntimeException("AnerroroccuredwhileexecutingdoInBackground()",
  83. e.getCause());
  84. }catch(CancellationExceptione){
  85. message=sHandler.obtainMessage(MESSAGE_POST_CANCEL,
  86. newPreReadTaskResult<Result>(PreReadTask.this,(Result[])null));
  87. message.sendToTarget();
  88. return;
  89. }catch(Throwablet){
  90. thrownewRuntimeException("Anerroroccuredwhileexecuting"
  91. +"doInBackground()",t);
  92. }
  93. message=sHandler.obtainMessage(MESSAGE_POST_RESULT,
  94. newPreReadTaskResult<Result>(PreReadTask.this,result));
  95. message.sendToTarget();
  96. }
  97. };
  98. }
  99. /**
  100. *Returnsthecurrentstatusofthistask.
  101. *
  102. *@returnThecurrentstatus.
  103. */
  104. publicfinalStatusgetStatus(){
  105. returnmStatus;
  106. }
  107. /**
  108. *Overridethismethodtoperformacomputationonabackgroundthread.The
  109. *specifiedparametersaretheparameterspassedto{@link#execute}
  110. *bythecallerofthistask.
  111. *
  112. *Thismethodcancall{@link#publishProgress}topublishupdates
  113. *ontheUIthread.
  114. *
  115. *@paramparamsTheparametersofthetask.
  116. *
  117. *@returnAresult,definedbythesubclassofthistask.
  118. *
  119. *@see#onPreExecute()
  120. *@see#onPostExecute
  121. *@see#publishProgress
  122. */
  123. protectedabstractResultdoInBackground(Params...params);
  124. /**
  125. *RunsontheUIthreadbefore{@link#doInBackground}.
  126. *
  127. *@see#onPostExecute
  128. *@see#doInBackground
  129. */
  130. protectedvoidonPreExecute(){
  131. }
  132. /**
  133. *RunsontheUIthreadafter{@link#doInBackground}.The
  134. *specifiedresultisthevaluereturnedby{@link#doInBackground}
  135. *ornullifthetaskwascancelledoranexceptionoccured.
  136. *
  137. *@paramresultTheresultoftheoperationcomputedby{@link#doInBackground}.
  138. *
  139. *@see#onPreExecute
  140. *@see#doInBackground
  141. */
  142. @SuppressWarnings({"UnusedDeclaration"})
  143. protectedvoidonPostExecute(Resultresult){
  144. }
  145. /**
  146. *RunsontheUIthreadafter{@link#publishProgress}isinvoked.
  147. *Thespecifiedvaluesarethevaluespassedto{@link#publishProgress}.
  148. *
  149. *@paramvaluesThevaluesindicatingprogress.
  150. *
  151. *@see#publishProgress
  152. *@see#doInBackground
  153. */
  154. @SuppressWarnings({"UnusedDeclaration"})
  155. protectedvoidonProgressUpdate(Progress...values){
  156. }
  157. /**
  158. *RunsontheUIthreadafter{@link#cancel(boolean)}isinvoked.
  159. *
  160. *@see#cancel(boolean)
  161. *@see#isCancelled()
  162. */
  163. protectedvoidonCancelled(){
  164. }
  165. /**
  166. *Returns<tt>true</tt>ifthistaskwascancelledbeforeitcompleted
  167. *normally.
  168. *
  169. *@return<tt>true</tt>iftaskwascancelledbeforeitcompleted
  170. *
  171. *@see#cancel(boolean)
  172. */
  173. publicfinalbooleanisCancelled(){
  174. returnmFuture.isCancelled();
  175. }
  176. /**
  177. *Attemptstocancelexecutionofthistask.Thisattemptwill
  178. *failifthetaskhasalreadycompleted,alreadybeencancelled,
  179. *orcouldnotbecancelledforsomeotherreason.Ifsuccessful,
  180. *andthistaskhasnotstartedwhen<tt>cancel</tt>iscalled,
  181. *thistaskshouldneverrun.Ifthetaskhasalreadystarted,
  182. *thenthe<tt>mayInterruptIfRunning</tt>parameterdetermines
  183. *whetherthethreadexecutingthistaskshouldbeinterruptedin
  184. *anattempttostopthetask.
  185. *
  186. *@parammayInterruptIfRunning<tt>true</tt>ifthethreadexecutingthis
  187. *taskshouldbeinterrupted;otherwise,in-progresstasksareallowed
  188. *tocomplete.
  189. *
  190. *@return<tt>false</tt>ifthetaskcouldnotbecancelled,
  191. *typicallybecauseithasalreadycompletednormally;
  192. *<tt>true</tt>otherwise
  193. *
  194. *@see#isCancelled()
  195. *@see#onCancelled()
  196. */
  197. publicfinalbooleancancel(booleanmayInterruptIfRunning){
  198. returnmFuture.cancel(mayInterruptIfRunning);
  199. }
  200. /**
  201. *Waitsifnecessaryforthecomputationtocomplete,andthen
  202. *retrievesitsresult.
  203. *
  204. *@returnThecomputedresult.
  205. *
  206. *@throwsCancellationExceptionIfthecomputationwascancelled.
  207. *@throwsExecutionExceptionIfthecomputationthrewanexception.
  208. *@throwsInterruptedExceptionIfthecurrentthreadwasinterrupted
  209. *whilewaiting.
  210. */
  211. publicfinalResultget()throwsInterruptedException,ExecutionException{
  212. returnmFuture.get();
  213. }
  214. /**
  215. *Waitsifnecessaryforatmostthegiventimeforthecomputation
  216. *tocomplete,andthenretrievesitsresult.
  217. *
  218. *@paramtimeoutTimetowaitbeforecancellingtheoperation.
  219. *@paramunitThetimeunitforthetimeout.
  220. *
  221. *@returnThecomputedresult.
  222. *
  223. *@throwsCancellationExceptionIfthecomputationwascancelled.
  224. *@throwsExecutionExceptionIfthecomputationthrewanexception.
  225. *@throwsInterruptedExceptionIfthecurrentthreadwasinterrupted
  226. *whilewaiting.
  227. *@throwsTimeoutExceptionIfthewaittimedout.
  228. */
  229. publicfinalResultget(longtimeout,TimeUnitunit)throwsInterruptedException,
  230. ExecutionException,TimeoutException{
  231. returnmFuture.get(timeout,unit);
  232. }
  233. /**
  234. *Executesthetaskwiththespecifiedparameters.Thetaskreturns
  235. *itself(this)sothatthecallercankeepareferencetoit.
  236. *
  237. *ThismethodmustbeinvokedontheUIthread.
  238. *
  239. *@paramparamsTheparametersofthetask.
  240. *
  241. *@returnThisinstanceofAsyncTask.
  242. *
  243. *@throwsIllegalStateExceptionIf{@link#getStatus()}returnseither
  244. *{@linkFifoAsyncTask.Status#RUNNING}or{@linkFifoAsyncTask.Status#FINISHED}.
  245. */
  246. publicfinalPreReadTask<Params,Progress,Result>execute(Params...params){
  247. if(mStatus!=Status.PENDING){
  248. switch(mStatus){
  249. caseRUNNING:
  250. thrownewIllegalStateException("Cannotexecutetask:"
  251. +"thetaskisalreadyrunning.");
  252. caseFINISHED:
  253. thrownewIllegalStateException("Cannotexecutetask:"
  254. +"thetaskhasalreadybeenexecuted"
  255. +"(ataskcanbeexecutedonlyonce)");
  256. }
  257. }
  258. mStatus=Status.RUNNING;
  259. onPreExecute();
  260. mWorker.mParams=params;
  261. sExecutor.execute(mFuture);
  262. returnthis;
  263. }
  264. /**
  265. *Thismethodcanbeinvokedfrom{@link#doInBackground}to
  266. *publishupdatesontheUIthreadwhilethebackgroundcomputationis
  267. *stillrunning.Eachcalltothismethodwilltriggertheexecutionof
  268. *{@link#onProgressUpdate}ontheUIthread.
  269. *
  270. *@paramvaluesTheprogressvaluestoupdatetheUIwith.
  271. *
  272. *@see#onProgressUpdate
  273. *@see#doInBackground
  274. */
  275. protectedfinalvoidpublishProgress(Progress...values){
  276. sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
  277. newPreReadTaskResult<Progress>(this,values)).sendToTarget();
  278. }
  279. privatevoidfinish(Resultresult){
  280. if(isCancelled())result=null;
  281. onPostExecute(result);
  282. mStatus=Status.FINISHED;
  283. }
  284. privatestaticclassInternalHandlerextendsHandler{
  285. @SuppressWarnings({"unchecked","RawUseOfParameterizedType"})
  286. @Override
  287. publicvoidhandleMessage(Messagemsg){
  288. PreReadTaskResultresult=(PreReadTaskResult)msg.obj;
  289. switch(msg.what){
  290. caseMESSAGE_POST_RESULT:
  291. //Thereisonlyoneresult
  292. result.mTask.finish(result.mData[0]);
  293. break;
  294. caseMESSAGE_POST_PROGRESS:
  295. result.mTask.onProgressUpdate(result.mData);
  296. break;
  297. caseMESSAGE_POST_CANCEL:
  298. result.mTask.onCancelled();
  299. break;
  300. }
  301. }
  302. }
  303. privatestaticabstractclassWorkerRunnable<Params,Result>implementsCallable<Result>{
  304. Params[]mParams;
  305. }
  306. @SuppressWarnings({"RawUseOfParameterizedType"})
  307. privatestaticclassPreReadTaskResult<Data>{
  308. finalPreReadTaskmTask;
  309. finalData[]mData;
  310. PreReadTaskResult(PreReadTasktask,Data...data){
  311. mTask=task;
  312. mData=data;
  313. }
  314. }
  315. }


对比AsyncTask我们实际只修改了一个地方

  1. privatestaticfinalExecutorServicesExecutor=Executors.newSingleThreadExecutor(sThreadFactory);//只有一个工作线程的线程池

通过Executors.newSingleThreadExecutor,我们把PreReadTask的的线程池设置成只有一个工作线程,并且带有一个无边界的缓冲队列,这一个工作线程以先进先出的顺序不断从缓冲队列中取出并执行任务。

创建完后台预读的线程。我们通过一个例子介绍如何使用这个后台预读线程。

这个例子由两个Activity组成,WelcomeActivity是欢迎界面,在欢迎界面中会停留三秒,在此时我们对数据进行预读,预读成功后保存到一个全局的静态hashmap中。MainActivity是主界面,在主界面中显示一个listview,listview中的图片是模拟从网络获取的,当静态hashmap中存在数据(也就是已经成功预读的数据)的时,从hashmap中取,如果不存在,才从网络获取。

点此下载工程代码

WelcomeActivity.java 欢迎界面,停留三秒,预读数据

  1. packagecom.zhuozhuo;
  2. importandroid.app.Activity;
  3. importandroid.content.Intent;
  4. importandroid.graphics.BitmapFactory;
  5. importandroid.os.AsyncTask;
  6. importandroid.os.Bundle;
  7. importandroid.os.Handler;
  8. importandroid.widget.Toast;
  9. publicclassWelcomeActivityextendsActivity{
  10. privateHandlerhandler=newHandler();
  11. @Override
  12. publicvoidonCreate(BundlesavedInstanceState){
  13. super.onCreate(savedInstanceState);
  14. setContentView(R.layout.welcome);
  15. for(inti=0;i<15;i++){//预读15张图片
  16. ReadImgTasktask=newReadImgTask();
  17. task.execute(String.valueOf(i));
  18. }
  19. Toast.makeText(getApplicationContext(),"PreReading...",Toast.LENGTH_LONG).show();
  20. handler.postDelayed(newRunnable(){
  21. @Override
  22. publicvoidrun(){
  23. startActivity(newIntent(WelcomeActivity.this,MainActivity.class));//启动MainActivity
  24. finish();
  25. }
  26. },3000);//显示三秒钟的欢迎界面
  27. }
  28. classReadImgTaskextendsPreReadTask<String,Void,Void>{
  29. @Override
  30. protectedVoiddoInBackground(String...arg0){
  31. try{
  32. Thread.sleep(200);//模拟网络延时
  33. }catch(InterruptedExceptione){
  34. //TODOAuto-generatedcatchblock
  35. e.printStackTrace();
  36. }
  37. Data.putData(arg0[0],BitmapFactory.decodeResource(getResources(),R.drawable.icon));//把预读的数据放到hashmap中
  38. returnnull;
  39. }
  40. }
  41. }


MainActivity.java 主界面,有一个listview,listview中显示图片和文字,模拟图片从网络异步获取

  1. packagecom.zhuozhuo;
  2. importjava.util.ArrayList;
  3. importjava.util.Collection;
  4. importjava.util.HashMap;
  5. importjava.util.Iterator;
  6. importjava.util.List;
  7. importjava.util.ListIterator;
  8. importjava.util.Map;
  9. importandroid.app.Activity;
  10. importandroid.app.AlertDialog;
  11. importandroid.app.Dialog;
  12. importandroid.app.ListActivity;
  13. importandroid.app.ProgressDialog;
  14. importandroid.content.Context;
  15. importandroid.content.DialogInterface;
  16. importandroid.content.Intent;
  17. importandroid.database.Cursor;
  18. importandroid.graphics.Bitmap;
  19. importandroid.graphics.BitmapFactory;
  20. importandroid.os.AsyncTask;
  21. importandroid.os.Bundle;
  22. importandroid.provider.ContactsContract;
  23. importandroid.util.Log;
  24. importandroid.view.LayoutInflater;
  25. importandroid.view.View;
  26. importandroid.view.ViewGroup;
  27. importandroid.widget.AbsListView;
  28. importandroid.widget.AbsListView.OnScrollListener;
  29. importandroid.widget.Adapter;
  30. importandroid.widget.AdapterView;
  31. importandroid.widget.AdapterView.OnItemClickListener;
  32. importandroid.widget.BaseAdapter;
  33. importandroid.widget.GridView;
  34. importandroid.widget.ImageView;
  35. importandroid.widget.ListAdapter;
  36. importandroid.widget.ListView;
  37. importandroid.widget.SimpleAdapter;
  38. importandroid.widget.TextView;
  39. importandroid.widget.Toast;
  40. publicclassMainActivityextendsActivity{
  41. privateListViewmListView;
  42. privateList<HashMap<String,Object>>mData;
  43. privateBaseAdaptermAdapter;
  44. @Override
  45. publicvoidonCreate(BundlesavedInstanceState){
  46. super.onCreate(savedInstanceState);
  47. setContentView(R.layout.main);
  48. mListView=(ListView)findViewById(R.id.listview);
  49. mData=newArrayList<HashMap<String,Object>>();
  50. mAdapter=newCustomAdapter();
  51. mListView.setAdapter(mAdapter);
  52. for(inti=0;i<100;i++){//初始化100项数据
  53. HashMapdata=newHashMap<String,Object>();
  54. data.put("title","title"+i);
  55. mData.add(data);
  56. }
  57. }
  58. classCustomAdapterextendsBaseAdapter{
  59. CustomAdapter(){
  60. }
  61. @Override
  62. publicintgetCount(){
  63. returnmData.size();
  64. }
  65. @Override
  66. publicObjectgetItem(intposition){
  67. returnmData.get(position);
  68. }
  69. @Override
  70. publiclonggetItemId(intposition){
  71. return0;
  72. }
  73. @Override
  74. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  75. Viewview=convertView;
  76. ViewHoldervh;
  77. if(view==null){
  78. view=LayoutInflater.from(MainActivity.this).inflate(R.layout.list_item,null);
  79. vh=newViewHolder();
  80. vh.tv=(TextView)view.findViewById(R.id.textView);
  81. vh.iv=(ImageView)view.findViewById(R.id.imageView);
  82. view.setTag(vh);
  83. }
  84. vh=(ViewHolder)view.getTag();
  85. vh.tv.setText((String)mData.get(position).get("title"));
  86. Bitmapbitmap=(Bitmap)mData.get(position).get("pic");
  87. if(bitmap!=null){
  88. vh.iv.setImageBitmap(bitmap);
  89. }
  90. else{
  91. vh.iv.setImageBitmap(null);
  92. }
  93. AsyncTasktask=(AsyncTask)mData.get(position).get("task");
  94. if(task==null||task.isCancelled()){
  95. mData.get(position).put("task",newGetItemImageTask(position).execute(null));//启动线程异步获取图片
  96. }
  97. returnview;
  98. }
  99. }
  100. staticclassViewHolder{
  101. TextViewtv;
  102. ImageViewiv;
  103. }
  104. classGetItemImageTaskextendsAsyncTask<Void,Void,Void>{//获取图片仍采用AsyncTask,这里的优化放到下篇再讨论
  105. intid;
  106. GetItemImageTask(intid){
  107. this.id=id;
  108. }
  109. @Override
  110. protectedVoiddoInBackground(Void...params){
  111. Bitmapbm=(Bitmap)Data.getData(String.valueOf(id));
  112. if(bm!=null){//如果hashmap中已经有数据,
  113. mData.get(id).put("pic",bm);
  114. }
  115. else{//模拟从网络获取
  116. try{
  117. Thread.sleep(200);//模拟网络延时
  118. }catch(InterruptedExceptione){
  119. e.printStackTrace();
  120. }
  121. mData.get(id).put("pic",BitmapFactory.decodeResource(getResources(),R.drawable.icon));
  122. }
  123. returnnull;
  124. }
  125. protectedvoidonPostExecute(Voidresult){
  126. mAdapter.notifyDataSetChanged();
  127. }
  128. }


Data.java 静态的Hashmap保存预读数据

  1. packagecom.zhuozhuo;
  2. importjava.util.AbstractMap;
  3. importjava.util.HashMap;
  4. importjava.util.concurrent.ConcurrentHashMap;
  5. publicclassData{
  6. privatestaticAbstractMap<String,Object>mData=newConcurrentHashMap<String,Object>();
  7. privateData(){
  8. }
  9. publicstaticvoidputData(Stringkey,Objectobj){
  10. mData.put(key,obj);
  11. }
  12. publicstaticObjectgetData(Stringkey){
  13. returnmData.get(key);
  14. }
  15. }

运行结果:

Android多线程任务优化2:实现后台预读线程

Android多线程任务优化2:实现后台预读线程

从执行结果可以看到,当进入MainActivity时,listview中的第一屏的图片已经加载好了。

这个简单例子中还不能很好地体现预读带来的用户体验的优势,不过一些应用(如前面提到过的新闻阅读类应用),实现了预读机制,使响应性大大提高,增强了用户体验。

总结

1、通过实现自定义的AsyncTask来避免AsyncTask引起的FC风险和满足特定的后台异步任务需求

2、实现后台预读可以提高应用的响应性。

3、使用Executors.newSingleThreadExecutor()创建只有一个工作队列的线程池来实现预读需求。

引申

1、预读队列的工作线程可以不止一个,请根据需求配置自己的线程池。

2、adapter的getview()方法中,我们仍然采用了AsyncTask实现异步获取图片,下篇我们将探讨更好的解决办法,在提高响应性的同时,避免了AyncTask带来的FC风险

3、预读也要控制成本,存储空间、耗电和流量都是要考虑的因素。