現象: 在特定的機型天語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