今年开发的一个项目发生了内存泄漏,在六七月份时就观察到即使退出了所有的activity,但是app在后台占用的内存还是没有降下来,保持到75M左右。一直以为是项目使用的代码出现问题,然后继续优化,可是无论怎么优化都达不到合理的内存占用值,最后挤挤牙膏也是降到了60M那样。当时优化了一周效果并不明显。好在60M对于我们应用来说还能够接受,就继续开发迭代功能。最近其他项目迭代开发完成比较有空了,就回到原来项目区看看还能不能继续优化。由于当时项目用的Android studio2.3作为开发工具所以内存调优工具没有那么好,于是就换到最新的Android studio3.2
Android studio3.2的内存工具是profiler,在Android studio的左下角可以打开
经过两天的debug准确定位了问题
这里就形成了变量循环链
可以看到因为InputMethodManager的生命周期非常长,导致即使activity被onDestroy()后也无法顺利被回收,进而导致更多变量无法回收的内存泄漏。
为此特地Google了一把,发现从Android4.4到Android6.0这个bug一直存在。
解决方案:这个问题在不少人遇到过,已经有成形的代码可以使用,具体代码下面的GitHub
https://gist.github.com/pyricau/4df64341cc978a7de414
当然这份代码有两个地方需要修改
public static void fixFocusedViewLeak(Application application) {
// Don't know about other versions yet.
if (SDK_INT < KITKAT || SDK_INT > 22) {
return;
}
//......
}
修改原因
1. InputMethodManager内存泄漏的bug在Android6.0还存在;
2. SDK_INT > 22使用了魔鬼数字,可以更新最新的SDK将魔鬼数字改为Version代号
修改结果如下:
public static void fixFocusedViewLeak(Application application) {
// Don't know about other versions yet.
if (SDK_INT < Build.VERSION_CODES.KITKAT || SDK_INT > Build.VERSION_CODES.M) {
return;
}
//......
}
以下代码解决了在activity退出后立即将view与InputMethodManager解绑
package com.lucky.utils;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.KITKAT;
public class IMMLeaks {
static class ReferenceCleaner
implements MessageQueue.IdleHandler, View.OnAttachStateChangeListener,
ViewTreeObserver.OnGlobalFocusChangeListener {
private final InputMethodManager inputMethodManager;
private final Field mHField;
private final Field mServedViewField;
private final Method finishInputLockedMethod;
ReferenceCleaner(InputMethodManager inputMethodManager, Field mHField, Field mServedViewField,
Method finishInputLockedMethod) {
this.inputMethodManager = inputMethodManager;
this.mHField = mHField;
this.mServedViewField = mServedViewField;
this.finishInputLockedMethod = finishInputLockedMethod;
}
@Override public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if (newFocus == null) {
return;
}
if (oldFocus != null) {
oldFocus.removeOnAttachStateChangeListener(this);
}
Looper.myQueue().removeIdleHandler(this);
newFocus.addOnAttachStateChangeListener(this);
}
@Override public void onViewAttachedToWindow(View v) {
}
@Override public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
Looper.myQueue().removeIdleHandler(this);
Looper.myQueue().addIdleHandler(this);
}
@Override public boolean queueIdle() {
clearInputMethodManagerLeak();
return false;
}
private void clearInputMethodManagerLeak() {
try {
Object lock = mHField.get(inputMethodManager);
// This is highly dependent on the InputMethodManager implementation.
synchronized (lock) {
View servedView = (View) mServedViewField.get(inputMethodManager);
if (servedView != null) {
boolean servedViewAttached = servedView.getWindowVisibility() != View.GONE;
if (servedViewAttached) {
// The view held by the IMM was replaced without a global focus change. Let's make
// sure we get notified when that view detaches.
// Avoid double registration.
servedView.removeOnAttachStateChangeListener(this);
servedView.addOnAttachStateChangeListener(this);
} else {
// servedView is not attached. InputMethodManager is being stupid!
Activity activity = extractActivity(servedView.getContext());
if (activity == null || activity.getWindow() == null) {
// Unlikely case. Let's finish the input anyways.
finishInputLockedMethod.invoke(inputMethodManager);
} else {
View decorView = activity.getWindow().peekDecorView();
boolean windowAttached = decorView.getWindowVisibility() != View.GONE;
if (!windowAttached) {
finishInputLockedMethod.invoke(inputMethodManager);
} else {
decorView.requestFocusFromTouch();
}
}
}
}
}
} catch (IllegalAccessException | InvocationTargetException unexpected) {
Log.e("IMMLeaks", "Unexpected reflection exception", unexpected);
}
}
private Activity extractActivity(Context context) {
while (true) {
if (context instanceof Application) {
return null;
} else if (context instanceof Activity) {
return (Activity) context;
} else if (context instanceof ContextWrapper) {
Context baseContext = ((ContextWrapper) context).getBaseContext();
// Prevent Stack Overflow.
if (baseContext == context) {
return null;
}
context = baseContext;
} else {
return null;
}
}
}
}
/**
* Fix for https://code.google.com/p/android/issues/detail?id=171190 .
*
* When a view that has focus gets detached, we wait for the main thread to be idle and then
* check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got
* focus, which is what happens if you press home and come back from recent apps. This replaces
* the reference to the detached view with a reference to the decor view.
*
* Should be called from {@link Activity#onCreate(android.os.Bundle)} )}.
*/
public static void fixFocusedViewLeak(Application application) {
// Don't know about other versions yet.
if (SDK_INT < Build.VERSION_CODES.KITKAT || SDK_INT > Build.VERSION_CODES.M) {
return;
}
final InputMethodManager inputMethodManager =
(InputMethodManager) application.getSystemService(INPUT_METHOD_SERVICE);
final Field mServedViewField;
final Field mHField;
final Method finishInputLockedMethod;
final Method focusInMethod;
try {
mServedViewField = InputMethodManager.class.getDeclaredField("mServedView");
mServedViewField.setAccessible(true);
mHField = InputMethodManager.class.getDeclaredField("mServedView");
mHField.setAccessible(true);
finishInputLockedMethod = InputMethodManager.class.getDeclaredMethod("finishInputLocked");
finishInputLockedMethod.setAccessible(true);
focusInMethod = InputMethodManager.class.getDeclaredMethod("focusIn", View.class);
focusInMethod.setAccessible(true);
} catch (NoSuchMethodException | NoSuchFieldException unexpected) {
Log.e("IMMLeaks", "Unexpected reflection exception", unexpected);
return;
}
application.registerActivityLifecycleCallbacks(new LifecycleCallbacksAdapter() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
ReferenceCleaner cleaner =
new ReferenceCleaner(inputMethodManager, mHField, mServedViewField,
finishInputLockedMethod);
View rootView = activity.getWindow().getDecorView().getRootView();
ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver();
viewTreeObserver.addOnGlobalFocusChangeListener(cleaner);
}
});
}
}
LifeCycleCallbacksAdapter.java
package com.lucky.utils;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
/** Helper to avoid implementing all lifecycle callback methods. */
public class LifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override public void onActivityStarted(Activity activity) {
}
@Override public void onActivityResumed(Activity activity) {
}
@Override public void onActivityPaused(Activity activity) {
}
@Override public void onActivityStopped(Activity activity) {
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override public void onActivityDestroyed(Activity activity) {
}
}
最后在项目的Application中注册回调即可
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
InputMethodManagerLeak.fixFocusedViewLeak(this);
}
}
终于解决了问题,一直以为是自己的代码有问题,没想到是一个系统的bug——Android4.4到Android6.0,真是不容易。