Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> [Android][Memory Leak] InputMethodManager內存洩露現象及解決

[Android][Memory Leak] InputMethodManager內存洩露現象及解決

編輯:關於Android編程

[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

一番搜索後,已經有人也碰到過這個問題(見文章最後引用鏈接),給出的方法是:

@Override
public void onDestory() {
    //Fix memory leak: http://code.google.com/p/android/issues/detail?id=34731
    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.windowDismissed(this.getWindow().getDecorView().getWindowToken()); // hide method
    imm.startGettingWindowFocus(null); // hide method
    super.onDestory();
}


但在實踐中使用後,沒有真正解決,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. 判空邏輯

最後貼出代碼實現:

              public static void fixInputMethodManagerLeak(Context context) {
                            if (context == null) {
                                          return;
                            }
                            try {
                                          // 對 mCurRootView mServedView mNextServedView 進行置空...
                                          InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
                                          if (imm == null) {
                                                        return;
                                          }// author:sodino mail:[email protected]
 
                                          Object obj_get = null;
                                          Field f_mCurRootView = imm.getClass().getDeclaredField("mCurRootView");
                                          Field f_mServedView = imm.getClass().getDeclaredField("mServedView");
                                          Field f_mNextServedView = imm.getClass().getDeclaredField("mNextServedView");
 
                                          if (f_mCurRootView.isAccessible() == false) {
                                                        f_mCurRootView.setAccessible(true);
                                          }
                                          obj_get = f_mCurRootView.get(imm);
                                          if (obj_get != null) { // 不為null則置為空
                                                        f_mCurRootView.set(imm, null);
                                          }
 
                                          if (f_mServedView.isAccessible() == false) {
                                                        f_mServedView.setAccessible(true);
                                          }
                                          obj_get = f_mServedView.get(imm);
                                          if (obj_get != null) { // 不為null則置為空
                                                        f_mServedView.set(imm, null);
                                          }
 
                                          if (f_mNextServedView.isAccessible() == false) {
                                                        f_mNextServedView.setAccessible(true);
                                          }
                                          obj_get = f_mNextServedView.get(imm);
                                          if (obj_get != null) { // 不為null則置為空
                                                        f_mNextServedView.set(imm, null);
                                          }
                            } catch (Throwable t) {
                                          t.printStackTrace();
                            }
              }


在Activity.onDestory()方法中執行以上方法即可解決.

              public  void onDestroy() {
                            super.ondestroy();
                            fixInputMethodManagerLeak(this);
              }

事情看上去圓滿的解決了,但真的是嗎?

經過以上處理後,內存洩露是不存在了,但出現另外一個問題,就是有輸入框的地方,點擊輸入框後,卻無法出現輸入法界面了!

事故現場復現的操作步驟為:

ActivityA界面,點擊進入Activity B界面,B有輸入框,點擊輸入框後,沒有輸入法彈出。原因是InputMethodManager的關聯View已經被上面的那段代碼置空了。

事故原因得從Activity間的生命周期方法調用順序說起:

從Activity A進入Activity B的生命周期方法的調用順序是:

A.onCreate()→A.onResume()→B.onCreate()→B.onResume()→A.onStop()→A.onDestroy()
 

也就是說,Activity B已經創建並顯示了,ActivityA這裡執行onDestroy()將InputMethodManager的關聯View置空了,導致輸入法無法彈出。

原因發現了,要解決也就簡單了。

fixInputMethodManagerLeak(ContextdestContext)方法參數中將目標要銷毀的Activity A作為參數傳參進去。在代碼中,去獲取InputMethodManager的關聯View,通過View.getContext()與Activity A進行對比,如果發現兩者相同,就表示需要回收;如果兩者不一樣,則表示有新的界面已經在使用InputMethodManager了,直接不處理就可以了。

修改後,最終代碼如下:

public static void fixInputMethodManagerLeak(Context destContext) {
	if (destContext == null) {
		return;
	}
	
	InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE);
	if (imm == null) {
		return;
	}

	String [] arr = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
	Field f = null;
	Object obj_get = null;
	for (int i = 0;i < arr.length;i ++) {
		String param = arr[i];
		try{
			f = imm.getClass().getDeclaredField(param);
			if (f.isAccessible() == false) {
				f.setAccessible(true);
			} // author: sodino mail:[email protected]
			obj_get = f.get(imm);
			if (obj_get != null && obj_get instanceof View) {
				View v_get = (View) obj_get;
				if (v_get.getContext() == destContext) { // 被InputMethodManager持有引用的context是想要目標銷毀的
					f.set(imm, null); // 置空,破壞掉path to gc節點
				} else {
					// 不是想要目標銷毀的,即為又進了另一層界面了,不要處理,避免影響原邏輯,也就不用繼續for循環了
					if (QLog.isColorLevel()) {
						QLog.d(ReflecterHelper.class.getSimpleName(), QLog.CLR, "fixInputMethodManagerLeak break, context is not suitable, get_context=" + v_get.getContext()+" dest_context=" + destContext);
					}
					break;
				}
			}
		}catch(Throwable t){
			t.printStackTrace();
		}
	}
}


引用:

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved