編輯:關於Android編程
現象:
在特定的機型天語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
BlueStacks安卓模擬器屏幕窗口大小的調整方法,使用過BlueStacks安卓模擬器的朋友都知道,這款安卓模擬器非常好用,占用資源很少,但是有個缺點是
前言話說這universalimageloader加載圖片對搞過2年安卓程序都是用爛了再熟悉不過了,就是安卓新手也是百度就會有一大堆東西出來,今天為什麼這裡還要講使用un
本文實例講述了Android控件之ListView用法。分享給大家供大家參考。具體如下:示例一:在android開發中ListView是比較常用的組件,它以列表的形式展示
前面為大家講過計時器的順時針的兩種方法,在錄制視頻等操作中頗有使用,今天就給大家帶來倒計時實現的兩種方式。雖然最近寫的都比較簡單和基礎,不過簡單不代表熟悉,基礎不代表就會
Adapter相當於一個數據源,可以給AdapterView提供數據,並