Android开发

Android Profiler(二)Memory Profil

2020-04-28  本文已影响0人  Parallel_Lines

总述

上节 Android Profiler(一)CPU Profiler

本文基于 Android Studio 3.6.3,Android 9.0。

Memory Profiler 可帮助识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。

文本旨在 Memory Profiler 快速实战入门,详细文档参见官网。

基础说明

打开 Memory Profiler

1.点击工具栏中的 Profiler 图标。

2.点击内存轴上的任意位置以打开 Memory Profiler。

基础图示说明

基础图示.png

图中,曲线代表了不同对象占用的内存:

Java:从 Java 代码分配的对象的内存;
Native:从 C 或 C++ 代码分配的对象的内存;

Android 原生框架中内部使用了 C++,所以即使你的代码是纯 Java 创建对象,也可能是 Native 这一栏内存升高。总之,不用太纠结是哪一种内存增高。

Stack:原生堆栈和 Java 堆栈使用的内存。
Allocated:当前 Java 对象数,不包含 C/C++ 对象。

注意:
1.虚线即 Allocated 的对象数。
2.Allocation Tracking 一栏,务必选 Full。默认是 Simple,它会对监测做优化,导致虚线不显示、划定区域内的对象数并非真实对象数等等,进而影响内存泄漏检测。
3.由于 GC 回收机制 不是 对象一旦弃用就立即回收,因此你可能会看到虚线条一直增加。为了方便测试,需要即时点击垃圾回收按钮(图中标注1)。

实战

目标:使用 Memory Profiler 检测内存泄漏。

例子一 基础操作

说明

首先通过实战说明如何查看当前对象数以及对象数的创建、销毁。

下面给出了简单的例子,包名为 com.dixon.profiledemo

public class SecondActivity extends AppCompatActivity {

    private Card[] array = new Card[10000];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        for (int i = 0; i < array.length; i++) {
            array[i] = new Card(i);
        }
    }
}

每次启动 SecondActivity,就创建 10000 个 Card 对象,其中 Card.class 代码如下:

public class Card {

    private int num;

    public Card(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Card{" +
                "num=" + num +
                '}';
    }
}

再次说明这个例子是没有内存泄漏的,只为了说明如何查看对象数。

操作步骤

1.点击启动 SecondActivity。

2.在基础图示中,划定你要检测的范围。

如何划定参考官网视频,很简单。
https://storage.googleapis.com/androiddevelopers/videos/studio/memory-profiler-allocations-jvmti.mp4

这里我们划定的范围就是 SecondActivity 启动前和启动完毕的区间。划定完毕后,出现下面的界面:

划定范围

其中图中标注 1 就是我划定的范围。

图示说明

图中标注 1:划定的范围,即 SecondActivity 启动前到启动完毕的一段时间。
图中标注 2:筛选漏斗,可以使用它搜索我们应用自己类,以排除系统类的干扰。
图中标注 3:Class Name 这一栏列出了当前时间范围内的对象情况。其中:

Allocations:划定 范围内创建 的对象数。这里我们创建了 10000 个 Card 实例,所以 Lcom/dixon/profiledemo/Card 一栏显示 1000。

Deadllocation:划定 范围内销毁 的对象数。这里我们还没有退出 SecondActivity,所以 Lcom/dixon/profiledemo/Card 一栏显示 0。

Total Count:当前此对象类型的总对象数。

Shallow Size:当前此对象类型总共占用的内存,单位为字节。

通过图示,我们了解到,SecondActivity 内部确实创建了 10000 个 Card 对象。

再次操作

1.退出 SecondActivity

2.点击垃圾桶按钮,强制执行垃圾回收。

前面说过,由于 GC 策略,退出 SecondActivity 后不会立即进行垃圾回收,所以需要手动强制执行以方便我们测试。

另外,为了保险起见,垃圾桶按钮至少应该 点击俩次以上,以保证完整回收。

点击垃圾桶按钮后,如果白色虚线保持高度不变,说明至少此刻没有垃圾可回收了。

3.划定检测范围。这里我们要划定 SecondActivity 退出前到垃圾回收彻底完毕这段区间。此时图示如下。

图示

可以看到,此段时间范围内,创建对象数为 0,销毁了 10000 个 Card 对象,以及 1 个 SecondActivity 对象。

多出的一个 Card 对象这里不用关心,是作者的例子里多写了个 Card 而已,懒得删了,直接忽略就好...

例子二 累积性内存泄漏分析

说明

关于累积性内存泄漏,这里给出示例代码:

public class MemoryLeak {

    private static final List<Context> sList = new ArrayList<>();

    public static void putContext(Context context) {
        sList.add(context);
    }
    ...
}

public class SecondActivity extends AppCompatActivity {

    private Card card = new Card(0);
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        
        MemoryLeak.putContext(this);
    }
}

每次启动 SecondActivity,都会将一个新的实例添加到 MemoryLeak 的静态集合中,也就是说启动多少个 SecondActivity,就会有多少个 SecondActivity 不会被回收,该内存泄漏会不断累加,直至 OOM,是最危险的内存泄漏方式。

操作步骤

1.反复多次启动、退出 SecondActivity
2.点击垃圾桶按钮,强制执行多次垃圾回收;
3.划定检测范围。本例中范围应该从 SecondActivity 第一次启动前到垃圾最后一次回收结束。
4.筛选。因为我要检测应用自身的内存泄漏,所以直接筛选自己的包名。

这里我进行了 5 次反复启动,3 次垃圾回收,此时划定范围结果如下:

累积性内存泄漏

分析

通过图表可以看出,在此期间内,Lcom/dixon/profiledemo/SecondActivity 一共创建了 5 个实例,并且回收次数为 0。说明 SecondActivity 确实存在严重的内存泄漏。

例子三 覆盖性内存泄漏分析

照旧先上例子:

public class MemoryLeak {

    private static Context sContext;

    public static void setContext(Context context) {
        sContext = context;
    }
}

public class SecondActivity extends AppCompatActivity {

    private Card card = new Card(0);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        MemoryLeak.setContext(this);
}

每次启动 SecondActivity,都会有新的 SecondActivity 实例覆盖旧的存在内存泄漏的 SecondActivity 实例,即每次总有一个 SecondActivity 实例常驻内存。

操作步骤

操作步骤与前例完全相同。

这里我进行了 5 次反复启动,3 次垃圾回收,此时划定范围结果如下:

覆盖性内存泄漏

分析

通过图表可知,回收结束后,Deallocation 的数量总比 Allocations 小 1,即总有一个 SecondActivity 实例处于无法被回收的状态,因此可以得出 SecondActivity 存在内存泄漏。

例子四 短期性内存泄漏

说明

照旧,先上代码:

public class SecondActivity extends AppCompatActivity {

    private Card card = new Card(0);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

如果子线程还没运行完毕,SecondActivity 就退出了,由于匿名内部类持有外部类的实例,所以会导致 SecondActivity 在休眠期间的 10s 内(实际是子线程销毁前)无法被回收,导致短期性内存泄漏。

操作

操作步骤同上。

这里我进行了 3 次反复启动,3 次垃圾回收,此时划定范围结果如下:

短期性内存泄漏

分析

从图表可以看出,这里创建了 3 个 SecondActivity 实例,但是内存回收结束后却没有销毁,确实存在内存泄漏。

但是如果 10s 后我们再次执行强制垃圾回收,这时的情况又会如何?

10s后再次强制回收

可见 10s 后这部分泄漏的内存确实可以回收了。

实际开发中,由于网络调用,这种场景可能会很常见,但是并非 10s 这么理想,很可能只有短短几秒,如果是频繁调度的重型页面,又恰好用户手机性能不佳,短期性的内存泄漏就可能导致你的应用崩溃。

而且这种问题很难排查,因为有可能在我们点击强制回收按钮时,线程已经执行完毕,外部类 Activity 已处于可回收状态,此时 Memory Profiler 将很难察觉。

好在还有另一款利器:LeakCanary,关于它不是本文重点,后续会单独说明。

总结

如果对开发者来说, CPU Profiler 用于分析应用卡顿,那么 Memory Profiler 则主要用于排查内存泄漏。

关于 Memory Profiler 还有更多深入的使用方式,详情参考 官方文档,本文只说明 Memory Profiler 的基础用法。

[TOC]

上一篇下一篇

猜你喜欢

热点阅读