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