如何通过Profile分析看内存泄漏

1,959 阅读3分钟

Profile

Profile是AS自带的功能,可分析CPU、内存、Energy,我只用过分析内存。Profile跟AS版本有关,不同版本小有差异,我这里用的是Android Studio Dolphin | 2021.3.1 Patch 1。

image.png

(图一)

如上图一红框,依次点击“+”,选择设备,选择进程,就可以看到该进程的CPU、Memory、ENERGY信息,点击Memory,看该应用的内存使用情况。

image.png

(图二)

点击Memory后,即进入上图二,有Memory的标识,右边记录的某时刻内存占用情况。操作应用可以看到线条的起伏,这代表这内存的变化。如图中上升部分是我打开一个页面,下降部分是我关闭页面。上图中还有一个垃圾桶图标,因为java的内存回收不是实时触发的,我不知道什么时候触发,但内存紧张时更容易触发。这里的垃圾桶标识是手动触发,点击后就算内存不紧张,也必定触发内存回收机制。

Profile Memory可以用来查看内存泄漏情况。当我们打开某页面,或者使用某功能时,会申请新的内存,对应的就是线条的上升,但是当我们退出该页面或者功能时,线条没有下降到原来的水平(或者跟原来接近的水平),就算手动触发内存回收也没有,这就代表已经发生了内存泄漏。如果我们重复打开该页面或功能,每次打开都申请新内存,线条上升,那没得说,这是严重的内存泄漏了,虽然我是初级段位,但我也知道这种情况瞒不掉的,不要指望测试返回发现,改进解决掉。

在一次dump后,profile会停止更新内存占用情况,表现为内存线条不在动了,点击图二右上角的图标,让Profile继续同步实时内存情况。

如何dump当前内存

选择上图二左边红框的‘Capture heap dump’,点击‘Record’,等一会儿dump成功,就会显示具体的内存使用情况。

image.png

(图三)

如图三展示的一次dump成功的内存,是按照类型分配的。我们也可以根据类型搜索,排除我们不想看的数据。

用Profile抓内存溢出

在SecondActivity,申请了大内存,用上面的方法dump内存看。如下图四,

ArrayList<Object> list2 = new ArrayList<>();
onCreate(){
    File f2 = new File(Environment.getExternalStorageDirectory() + "/Pictures/bing/a1p.png");
    Bitmap bmp2 = BitmapFactory.decodeFile(f2.getAbsolutePath());
    list2.add(bmp2);
}

image.png

(图四)

上图4中的bitmap,根据宽、高,就是我申请的内存。

image.png

(图五)

图五延时的是,根据搜索关键字过滤的内存对象,可以看到第二个Activity:SecondActivity在内存中的信息。注意SecondActivity$2等,这是SencondActivity的匿名内部类对象。

之后退出SecondActivity,手动触发垃圾回收(点击‘垃圾桶’图标),之后重写dump内存,然后新的内存信息中就看不到SecondActivity了,对应的图四中的bitmap的数组中也找不到我们申请的那个图片的bitmap了。

如何制造一个内存溢出

在SecondActivity中发送一个延时消息,延时久一点,注意要在SecondActivity中重新实现Handler的handlerMessage().

new Handler() {
   @Override
   public void handleMessage(@NonNull Message msg) {
      Log.i("kobe", "receive msg");
   }
}.sendEmptyMessageDelayed(0, 100000);

加入这段代码后,重复上面的步骤,手动回收内存,可以看到线条不会明显下降,dump内存发现,SecondActivity对象依然在内存中,bitmap也依旧在内存中。此时尽管SecondActivity依旧关闭,但SecondActivity对象和他持有的对象都无法回收,这就是发生了内存溢出。PS:上面延时了100秒,100秒后,再次触发内存回收,就能成功回收内存了。dump内存,将看不到SecondActivity对象和bitmap了。

image.png

(图六)

上图六就是发送延时消息后,关闭SecondActivity后,手动回收内存后,dump的应用内存,可以看到SecondActivity对象是在内存中的。我看们SecondActivity被持有的情况。MessageQueue持有Message,Message持有Handler,这里target=SecondActivity$1(原来我截图SecondActivity2$1,我写了两个SecondActivity页面,但我实在不想重新截图了)就是SecondActivity中的匿名内部类对象Handler,这个Handler是持有外部类SecondActivity的应用的,这就导致SecondActivity无法被内存回收。

一般答这个问题时,都是说非静态内部类隐式持有外部类对象。看这段代码,

public class Test {

    int i = 0;
    Runnable r = new Runnable() {
        @Override
        public void run() {
            i++;
        }
    };

    static class StaticInner {
        void aa() {
        }
    }

匿名内部类Runnable是不持有外部类对象的,但真的不持有吗,我们编译(javac xxx.java)成class文件看,如下,外部类Test是传入到匿名内部类的构造方法的,即是被持有的,这种就是隐式持有。

class Test$1 implements Runnable {
    Test$1(Test var1) {
        this.this$0 = var1;
    }

    public void run() {
    }
}

我们再看另一个静态内部类,这种是不持有外部类对象。

class Test$StaticInner {
    Test$StaticInner() {
    }
}

上面的Handler是匿名内部类,是内部类的一种。Handler被Message持有,Message又被MessageQueue持有,这个MessageQueue是主线程的,肯定是无法被回收的;向下,Handler又隐式持有了外部类Activity,这就导致Activity的内存泄漏。

补充一下内部类的知识

内部类包括成员内部类,局部内部类,匿名内部类,静态内部类,前三者是非静态内部类。形式分别如下。

public class Test {

    int i = 0;
    /* 匿名内部类 */
    Runnable r = new Runnable() {
        @Override
        public void run() {
            i++;
        }
    };

    /* 静态内部类 */
    static class StaticInner {
        void aa() {
        }
    }

    /* 成员内部类 */
    class Inner {
        void aa() {
            i++;
        }

    }

    public void print() {
        /* 局部内部类 */
        class LocalInner {
            void aa() {
                i++;
            }
        }
        /* 匿名内部类 */
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
            }
        };
    }

}