关于Android系统级内存泄露的一些坑

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/azpoiu/article/details/59103921

今天用adb shell dumpsys meminfo命令查看公司app信息时,无意间发现了一个MainActivity的内存泄漏。具体是这样的。

打开我司app,启动Splash页面后跳转至MainActivity页面,这时通过adb shell dumpsys meminfo 命令看到我司app信息如下图:

目前只有一个activity,就是MainActivity了。这时双击back键,将MainActivity退出后,再次输入adb shell dumpsys meminfo命令。结果如下图:

看到ViewRootImpl变成了0,但是Activities仍然还是1,这表示退出我司的应用后MainActivity对象并没有被销毁,发生了内存泄漏。(这里有个点需要了解:一个应用在退出所有Activity后,系统并不会立刻把应用所在进程也干掉,但由于进程中并没有托管的Activity与用户交互,系统会把进程降级为后台进程放在LRU列表中,Low Memory Killer会根据进程的adj级别以及所占的内存,来决定是否杀掉该进程,adj越大,占用内存越多,进程越容易被杀掉。因此,MainActivit发生泄漏后由于进程还在,所以MainActivity对象仍然存在内存中)

通过dump java heap观察MainActivity实例对象的数量也印证了内存泄漏

Mat上观察MainActivity对象与GCRoots的引用关系。发现是被InputMethodManager的mServedView所持有。
这里写图片描述
Google搜索关键字”InputMethodManager Leak
“后发现原来是Android系统层的一个issue,已经有人在google code上提出来了。点我需要翻墙

这里写图片描述

翻译一下就是:InputMethodManager.mServedView 持有一个最后聚焦View的引用,直到另外的一个View聚焦后才会释放当前的View。当发生GC时mServedView(GCRoot)持有的View的引用不会被回收,导致了内存泄漏。

因为这个问题出现的频率比较高,LeakCanary上经常有这个泄漏的弹窗,所以LeakCanary的pyricau提供了一份代码解决这个问题。点击查看

但是我试了一下仍然存在泄漏的可能,无奈下又找了另一个解决方案。点击查看

具体代码贴出:

    public static void fixInputMethodManagerLeak(Context destContext) {

        if (SDK_INT < KITKAT || SDK_INT > 22) {
            return;
        }

        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 == null) {
                    return;
                }
                f.setAccessible(true);
                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循环了
                        break;
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

这个方案很暴力,使用反射得到InputMethodManager的成员变量(”mCurRootView”, “mServedView”, “mNextServedView”),在将这些Field置空,断掉View与GCRoot的链接。

在MainActivity的onDestroy中使用这个方法后果然解决了InputMethodManager内存泄漏的问题。

但事情还没有结束。。。

因为我司的MainActivity下方有五个按钮,点击某个按钮就会切换到具体的Fragment,MainActivity只充当了一个容器。一般来讲大部分app进入MainActivity都会初始化第一个Fragment,之前退出app而MainActivity没被释放也是因为第一个Fragment被InputMethodManager持有了引用导致内存泄漏,也通过上面的方案解决了。但当我切换到第二Fragment后退出MainActivity发现Activities的数量仍然是1,也就是说第二个Fragment也存在内存泄漏!而且和第一个Fragment的原因不一样。

没办法,dump java heap后使用Mat分析引用关系如下:
这里写图片描述

发现是TextLine的sCached持用了MainActivity的引用。Google后发现这也是一个Android系统级别的内存泄漏。点击查看

TextLine.sCached is a pool of 3 TextLine instances. TextLine.recycle() has had at least two
bugs that created memory leaks by not correctly clearing the recycled TextLine instances.
The first was fixed in android-5.1.0_r1:
https://github.com/android/platform_frameworks_base/commit/893d6fe48d37f71e683f722457bea646994a10bf
The second was fixed, not released yet:
https://github.com/android/platform_frameworks_base/commit/b3a9bc038d3a218b1dbdf7b5668e3d6c12be5ee4
Hack: to fix this, you could access TextLine.sCached and clear the pool every now and then
(e.g. on activity destroy).

上面说这个问题已经在android-5.1.0_r1版本中修复了,不过要兼容低版本还是要处理一下的,方案和上面InputMethodManager的处理方式差不多都是通过反射成员变量后置空断开与GCRoot的引用。在生命周期的onDestroy中调用下面的代码即可:

    public static void fixTextLineCacheLeak() {

        if (SDK_INT > LOLLIPOP) {
            return;
        }
        try {
            Field textLineCached;

            textLineCached = Class.forName("android.text.TextLine").getDeclaredField("sCached");
            if (textLineCached == null) {
                return;
            }
            textLineCached.setAccessible(true);

            // Get reference to the TextLine sCached array.
            Object cached = textLineCached.get(null);
            if (cached != null) {
                // Clear the array.
                for (int i = 0, size = Array.getLength(cached); i < size; i++) {
                    Array.set(cached, i, null);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

总结:个人认为关于系统级别的内存泄漏,如果影响不是很大的话其实是没必要主动去修复的,因为修复系统级泄漏普遍做法是通过暴力反射切断变量之间的引用关系,但是Android框架在使用这些变量时有没有增加空指针保护就不清楚了,再加上国内的手机厂商对Framework改动了不少,我们的修改并不一定具有很强的兼容性。反而修复后很可能会出现一些想不到的bug,到时定位都不好定位。但深入了解这些内存泄漏的原因,高效排查出内存泄漏的方法是我们应用开发工程师必不可少的技能。


展开阅读全文

没有更多推荐了,返回首页