[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
一番搜尋後,已經有人也碰到過這個問題(見文章最後引用連結),給出的方法是:
@Overridepublic 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: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); }
事情看上去圓滿的解決了,但真的是嗎?
經過以上處理後,記憶體泄露是不存在了,但出現另外一個問題,就是有輸入框的地方,點擊輸入框後,卻無法出現IME介面了!
事故現場複現的操作步驟為:
ActivityA介面,點擊進入Activity B介面,B有輸入框,點擊輸入框後,沒有IME彈出。原因是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置空了,導致IME無法彈出。
原因發現了,要解決也就簡單了。
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.comobj_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