阅读 1043

【内存泄露】Android常见Java内存泄露分析

前言

自从工作以来,有很长一段时间没发博客了。很久之前说要对我的个人开源库AppInject原理做一个讲解也搁置了很久...这里挖个坑,11月之内把这块补齐...

最近接手了项目中比较老的一个模块(可能有8年以上的历史了),性能组的同学扫出了很多模块内存在的内存泄露问题,然后我最近工作的状态的就是一边赶需求一边修复内存泄露(同时还要补上单测)。解决了蛮多的内存泄漏问题,也对这一块有了更多的理解,这里做一些简单的总结。

何为内存泄露?

内存泄露的本质就是本该被回收的对象没有被回收,具体例子如已经关闭的页面Activity对象一直存在于内存中得不到释放,慢慢累积直到OOM,发生crash。可能有人会有疑问,JVM自带垃圾回收机制 (GC),不需要像C/C++需要手动释放内存,为什么会存在内存泄露呢?

这里就要简单的了解一下JVM回收垃圾的机制了,并不是所有的对象都会在GC时被回收,简单来说,如果你的对象正在被使用,他肯定是不会被回收的(否则谁还敢用JVM啊)。而这里的正在被使用对于JVM来讲,是通过可达性分析来确定的,即从一些固定确定的不会被回收的对象按照引用关系出发,被引用到的对象都是不会被回收的。这里的 “固定确定的不会被回收的对象” 主要是以下几类:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象:即方法中的局部变量,包括参数和在方法中定义的一些变量,都属于GCRoot。这里要注意每个线程都有一个栈,不单单只主线程。
  • 方法区中类静态属性引用的对象:被static修饰的对象都属于GCRoot,不会被回收。
  • 方法区中常量引用的对象:主要是class文件信息和一些常量(字符串、数值等)。这一块一般不会引起内存泄露,对于分析内存泄露可以忽略。
  • 本地方法栈中Native方法引用的对象:Native方法中可以通过诸如NewGlobalRef等方法使对象成为GCRoot,不会被回收。本篇主要讨论Java层,不涉及这一块。

常见内存泄露点分析

既然知道了为什么内存不会被释放,我们就可以从一些常见的场景分析一下内存泄露的情况。

订阅关系

写Android的肯定接触了不少XXListenerXXCallback,例如我们要做一个做一个网络请求,经常能看见这种代码:

public class XXActivity extends Activity implements XXListener{
	 // ...

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...
        xxRepository().addListener(this);
    }
    
    public void doRequest(){
        xxRepository().request();
    }
    
    @Override
    public void onRequestFinish(XXX data) {
        // update UI...
    }
    
    // ...
    
    
}
复制代码

这里的Activity实现了XXListener,并传入了xxRepository。那么此时,xxRepository这个对象中,虽然只是拿到了XXListener这一层接口,但实际上拿到的这个对象就是Activity的对象。如果xxRepository这个对象是全局的(例如单例)或者生命周期长于这个Activity,那么当我们离开这个页面,就会因为xxRepository仍旧持有这个Activity的引用导致内存泄露。所以,这种订阅的关系需要在对象生命周期结束时解除掉订阅关系,比如onDestroy时。

@Override
protected void onDestroy() {
    super.onDestroy();
    xxRepository().removeListener(this);
}
复制代码

当然,我解决的那几个有关订阅关系的内存泄露,是存在显示调用解除订阅关系的,但有一些是因为多线程并发没有正确解除订阅关系,有的是因为上层生命周期管理出现了问题,没有走到对象的destroy方法去。这里给出的例子,只是最简单的一种情况,但在实际开发过程中,每个人都应该养成及时解除订阅关系的习惯。

匿名内部类

这里直接举例子,如下:

public class XXActivity extends Activity implements XXListener {
    private View xxView;
    private int id;
    
    // ...
    
    public void doRequest() {
        SomeHeavyTaskManager.getInstance().doHeavyTaskAsync(new ICallback() {
            @Override
            public void onResult(Object data) {
                xxView.updateData(data);
                doSomothing(id);
            }
        });
    }
    
    // ...
}
复制代码

上述的doHeavyTaskAsync会在工作线程执行的无法预知执行时长的任务,然后再抛回主线程调用传入的ICallbackonResult方法。所以,就会存在这样的情况,用户已经退出这个页面很久了 ,doHeavyTaskAsync还没有回调,此时工作线程一定持有传入的ICallback的引用,这个时候我们的XXActivity就会通过这个匿名内部类泄露了。

为什么泄露的是XXAcivity,因为匿名内部类和普通的不带static的内部类一样,会在内部持有到外部类实例的引用,一般dump内存后我们看到的this$0就是对外部类的this

对于这种泄露,是很难察觉的。有人可能会以为仅仅是xxView泄露了,从而只是把xxView包了一层弱引用传入ICallback,如下方代码所示,这样就没有干净的解决掉这个内存泄露,XXActivity仍然会因为ICallback内存泄露:

public class XXActivity extends Activity implements XXListener {
    private View xxView;
    private int id;

    // ...

    public void doRequest() {
        final WeakReference<View> xxViewRef = new WeakReference<>(xxView);
        SomeHeavyTaskManager.getInstance().doHeavyTaskAsync(new ICallback() {
            @Override
            public void onResult(Object data) {
                View view = xxViewRef.get();
                if (view != null) {
                    view.updateData(data);
                }
                doSomothing(id);
            }
        });
    }

    // ...
}
复制代码

对于这种情况,建议把回调单独写做一个类,或者使用静态内部类来实现,回调中需要用到的参数如果是基本类型,直接赋值传入回调类中,非基本类型视情况使用深拷贝或者弱引用方式传入,解决后的代码如下所示:

public class XXActivity extends Activity implements XXListener {
    private View xxView;
    private int id;

    // ...

    public void doRequest() {
        final WeakReference<View> xxViewRef = new WeakReference<>(xxView);
        SomeHeavyTaskManager.getInstance().doHeavyTaskAsync(new HeavyTaskCallback(xxView, id));
    }

	// 注意,是静态内部类,否则仍然会持有外部类实例
    private static class HeavyTaskCallback implements ICallback {
        private final WeakReference<View> xxViewRef;
        private final int id;

        private HeavyTaskCallback(View xxView, int id) {
            // View有自己的生命周期,使用弱引用
            this.xxViewRef = new WeakReference<>(xxView);
            // id是基本类型,赋值即可
            this.id = id;
        }

        @Override
        public void onResult(Object data) {
            View view = xxViewRef.get();
            if (view != null) {
                view.updateData(data);
            }
            doSomothing(id);
        }
    }

    // ...
}
复制代码

这样,即使doHeavyTaskAsync中在另一个线程执行了很久,也仅仅只会持有new出来的HeavyTaskCallback这一个对象,其中的xxViewRef是弱引用,也不会影响到这个View的回收。

InputMethodManager内存泄露

这是一个比较偏的东西,也只会在部分国产rom,如小米、华为的Android6、7机型上多见。通过LeakCanery的检测,发现有时候一些机型在退出了界面后,界面的TextView以及其继承的子类可能会被下图中mNextServerdView持有。 对于这种情况的内存泄露,就要视情况来看解决方案了,如果仅仅是泄露一个TextView,InputMethodManager是单例的,就这一个对象也无关紧要。但如果通过这个View间接的持有了其他一些比较重的对象,如多媒体、Activity等,那这个时候就需要考虑以下两个方式解决这个 问题:

  1. (不推荐) 在生命周期该结束时通过反射把mNextServedView设置为null。
  2. 在生命周期结束时切断这些对象和View的关系(指被赋值到mNextServedView的View)。

总结

其实从上面来看,Android中的内存泄露简单用一句话概括就是:

生命周期已经结束的对象仍然被某个存活的对象引用。

而解决这些内存泄露的方法本质上就是想办法切断这个已经结束了生命周期的对象的被引用的关系。

从个人角度来讲,平常写代码时,如果要把当前对象给发布出去(设置为其他对象Listener/Callback、或者以其他方式被引用)或者在内部写一些匿名内部类或者非静态内部类是,都能好好思考当前对象的生命周期与发布的目的对象的生命周期的话,很多内存泄露都可以直接避免。另外,对于存量代码,可以使用一些工具,如LeakCanary去揪出项目中存在的内存泄露。

文章分类
Android