Android 内存泄露简介典型情景及检测解决

Posted 一口仨馍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 内存泄露简介典型情景及检测解决相关的知识,希望对你有一定的参考价值。

本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。

什么是内存泄露?

android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,导致不能及时回收这个对象所占用的内存。内存泄露积累超过Dalvik堆大小,就会发生OOM(OutOfMemory)。

内存泄露的经典场景

非静态内部类的静态实例

由于内部类默认持有外部类的引用,而静态实例属于类。所以,当外部类被销毁时,内部类仍然持有外部类的引用,致使外部类无法被GC回收。因此造成内存泄露。

举个栗子

    private static Leak mLeak;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mLeak = new Leak();
    

    class Leak 
    

错误栗子说明:static关键字修饰mLeak属性,将mLeak存在静态区中,而Leak为内部类,默认持有外部类的引用。当Activity销毁时,mLeak紧紧抱住Activity的大腿深情告白:“MLGB!劳资就是不放你走!”。斗不过mLeak属性的GC,自然不敢回收二手娘们Activity。因此造成内存泄露。

不正确的Handler

错误代码示例:

    private MyHandler mMyHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mMyHandler = new MyHandler();
        mMyHandler.sendMessageDelayed(new Message(), 10 * 1000);
    

    class MyHandler extends Handler 
        @Override
        public void handleMessage(Message msg) 
            super.handleMessage(msg);
        
    

正确写法如下:

    private MyHandler mMyHandler;
    static class MyHandler extends Handler 
        WeakReference<Activity> mActivityWeak;

        MyHandler(Activity act) 
            mActivityWeak = new WeakReference<Activity>(act);
        

        @Override
        public void handleMessage(Message msg) 
            super.handleMessage(msg);
            if (mActivityWeak.get() != null) 
                // doSomething
            
        
    

我们知道在handler.sendMessage(msg)时,msg.target会指向handlermsg会插入MessageQueue。此为下面讲解的基础,对这部分不太熟悉的同学可以参考这篇博客。

错误之处

MyHandler为内部类,默认持有外部类的引用。当Activity销毁时,如果MessageQueue中仍有未处理的消息,那么mMyHandler示例将继续存在。而mMyHandler持有Activity的引用。故Activity无法被GC回收。

正确解析

static关键字修饰MyHandler类,使MyHandler不持有外部类的引用。使用WeakReference<activity>保证当
activity销毁后,不耽误gc回收activity占用的内存空间,同时在没被销毁前,可以引用activity

管它正确错误都让它正确

通过上面的分析,可以得出结论:Handler造成内存泄露时,是因为MessageQueue中还有待处理的Message,那我们在Activity#onDestroy()中移除所有的消息不完事了嘛。反正Activity都销毁了,MessageQueue中的msg也就什么存在的意义了,可以移除。代码如下:

    @Override
    protected void onDestroy() 
        super.onDestroy();
        // 移除所有的callback和msg
        mMyHandler.removeCallbacksAndMessages(null);
    

静态变量引起内存泄露

这里以单例模式引起Context泄露为例

public class Singleton 
    private static Singleton instance;
    private Singleton(Context context)
    

    public static Singleton getInstance(Context context)
        if (instance == null)
            synchronized (Singleton.class)
                if (instance == null)
                    instance = new Singleton(context);
                
            
        
        return instance;
    

错误之处

在调用Singleton#getInstance()方法如果传入了Activity。如果instance没有释放,那么这个Activity将一直存在。因此造成内存泄露。

修正版

new Singleton(context)改为new Singleton(context.getApplicationContext())即可,这样便和传入的Activity没撒关系了。该释放释放、该回家回家。

碎碎念

  • 当使用CursorFileSocket等资源时往往都使用了缓冲。在不需要的时候应该及时关闭它们,收回所占的内存空间。
  • Bitmap不用就recycle掉。注意调用recycle后并不意味着立马recycle,只是告诉虚拟机:小子,该干活咯!
  • ListView一定要使用ConvertViewViewHolder
  • BraodcastReceiver注册完事,不用时也要反注册

内存泄露的检测

Heap工具

  1. 打开DDMS视图
  2. 选中Devices下某个具体的应用程序
  3. 选中Devices下第二个小绿点Update Heap
  4. 不断运行程序并点击Cause GC
  5. 关注data Object行、Toal Size列
  6. 耍你的APP去吧,如果发现Toal Size越来越大,很可能有内存泄露的发生

MAT(Memory Analyzer Tool)工具

导出.hprof文件

  1. 打开DDMS视图
  2. 选中Devices下某个具体的应用程序
  3. 选中Devices下第二个小绿点Update Heap
  4. 点击Cause GC
  5. 点击Dump HPROF file
  6. 切换到MAT页卡,默认如下图所示

最显眼的就是饼图了,里面列出了每种类型的数据所占大小。和红色箭头所指的Dominator有的一拼,然而这并没有什么卵用。我们的重点在Histogram。没撒说的,点击它。默认图如下

默认是按Class排序,第一行支持正则表达式。为了查看方便,下面我们会以Group by package的形式分组。正确的打开方式应该是这个样子的。

内存泄露Demo

这里以非静态内部类的静态实例为例,Demo只有两个Activity,MainActivity中只有一个按钮,点击跳转到SecondActivity

public class SecondActivity extends Activity 

    private static Leak mLeak;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mLeak = new Leak();
    

    class Leak 

    

查找内存泄露

启动APP,点击进入SecondActivity,然后按back键返回到MainActivity。打开.hprof文件。查找我们的包名com.dyk.memoryleak

可以看到,虽然我们结束了SecondActivity,但是SecondActivity仍然存在,内存泄露无疑。

1.右键SecondActivity,选择List Objects—->with incoming references

结果如下图:

2.右键com.dyk.memoryleak.SecondActivity,选择Path to GC—->with all references

结果如下图:

可以看到是因为mLeak属性的引用导致SecondActivity无法回收。既然找到了内存泄露的原因,通过上文的介绍,相信改起来难度应该不是很大的。

3.再次进入SecondActivity。由于上次创建的SecondActivity还没有被回收,可以预期到此时应该存在两个SecondActivity实例。

关于内存泄露的内容暂时到此为止。MAT更多的功能,请自行查找学习。感谢耐心阅读到最后~

以上是关于Android 内存泄露简介典型情景及检测解决的主要内容,如果未能解决你的问题,请参考以下文章

Windows中内存泄漏检测工具vld简介及使用

Android工具:LeakCanary—内存泄露检测神器

Android Studio检测内存泄露和性能

Android内存优化1 内存检测工具1 Memory Monitor检测内存泄露

Android性能优化第篇---Memory Monitor检测内存泄露

Android性能优化:阿里腾讯等关于内存泄露的知识都在这里了!