Android 内存泄露分析

来源:互联网 发布:淘宝客服该怎么做 编辑:程序博客网 时间:2024/05/18 17:45

编程工具:Android Studio
分析内存工具:Android Studio和MAT

内存泄露的例子:

// MainActivity类public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }    public void openNextActivity(View view) {        // 跳转到内存泄露的Activity        startActivity(new Intent(this, SecondActivity.class));    }}// SecondActivity类 : 存在内存泄露public class SecondActivity extends AppCompatActivity {    private List<Student> dataList = new ArrayList<>();    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_second);        // 模拟储存10000个数据,这样更分析更明显        for (int i = 0; i < 10000; i++) {            dataList.add(new Student("test" + i, i, "school" + i));        }        new Thread(runnable).start();    }    public static class Student {        public String name;        public int age;        public String school;        public Student(String name, int age, String school) {            this.name = name;            this.age = age;            this.school = school;        }    }    // 内存类,依赖外部类,持有外部类SecondActivity实例    private Runnable runnable = new Runnable() {        @Override        public void run() {            // 模拟延时            try {                Thread.sleep(100 * 60 * 60 * 1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    };}

初步分析

我们可以利用Android Studio的【Monitors窗口】查看应用内存情况:

用于对比,先把存在内存泄露的Activity恢复正常:

// SecondActivity类 : 不存在内存泄露问题public class SecondActivity extends AppCompatActivity {    private List<Student> dataList = new ArrayList<>();    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_second);        // 模拟储存10000个数据,这样更分析更明显        for (int i = 0; i < 10000; i++) {            dataList.add(new Student("test" + i, i, "school" + i));        }    }    public static class Student {        public String name;        public int age;        public String school;        public Student(String name, int age, String school) {            this.name = name;            this.age = age;            this.school = school;        }    }}

然后反复启动10-20次SecondActivity,内存使用情况是这样的:

从结果可以看到,几次GC之后,内存可以正常被回收。

我们来看看SecondActivity存在内存泄露的情况:

对比上一个图,几次GC之后,程序占用的内存一直上升,说明本该被回收的内存一直没有被释放。初步分析,存在内存泄露问题。

接下来我详细分析哪里存在内存泄露。

进一步分析

Android Studio自带的界面,查看内存泄露还不是很智能,我们可以借助第三方工具。
常见的工具就是MAT了,下载地址 http://eclipse.org/mat/downloads.php ,这里我们需要下载独立版的MAT。

生成hprof文件

hprof文件可以让Android Studio帮我们生成,具体做法是:

启动程序,MainActivity中点击按钮跳转到SecondActivity(初步分析,存在内存泄露),返回点击返回键,然后点击按钮跳转到SecondActivity,再点击返回,反复几次,然后点击【Monitors窗口】下【Momery】右边的【Dump Java Heap】按钮

此时hprof文件已经生成了,在【Captrues窗口】可以找到:

但是此时的hprof文件还不能被MAT识别,我们右击我们需要的hprof文件,导出标准的hprof文件:

分析hprof文件

我们用MAT分析内存工具打开导出的hprof文件:

打开之后,点击工具栏中的直方图图标查看内存详细情况(就是哪些类使用了多少内存):

打开的界面会显示所有的内存使用情况,所以需要我们过滤一下(一般关键字为包名):

过滤之后显示的界面是这样的:

我们从这里可以看到,因为SecondActivity被我启动了7次,产生了7个SecondActivity实例,而且都没有回收,因为每个SecondActivity实例都保存了10000个Student实例,所以Student实例有70000个,我们查查SecondActivity为什么没有被回收。

右击【com.johan.demo.SecondActivity】,选择【Merge Shortest Paths to GC Roots】,然后选择【with all reference】:

然后展开一个查看:

发现SecondActivity实例被Thread匿名内部类的this$0引用,Thread还存活,所以持有的SecondActivity实例也认为是“存活”的,所以GC不能回收,造成内存泄露。

这就找到了内存泄露的原因!!

SecondActivity内存泄露的原因是静态内部类持有外部类导致的,我们这么改造一下:

public class SecondActivity extends AppCompatActivity {    private List<Student> dataList = new ArrayList<>();    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_second);        for (int i = 0; i < 10000; i++) {            dataList.add(new Student("test" + i, i, "school" + i));        }        new Thread(new MyRunnable()).start();    }    public static class Student {        public String name;        public int age;        public String school;        public Student(String name, int age, String school) {            this.name = name;            this.age = age;            this.school = school;        }    }    // 声明为静态内部类    public static class MyRunnable implements Runnable {        @Override        public void run() {            // 模拟延时            try {                Thread.sleep(100 * 60 * 60 * 1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}

我们重新导出hprof文件,查看内存详细情况:

如图,MyRunnable对象有7个,证明SecondActivity已经启动了7次,而SecondActivity只剩下一个(应该是还没有来得回收),所以SecondActivity实例可以被GC正常回收了,内存泄露问题得以解决!!其实这里还有泄露,就是MyRunnable,我们关闭SecondActivity的时候,应该中断线程。不过还是建议使用线程池,靠谱!

LeakCanary

目前除了导出hprof文件分析内存泄露,还有一种更简单的办法,就是在程序代码中集成LeakCanary。

项目地址:https://github.com/square/leakcanary

有兴趣的自己试一下!!!