InputMethodManager內存泄露現象及解決

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

現象: 在特定的機型天語k_touch_v9機型上,某個界面上出現InputMethodManager持有一Activity,致使該Activity沒法回收.若是該Activity再次被打開,則舊的會釋放掉,但新打開的會被繼續持有沒法釋放回收.MAT顯示Path to gc以下:android

輸入圖片說明

圖1. Leak pathide

天語k_touch_v9手機版本信息:工具

輸入圖片說明

圖2. K_touch_v9ui

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

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.以下圖:google

輸入圖片說明

圖3. Unknownpath編碼

搜索來的代碼無論用,就再想辦法..net

要想讓Activity釋放掉,思路就是將path togc這個鏈路剪斷就能夠.在這個bug中這個鏈路上有兩個節點mContext(DecorView)和 mCurRootView(InputMethodManager)可供考慮.下面思路就是從這兩個節點中選擇一個入手剪斷path to gc便可. 閱讀源碼可知,日誌

DecorView繼承自FrameLayout,mContext是其上下文環境,牽涉太多,不適合操做入手.mCurRootView在InputMehtodManager中的使用就簡單得多了,在被賦值初始化後,被使用的場景只有一次判斷及一第二天志打印.因此這裏選中mCurRootView爲突破口.剪斷其path to gc的操做爲經過Java Reflection方法將mCurRootView置空便可(見文後代碼). 編碼實現後,再測,發現仍有泄露,但泄露狀況有所變化,以下圖:code

輸入圖片說明

圖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:sodino@qq.com  

		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:sodino@qq.com  
			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();  
		}  
	}  
}

Android InputMethodManager 致使的內存泄露及解決方案

https://zhuanlan.zhihu.com/p/20828861?refer=zmywly8866

內存分析工具 MAT 的使用

http://blog.csdn.net/aaa2832/article/details/19419679

引用:

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
相關文章
相關標籤/搜索