[转][Android][Memory Leak] InputMethodManager内存泄露现象及解决

来源:互联网 发布:卖家开通淘宝客条件 编辑:程序博客网 时间:2024/06/17 12:12
 

http://blog.csdn.net/sodino/article/details/32188809

标签: 内存泄露android
2014-06-18 22:54 11537人阅读 评论(9) 收藏 举报
 分类:

版权声明:本文为博主原创文章,未经博主允许不得转载。

[Android][Memory Leak] InputMethodManager内存泄露现象及解决

现象:

         在特定的机型天语k_touch_v9机型上,某个界面上出现InputMethodManager持有一Activity,导致该Activity无法回收.如果该Activity再次被打开,则旧的会释放掉,但新打开的会被继续持有无法释放回收.MAT显示Path to gc如下:

图1. Leak path

         天语k_touch_v9手机版本信息:

图2. K_touch_v9

 

一番搜索后,已经有人也碰到过这个问题(见文章最后引用链接),给出的方法是:

[java] view plain copy
  1. @Override  
  2. public void onDestory() {  
  3.     //Fix memory leak: http://code.google.com/p/android/issues/detail?id=34731  
  4.     InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);  
  5.     imm.windowDismissed(this.getWindow().getDecorView().getWindowToken()); // hide method  
  6.     imm.startGettingWindowFocus(null); // hide method  
  7.     super.onDestory();  
  8. }  


但在实践中使用后,没有真正解决,Activity仍存在,但path to gc指向为unknown.如下图:

图3. Unknownpath

 

搜索来的代码不管用,就再想办法.

要想让Activity释放掉,思路就是将path togc这个链路剪断就可以.在这个bug中这个链路上有两个节点mContext(DecorView)和mCurRootView(InputMethodManager)可供考虑.下面思路就是从这两个节点中选择一个入手剪断path to gc即可.

阅读源码可知, DecorView继承自FrameLayout,mContext是其上下文环境,牵涉太多,不适合操作入手.mCurRootView在InputMehtodManager中的使用就简单得多了,在被赋值初始化后,被使用的场景只有一次判断及一次日志打印.所以这里选中mCurRootView为突破口.剪断其path to gc的操作为通过Java Reflection方法将mCurRootView置空即可(见文后代码).

编码实现后,再测,发现仍有泄露,但泄露情况有所变化,如下图:

图4. Leak path

         新的泄露点为mServedView/mNextServedView,可以通过同样的JavaReflection将其置空,剪断path to gc.但这里有个问题得小心,这里强制置空后,会不会引起InputMethodManager的NullPointerException呢?会不会引起系统内部逻辑崩溃?再次查阅源码,发现mServedView及mNextServedView在代码逻辑中一直有判空逻辑,所以这时就可以放心的强制置空来解决问题了.


图5. 判空逻辑

         最后贴出代码实现:

[java] view plain copy
  1. public static void fixInputMethodManagerLeak(Context context) {  
  2.               if (context == null) {  
  3.                             return;  
  4.               }  
  5.               try {  
  6.                             // 对 mCurRootView mServedView mNextServedView 进行置空...  
  7.                             InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);  
  8.                             if (imm == null) {  
  9.                                           return;  
  10.                             }// author:sodino mail:sodino@qq.com  
  11.   
  12.                             Object obj_get = null;  
  13.                             Field f_mCurRootView = imm.getClass().getDeclaredField("mCurRootView");  
  14.                             Field f_mServedView = imm.getClass().getDeclaredField("mServedView");  
  15.                             Field f_mNextServedView = imm.getClass().getDeclaredField("mNextServedView");  
  16.   
  17.                             if (f_mCurRootView.isAccessible() == false) {  
  18.                                           f_mCurRootView.setAccessible(true);  
  19.                             }  
  20.                             obj_get = f_mCurRootView.get(imm);  
  21.                             if (obj_get != null) { // 不为null则置为空  
  22.                                           f_mCurRootView.set(imm, null);  
  23.                             }  
  24.   
  25.                             if (f_mServedView.isAccessible() == false) {  
  26.                                           f_mServedView.setAccessible(true);  
  27.                             }  
  28.                             obj_get = f_mServedView.get(imm);  
  29.                             if (obj_get != null) { // 不为null则置为空  
  30.                                           f_mServedView.set(imm, null);  
  31.                             }  
  32.   
  33.                             if (f_mNextServedView.isAccessible() == false) {  
  34.                                           f_mNextServedView.setAccessible(true);  
  35.                             }  
  36.                             obj_get = f_mNextServedView.get(imm);  
  37.                             if (obj_get != null) { // 不为null则置为空  
  38.                                           f_mNextServedView.set(imm, null);  
  39.                             }  
  40.               } catch (Throwable t) {  
  41.                             t.printStackTrace();  
  42.               }  
  43. }  


         在Activity.onDestory()方法中执行以上方法即可解决.

[java] view plain copy
  1. public  void onDestroy() {  
  2.               super.ondestroy();  
  3.               fixInputMethodManagerLeak(this);  
  4. }  

 

         事情看上去圆满的解决了,但真的是吗?

         经过以上处理后,内存泄露是不存在了,但出现另外一个问题,就是有输入框的地方,点击输入框后,却无法出现输入法界面了!

         事故现场复现的操作步骤为:

         ActivityA界面,点击进入Activity B界面,B有输入框,点击输入框后,没有输入法弹出。原因是InputMethodManager的关联View已经被上面的那段代码置空了。

         事故原因得从Activity间的生命周期方法调用顺序说起:

         从Activity A进入Activity B的生命周期方法的调用顺序是:

 

[java] view plain copy
  1. A.onCreate()→A.onResume()→B.onCreate()→B.onResume()→A.onStop()→A.onDestroy()  
  2.    

也就是说,Activity B已经创建并显示了,ActivityA这里执行onDestroy()将InputMethodManager的关联View置空了,导致输入法无法弹出。

原因发现了,要解决也就简单了。

fixInputMethodManagerLeak(ContextdestContext)方法参数中将目标要销毁的Activity A作为参数传参进去。在代码中,去获取InputMethodManager的关联View,通过View.getContext()与Activity A进行对比,如果发现两者相同,就表示需要回收;如果两者不一样,则表示有新的界面已经在使用InputMethodManager了,直接不处理就可以了。

修改后,最终代码如下:

[java] view plain copy
  1. public static void fixInputMethodManagerLeak(Context destContext) {  
  2.     if (destContext == null) {  
  3.         return;  
  4.     }  
  5.       
  6.     InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE);  
  7.     if (imm == null) {  
  8.         return;  
  9.     }  
  10.   
  11.     String [] arr = new String[]{"mCurRootView""mServedView""mNextServedView"};  
  12.     Field f = null;  
  13.     Object obj_get = null;  
  14.     for (int i = 0;i < arr.length;i ++) {  
  15.         String param = arr[i];  
  16.         try{  
  17.             f = imm.getClass().getDeclaredField(param);  
  18.             if (f.isAccessible() == false) {  
  19.                 f.setAccessible(true);  
  20.             } // author: sodino mail:sodino@qq.com  
  21.             obj_get = f.get(imm);  
  22.             if (obj_get != null && obj_get instanceof View) {  
  23.                 View v_get = (View) obj_get;  
  24.                 if (v_get.getContext() == destContext) { // 被InputMethodManager持有引用的context是想要目标销毁的  
  25.                     f.set(imm, null); // 置空,破坏掉path to gc节点  
  26.                 } else {  
  27.                     // 不是想要目标销毁的,即为又进了另一层界面了,不要处理,避免影响原逻辑,也就不用继续for循环了  
  28.                     if (QLog.isColorLevel()) {  
  29.                         QLog.d(ReflecterHelper.class.getSimpleName(), QLog.CLR, "fixInputMethodManagerLeak break, context is not suitable, get_context=" + v_get.getContext()+" dest_context=" + destContext);  
  30.                     }  
  31.                     break;  
  32.                 }  
  33.             }  
  34.         }catch(Throwable t){  
  35.             t.printStackTrace();  
  36.         }  
  37.     }  
  38. }  


 

引用:

l InputMethodManager:googlecode

l InputMethodManger导致的Activity泄漏

l MainActivity is not garbage collected after destruction because it is referenced byInputMethodManager indirectly

l InputMethodManagerholds reference to the tabhost - Memory Leak - OOM Error

0 0