`
king_tt
  • 浏览: 2083835 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Android 非UI线程使用View.post()方法一处潜在的内存泄漏

 
阅读更多

最近开发中,使用 AsyncTask + ProgressDialog 显示进度信息,但在AsyncTask停止,Activity finish 后该Activity的实例始终不会被gc,多次运行程序后,会存在多个activity,造成内存泄漏。 下面详细分析一下:

一份显示进度条的测试代码:

  1. publicclassMainextendsActivity{
  2. @Override
  3. protectedvoidonCreate(BundlesavedInstanceState){
  4. super.onCreate(savedInstanceState);
  5. TextViewtv=newTextView(this);
  6. tv.setText("InitState");
  7. setContentView(tv);
  8. tv.setOnClickListener(newOnClickListener(){
  9. @Override
  10. publicvoidonClick(Viewv){
  11. showProgress(Main.this);
  12. }
  13. });
  14. }
  15. publicvoidshowProgress(finalActivityactivity){
  16. newAsyncTask<Void,Void,Void>(){
  17. ProgressDialogprogressDial;
  18. protectedvoidonPreExecute(){
  19. progressDial=newProgressDialog(activity);
  20. progressDial.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
  21. progressDial.show();
  22. };
  23. @Override
  24. protectedVoiddoInBackground(Void...params){
  25. doSomeHeavyWork(progressDial);
  26. returnnull;
  27. }
  28. protectedvoidonPostExecute(Voidresult){
  29. progressDial.dismiss();
  30. };
  31. }.execute();
  32. }
  33. voiddoSomeHeavyWork(ProgressDialogprogress){
  34. try{
  35. for(inti=1;i<=10;++i){
  36. progress.setProgress(i);
  37. Thread.sleep(1000);
  38. }
  39. }catch(Exceptione){
  40. }
  41. }
  42. }

上述代码发生内存泄漏的地方在 doSomeHeavyWork() 的 progress.setProgress(i);部分;我们看一下setProgress()的实现,最终会调用ProgressBar 类的如下方法:

  1. privatesynchronizedvoidrefreshProgress(intid,intprogress,booleanfromUser){
  2. if(mUiThreadId==Thread.currentThread().getId()){
  3. doRefreshProgress(id,progress,fromUser,true);
  4. }else{
  5. RefreshProgressRunnabler;
  6. if(mRefreshProgressRunnable!=null){
  7. //UsecachedRefreshProgressRunnableifavailable
  8. r=mRefreshProgressRunnable;
  9. //Uncacheit
  10. mRefreshProgressRunnable=null;
  11. r.setup(id,progress,fromUser);
  12. }else{
  13. //Makeanewone
  14. r=newRefreshProgressRunnable(id,progress,fromUser);
  15. }
  16. post(r);
  17. }
  18. }
  1. privateclassRefreshProgressRunnableimplementsRunnable{
  2. privateintmId;
  3. privateintmProgress;
  4. privatebooleanmFromUser;
  5. RefreshProgressRunnable(intid,intprogress,booleanfromUser){
  6. mId=id;
  7. mProgress=progress;
  8. mFromUser=fromUser;
  9. }
  10. publicvoidrun(){
  11. doRefreshProgress(mId,mProgress,mFromUser,true);
  12. //Putourselvesbackinthecachewhenwearedone
  13. mRefreshProgressRunnable=this;
  14. }
  15. publicvoidsetup(intid,intprogress,booleanfromUser){
  16. mId=id;
  17. mProgress=progress;
  18. mFromUser=fromUser;
  19. }
  20. }

if 语句表明当调用的该方法的线程是UI线程时,则直接执行doRefreshProgress() 方法以刷新界面;否则,创建一个RefreshProgressRunnable,并通过调用View.pos()方法将其插入到UI线程的消息队列中。 View.post()实现如下:

  1. publicbooleanpost(Runnableaction){
  2. Handlerhandler;
  3. AttachInfoattachInfo=mAttachInfo;
  4. if(attachInfo!=null){
  5. handler=attachInfo.mHandler;
  6. }else{
  7. //Assumethatpostwillsucceedlater
  8. ViewRootImpl.getRunQueue().post(action);
  9. returntrue;
  10. }
  11. returnhandler.post(action);
  12. }

在post() 函数注释中,明确写着:This method can be invoked from outside of the UI threadonly when this View is attached to a window.

当ProgressDialog还没有attach到当前window时(ProgressDialog.show() 方法是异步执行的),mAttachInfo 值为 null,故而执行 else语句,再看一下getRunQueue()和其post() 方法:

  1. staticfinalThreadLocal<RunQueue>sRunQueues=newThreadLocal<RunQueue>();
  2. staticRunQueuegetRunQueue(){
  3. RunQueuerq=sRunQueues.get();
  4. if(rq!=null){
  5. returnrq;
  6. }
  7. rq=newRunQueue();
  8. sRunQueues.set(rq);
  9. returnrq;
  10. }
  11. ……
  12. staticfinalclassRunQueue{
  13. privatefinalArrayList<HandlerAction>mActions=newArrayList<HandlerAction>();
  14. voidpost(Runnableaction){
  15. postDelayed(action,0);
  16. }
  17. voidpostDelayed(Runnableaction,longdelayMillis){
  18. HandlerActionhandlerAction=newHandlerAction();
  19. handlerAction.action=action;
  20. handlerAction.delay=delayMillis;
  21. synchronized(mActions){
  22. mActions.add(handlerAction);
  23. }
  24. }
  25. voidexecuteActions(Handlerhandler){
  26. synchronized(mActions){
  27. finalArrayList<handleraction>actions=mActions;
  28. finalintcount=actions.size();
  29. for(inti=0;i<count;i++){
  30. finalHandlerActionhandlerAction=actions.get(i);
  31. handler.postDelayed(handlerAction.action,handlerAction.delay);
  32. }
  33. actions.clear();
  34. }
  35. }
  36. ……
  37. }
  38. andleraction>

这样会把ProgressBar的RefreshProgressRunnable 插入到一个静态的ThreadLocal的RunQueue队列里,针对本文开头给出的例子,刷新进度的Runnable被插入到了AsyncTask 所在线程的RunQueue里; 那么插入的Runnable什么时候得到执行呢?

调用RunQueue.executeActions()方法只有一处,即在ViewRootImpl类的如下非静态方法中

  1. privatevoidperformTraversals(){
  2. ……
  3. if(mLayoutRequested&&!mStopped){
  4. //Executeenqueuedactionsoneverylayoutincaseaviewthatwasdetached
  5. //enqueuedanactionafterbeingdetached
  6. getRunQueue().executeActions(attachInfo.mHandler);
  7. ……
  8. }
  9. ……
  10. }
该方法是在UI线程执行的(见ViewRootImpl.handleMessage()), 故当UI线程执行到该performTraversals() 里的 getRunQueue() 时,得到的是UI线程中的RunQueue,这样AsyncTask 线程中的 RunQueue永远不会被执行到, 并且AsyncTask的是用线程池实现的,AsyncTask启动的线程会长期存在,造成如下引用关系:

AsyncTask线程 => 静态的ThreadLocal的RunQueue => Runnable => ProgressBar => Activity;

如此即使activity finish 了,确始终存在一个静态引用链引用这该activity,而 Activity一般又引用着很多资源,比如图片等,最终造成严重资源泄漏。


另外,上述问题不限与ProgressBar,凡是在非UI线程使用view.post()方法,如果view没有被attach,则均存在潜在的内存泄漏的问题!

针对本文给出的ProgressBar例子,一个简单fix方法实在 AsyncTask的doInbackground() 开始处sleep(500) 即可。 更为精准的方式可使用如下循环测试:

  1. Viewv=progressBar.getWindow().getDecorView();
  2. while(v.getWindowToken()==null){
  3. Thread.sleep(10);
  4. }

上述ProgressBar例子,并不是总能再现内存泄漏的情况的(因为异步执行的不缺定性),下面再给出一个更容易再现类似问题的例子:

  1. publicclassMainextendsActivity{
  2. publicstaticinta=0;
  3. @Override
  4. protectedvoidonCreate(BundlesavedInstanceState){
  5. super.onCreate(savedInstanceState);
  6. TextViewtv=newTextView(this);
  7. tv.setText("InitState");
  8. setContentView(tv);
  9. if(a++>3){
  10. return;
  11. }
  12. newAsyncTask<TextView,Void,Void>(){
  13. @Override
  14. protectedVoiddoInBackground(TextView...params){
  15. try{
  16. TextViewtv=params[0];
  17. //Thread.sleep(500);
  18. for(inti=1;i<=3;++i){
  19. doSomeHeavyWork(tv,"AsyncTask:"+i);
  20. Thread.sleep(1000);
  21. }
  22. }catch(Exceptione){
  23. }
  24. returnnull;
  25. }
  26. protectedvoidonPostExecute(Voidresult){
  27. recreate();
  28. };
  29. }.execute(tv);
  30. }
  31. voiddoSomeHeavyWork(finalTextViewtv,finalStringtext){
  32. tv.post(newRunnable(){
  33. @Override
  34. publicvoidrun(){
  35. tv.setText(text);
  36. }
  37. });
  38. }

现象是: TextView 很大概率不会显示 "AsyncTask: 1“ 文字; 而把 Thread.sleep(500)注释掉后,一切ok!

Log.e("Token:",tv.getWindowToken()+"");//输出的Token为null,这时候还没有和Activity绑定
Thread.sleep(500);
Log.e("Token:",tv.getWindowToken()+"");//输出的Token不为null,这时候已经和Activity绑定

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics