Android常见内存泄漏案例解剖

作者: 唠嗑008 | 来源:发表于2020-01-15 15:22 被阅读0次

    最近一直在学习JVM内存分配回收相关的知识,看了那么多东西,终归还是要回到项目,回到代码中来。

    今天就和大家聊一下内存泄漏的问题,我相信进入中高级开发的同学对这个问题都不陌生,甚至很多人觉得这个问题so easy,不就是常见的那几个吗,有什么高深的东西呢?这里先不争论。

    你是如何定义内存泄漏的?

    观点1:

    长生命周期的对象持有短生命周期对象的强引用,在短生命周期对象需要回收的时候发现不能被回收,视为泄漏。

    观点2:

    当不再使用的对象被其它对象所引用,导致其无法被gc回收,就会内存泄漏。

    这是目前我看过的比较主流的2种观点,也暂时不论对错,先看一下百科的定义:

    内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    百科的观点,我基本认同,泄漏的本质却是是程序运行时分配出去的内存没有及时回收,导致内存空间越来越小,直到最后OOM。注意:我这里说的是基本认同,实际上不仅仅是堆内存会泄漏,实际上虚拟机栈、本地方法栈、方法区也都有可能会内存泄漏和oom,只是平时在app开发中内存泄漏和GC回收的主要区域是堆内存,它也是运行时内存比较大的一块内存。

    上面说了,内存没有及时回收才导致的泄漏,那为什么会导致创建对象时分配的内存回收不了呢?
    这主要和GC垃圾回收机制有关,其中关键的流程有2步:1、标记哪些对象是可回收的(可达性算法GcRoot);2、回收内存空间。

    可达性分析算法GcRoot大致意思如下:

    先找到一系列“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,说明这个对象是可回收的。

    关于GcRoot大家可以先找资料学习一下。

    下面说一下常见的案例分析:
    今天先说一下最典型的2个,内部类/匿名内部类、Handler的内存泄漏,其它的后面有时间再补充。

    内部类/匿名内部类

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new InnerClass()).start();
        }
    
         class InnerClass implements Runnable {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    

    这段代码,当Activity退出时,如果Thread线程还有正在进行的任务就会发生内存泄漏。下面先来分析一下为什么会泄漏?

    很多人都听说过非静态内部类/匿名内部类会持有外部类的引用。可你知道是为什么吗?其实也不复杂,如果你创建一个包含内部类/匿名内部类的java文件,然后通过javac编译成class文件,你会发现,这些内部类/匿名内部类会被编译成一个单独的class文件,并且它的构造方法中会传入外部类对象。

    上面的MainActivity就会被编译成:

    #InnerClass
    class MainActivity$InnerClass  implements Runnable {
      /* synthetic */ MainActivity this$0;
    
        MainActivity$InnerClass(MainActivity mainActivity) {
            this.this$0 = mainActivity;
        }
    
        public void run() {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new MainActivity$InnerClass()).start();
        }
    
    }
    

    这其实是编译器的语法糖,匿名内部类也是差不多的,你可以自己尝试。

    回到刚才的问题,为什么MainActivity会泄漏呢?因为Activity退出的时候,InnerClass还没有被回收,就导致Activity也不能回收,所以就泄漏了。

    那为什么Activity退出了,InnerClass还不能被回收呢?
    看一下创建/执行Thread的地方。JVM中,Thread是直接被GC Root所引用在JVM中,所以运行的线程是不会被回收的。在Activity退出之后,Thread对象是不会被回收的,它持有的这个InnerClass就不会被回收,而InnerClass又持有MainActivity引用,所以就泄漏了。

    GcRoot引用链如下:


    GcRoot引用链

    解决方案
    既然现在已经知道内部类引发的内存泄漏是由于Activity对象存在一条可到达GcRoot的引用链造成的,那只要把引用关系1或者2断开就可以解决问题了。

    方案1:
    在退出Activity之前关闭线程,注意:用interruptstop更加可靠。这样的话,当Activity退出时,由于Thread处于非活跃状态,Thread和Activity都可以回收,这是针对引用1的解决方案。

    @Override
        protected void onDestroy() {
            super.onDestroy();
            thread.interrupt();
        }
    

    方案2:
    把InnerClass这个内部类变成静态内部类,这样的话就不再持有外部类Activity的引用,这是针对引用2的解决方案。

    Handler泄漏

    public class HandlerActivity extends AppCompatActivity {
    
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_handler);
    
              handler.sendEmptyMessageDelayed(0,10000);
        }
    
    }
    

    这段代码,大家也很熟悉,通过匿名内部类的方式创建Handler,如果退出Activity的时候,消息队列中还有未处理的消息,那么Handler和Activity就会发生内存泄漏。

    泄漏剖析:
    这里通过匿名内部类的方式创建的Handler,从案例1的解析可以知道,匿名内部类也是隐式持有外部类的引用,所以Activity没办法被回收是因为Handler没有被回收,handler又持有Activity的引用。那为什么Handler不被回收呢?

    发送消息时,是通过sendEmptyMessageDelayed去完成的,通过追踪这个方法内部的源码可以知道,最后会执行到如下方法

    #Handler.java
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            msg.target = this; //1
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    在代码1处的target实际上是Message类中的成员变量,它的类型是Handler,这里的target实际上就是当前Handler对象。这里可以看出Message对象持有了Handler对象的引用。如果你看过Handler源码的话,你会知道,Handler是在主线程创建了一个Looper对象,循环的从消息队列中中取消息/处理消息,如果你退出页面的时候,在消息队列中还有Message存在,那这个Message是不会被释放的,所以你的Handler对象就不会被释放,那Activity也无法释放。这里可以简单看下GcRoot引用链:

    Handler GcRoot引用链

    注意:这里的引用链我只画到了Message持有Handler的引用,实际上引用树还包含更复杂的引用关系,Message---》MessageQueue---》Looper---》ActivityThread,我们知道这个主线程的Thread和Looper是一直存活的,所以当退出Activity时有msg的话也会存在知道它处理完被清理。

    关于Handler更详细的内容,可以参考:
    解读在Activity中使用Handler的内存泄漏问题
    面试之为什么Handler会存在内存泄露

    分析到这里,要想解决问题就很容易了,要想不泄漏,只需要在退出时断开Message--->Handler的引用即可。

    要想解决这个问题,需要看一下Handler源码,在Handler源码中,可以看到Looper的loop()方法是循环处理消息的,在消息处理完之后,会调用如下方法来回收

     #Looper.loop
     msg.recycleUnchecked();
    
       #Message
        void recycleUnchecked() {
            // Mark the message as in use while it remains in the recycled object pool.
            // Clear out all other details.
            flags = FLAG_IN_USE;
            what = 0;
            arg1 = 0;
            arg2 = 0;
            obj = null;
            replyTo = null;
            sendingUid = -1;
            when = 0;
            target = null; //handler置空
            callback = null;
            data = null;
    }
    

    这里通过target = null;让Message和Handler的引用断开,这样的话Activity和Handler就可以回收了。

    解决方案

    方案1:
    通过handler的removeCallbacksAndMessages()方法可以清空消息队列,它里面会调用刚才说的recycleUnchecked()方法,使得Message和Handler断开引用。当然也可以选择移除指定消息,一样的。

     @Override
        protected void onDestroy() {
            super.onDestroy();
            //移除消息队列
            handler.removeCallbacksAndMessages(null);
        }
    

    方案2:
    就是网上说的最多的那种,写个静态内部类来继承Handler,然后通过弱引用来访问Activity的内容。这种就不写了。

    小结

    内存泄漏问题的解决,最本质的内容还是GcRoot引用链,只要知道了引用关系,你就知道为什么有些对象暂时无法回收。

    其它案例,后面在补充吧。大家可以留言说说你的问题。

    参考:
    https://www.cnblogs.com/ldq2016/p/8473376.html
    https://www.jianshu.com/p/3e59d129c05d

    相关文章

      网友评论

        本文标题:Android常见内存泄漏案例解剖

        本文链接:https://www.haomeiwen.com/subject/thxaactx.html