Andorid性能优化(三) 之 如何定位内存泄漏

1 定位内存泄漏工具

正所谓工欲善其事,必先利其器。定位内存泄漏,可以借助目前比较流行的一些工具来帮助发现和定位问题,下面我们就来看看这些工具。

1.1 Memory Profiler

Android Studio 3.0 采用全新的Android Profiler窗口取代 Android Monitor 工具。 这些全新的分析工具能够提供关于应用 CPU、内存和网络 Activity 的实时数据。你可以执行基于样本的函数跟踪来记录代码执行时间、采集堆转储数据、查看内存分配,以及查看网络传输文件的详情。Memory Profiler 是 Android Profiler 中的一个组件,可帮助你识别导致应用卡顿、冻结甚至崩溃的内存泄漏和流失。 它显示一个应用内存使用量的实时图表,让你可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。要打开 Memory Profiler,可以点击 View > Tool Windows > Android Profiler(也可以点击工具栏中的 Android Profiler https://img-blog.csdnimg.cn/2018122518253191)。打开Android Profiler,如下图。再从工具栏中选择你想要分析的设置和应用程进程,然后点击MEMORY时间线中的任意位置即可打开Memory Profiler。

Andorid性能优化(三) 之 如何定位内存泄漏_第1张图片  

1.1.1 Memory Profiler 概览

当你首次打开 Memory Profiler 时,你将看到一条表示应用内存使用量的详细时间线,并可访问用于强制执行垃圾回收、捕捉堆转储和记录内存分配的各种工具。

Andorid性能优化(三) 之 如何定位内存泄漏_第2张图片

 如图所示,Memory Profiler 的默认视图包括以下各项:

1.用于强制执行垃圾回收 Event 的按钮。

2.用于捕获堆转储的按钮。

3.用于记录内存分配情况的按钮(此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示)。

4.用于放大/缩小时间线的按钮。

5.用于跳转至实时内存数据的按钮。

6.Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。

7.内存使用量时间线,其包含以下内容:

        a.一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。

        b.虚线表示分配的对象数,如右侧的 y 轴所示。

        c.用于表示每个垃圾回收 Event 的图标。

8.展示应用根据Android系统机制所提交的所有私有内存页面数。 此计数不包含与系统或其他应用共享的页面。

        Java:从 Java 或 Kotlin 代码分配的对象内存。

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

        Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)

        Stack: 应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与应用运行多少线程有关。

        Code:应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。

        Other:应用使用的系统不确定如何分类的内存。

        Allocated:应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。当连接至运行 Android 7.1 及更低版本的设备时,此分配仅在 Memory Profiler 连接至你运行的应用时才开始计数。 因此,你开始分析之前分配的任何对象都不会被计入。 不过,Android 8.0 附带一个设备内置分析工具,该工具可记录所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示你的应用中待处理的 Java 对象总数。

1.1.2 查看内存分配

内存分配显示内存中每个对象是如何分配的。 具体而言,Memory Profiler 可为你显示有关对象分配的以下信息:

  1. 分配哪些类型的对象以及它们使用多少空间。
  2. 每个分配的堆叠追踪,包括在哪个线程中。
  3. 对象在何时被取消分配(仅当使用运行 Android 8.0 或更高版本的设备时)。

如果你的设备运行 Android 8.0 或更高版本,你只需点击并按住时间线,并拖动选择你想要查看分配的区域,操作后如下图。 不需要开始记录会话,因为 Android 8.0 及更高版本附带设备内置分析工具,可持续跟踪你的应用分配。

Andorid性能优化(三) 之 如何定位内存泄漏_第3张图片

如果你的设备运行 Android 7.1 或更低版本,则在 Memory Profiler 工具栏中点击 Record memory allocations 。 记录时,Android Monitor 将跟踪你的应用中进行的所有分配。 操作完成后,点击 Stop recording ,以查看分配。操作如图中红圈位置按钮。

Andorid性能优化(三) 之 如何定位内存泄漏_第4张图片

完成上面的区域跟踪操作后,便可以继续来检查内存分配情况了。请看下方出现分配对象的列表,列表中可以按下每个列的表头进行排序,根据你的实际查找需求排序即可。然后点击其中一项,此时在右侧将出现 Instance View 窗格,显示该类的每个实例。在Instance View 窗格中,点击一个实例。 此时下方将出现 Call Stack 标签,显示该实例被分配到何处以及哪个线程中。在 Call Stack 标签中,点击任意行以在编辑器中跳转到该代码。如下图。

Andorid性能优化(三) 之 如何定位内存泄漏_第5张图片

1.1.3 捕获堆转储

堆转储显示在你捕获堆转储时你的应用中哪些对象正在使用内存。 特别是在长时间的用户会话后,堆转储会显示你认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。在捕获堆转储后,你可以查看以下信息:

  1. 你的应用已分配哪些类型的对象,以及每个类型分配多少。
  2. 每个对象正在使用多少内存。
  3. 在代码中的何处仍在引用每个对象。
  4. 对象所分配到的调用堆栈。 (目前,如果你在记录分配时捕获堆转储,则只有在 Android 7.1 及更低版本中,堆转储才能使用调用堆栈。)

要捕获堆转储,在 Memory Profiler 工具栏中点击 Dump Java heap https://img-blog.csdnimg.cn/20181225182531111 ,转储后如下图。

Andorid性能优化(三) 之 如何定位内存泄漏_第6张图片

在图中分配对象的列表中,我们来关注一下两个列Shallow Size和Retained Size的代表意思。

Shallow Size表示对象在没有引用其他对象的情况下本身占用的内存大小。

Retained Size是指对象自己本身的Shallow heap的大小+对象直所引用到的对象的大小。

1.1.4 将堆转储另存为hprof文件

在捕获堆转储后,仅当分析器运行时才能在 Memory Profiler 中查看数据。 当你退出分析会话时,你将丢失堆转储。 因此,如果你要保存堆转储以供日后查看,可通过右击左边SESSIONS中想要导出的Dump,然后选择Export,如下图。

Andorid性能优化(三) 之 如何定位内存泄漏_第7张图片

要使用其他hporf分析器,你需要hporf文件从 Android 格式转换为 Java SE HPROF 格式。 你可以使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv 工具执行此操作。 运行包括以下两个参数的 hprof-conv命令:原始 HPROF 文件和转换后 HPROF 文件的写入位置。 例如:

hprof-conv  xx.hprof  xx_new.hprof

关于Memory Profile的详细介绍过参考官方文档:

https://developer.android.google.cn/studio/profile/memory-profiler

1.2 Memory Analyzer Tool

Memory Analyzer Tool简称MAT,它是一款非常强大的内存泄漏分析工具,其最大的作用就是通过程序中捕捉生成的hprof文件进行分析或比较能够定位大内存对象和内存泄漏源头。在Android Studio 3.0之前,要做内存分析几乎都是使用MAT。下载地址:http://www.eclipse.org/mat/downloads.php。

1.2.1 生成hprof文件

方法1

如果你使用的Android Studio 是3.0或更新的版本,就像前面介绍Memory Profiler中,“将堆转储另存为hprof文件”即可。

方法2

如果你使用的Android Studio是3.0之前的版本,从Tools -> Android -> Android Device Monitor中打开DDMS(Dalvik Debug Monitor Service)界面,如下图,选择要进行分析的app,然后点击“Dump HPROF file”按钮,等待一小段时间即可导出一个hprof文件。

Andorid性能优化(三) 之 如何定位内存泄漏_第8张图片

同样,因为导出的hprof文件不能被MAT直接识别,你需要hporf文件从 Android 格式转换为 Java SE HPROF 格式。 你可以使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv 工具执行此操作。 运行包括以下两个参数的 hprof-conv命令:原始 HPROF 文件和转换后 HPROF 文件的写入位置。 例如:

hprof-conv  xx.hprof  xx_new.hprof

方法3

如果你使用的Android Studio是3.0之前的版本,在Android Studio窗口中的Android Monitor中选中要分析的app,然后点击“Initiate GC”,然后再点击“Dump Java Heap”按钮,如下图1,也是等待一小段时间后,即可生成一个hprof文件。如下图2,点击刚生成的hprof文件,然后选择“Exprot to standard.hprof”就可直接导出一个不需要转换的hprof文件了。

Andorid性能优化(三) 之 如何定位内存泄漏_第9张图片

Andorid性能优化(三) 之 如何定位内存泄漏_第10张图片

1.2.2 开始分析hprof文件

打开MAT,通过菜单打开刚才保存的hprof文件,如图。MAT最常用功能的有Histogram和Dominator Tree,通过Histogram可直观看出内存中不同类型的buffer的数量和占用内存大小,而Dominator Tree则把内存中的对象按照从大到小顺序进行排序,并可分析对象之间的引用关系,内存泄露分析就是通过Dominator Tree来完成。

Andorid性能优化(三) 之 如何定位内存泄漏_第11张图片

1.2.3 Dominator Tree使用说明

在Dominator Tree中内存泄露原因一般不会直接显示出来,这时需要从大到小去排查一遍。列表中出现的就是所在类的实例信息,可以选定其中一你认为可能存在问题的项,然后右击鼠标->Path To GC Roots -> exclude wakd/soft references,如图操作。Path To GC Roots过程中之所以选择排除弱引用和软引用,是因为二者都是较大几率被gc回收掉,它们并不能造成内存泄漏,排除后那么剩下的就是强引用了。

Andorid性能优化(三) 之 如何定位内存泄漏_第12张图片

图中能看到列表中有四个列,我们来看看Shallow Heap和Retained Heap两列的代表意思。

Shallow Heap表示对象在没有引用其他对象的情况下本身占用的内存(包含的方法、元素)大小。

Retained heap是指对象自己本身的Shallow heap的大小+对象直所引用到的对象的大小。

Dominator Tree还可以使用搜索功能,比如我们知道MainActivity存在内存泄露,那么我们就可以直接搜索MainActivity,如图。

Andorid性能优化(三) 之 如何定位内存泄漏_第13张图片

1.2.4 hprof文件对比

比如现在分析ActivityA的内存泄露问题,可以参考如下步骤:

1、进入ActivityA之前,先dump一个hprof文件HprofA;

2、进入ActivityA操作一会,再退出ActivityA后dump个hprof文件HprofB;

3、采用Histogram对比分析这两个Hprof文件,即可得出ActivityA是否泄露

如图。操作步骤:

步骤一、先打开HprofB文件,接着再打开Histogram,然后点击“Compare to another Heap Dump”按钮

Andorid性能优化(三) 之 如何定位内存泄漏_第14张图片

步骤二、在弹出的对话框中,选择要进行对比的HprofA,然后便可以看到两个hporf文件在内存方面的对比,如图(这时使用自己的拷贝版来对比,所以都是0)。这里也可以使用搜索功能,一旦发现存在大于0,则可以断定存在泄露情况,找出泄露的地方,就可以使用Dominator TreePath To GC Roots -> exclude wakd/soft references再进行分析。

Andorid性能优化(三) 之 如何定位内存泄漏_第15张图片

1.3 Strict Mode

Strict Mode叫作严格模式,是用于检测开发过程中代码违规的工具,一般情况下用于Debug模式下的开发。它的作用主要是检测两大问题,一是ThreadPolicy,另一个是VmPolicy。

ThreadPolicy用于监控启用严格模式的线程是否进行磁盘读写、网格访问等操作,一般用于主线程的监控。

VmPolicy可以发现某此类型的内存泄漏。例如IO类操作是否没有及时调用close方法、Activity是否无法回收、BroadcastReceiver是否没有进行反注册、类的实例个数是否上限是否超出上限,等。

Strict Mode的使用一般会在Application中进行启用,如代码:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        initStricMode();
    }

    private void initStricMode() {
        if (!BuildConfig.DEBUG) {
            return;
        }

        // ThreadPolicy
        StrictMode.ThreadPolicy.Builder threadPolicyBuilder = new StrictMode.ThreadPolicy.Builder();
        threadPolicyBuilder
                .detectAll()            // 检测所有项
//                .permitAll()            // 允许所有项

//                .penaltyDialog()        // 处罚通知:弹出违规对话框
//                .penaltyDropBox()       // 处罚通知:将违规信息记录到 dropbox 系统日志目录中(/data/system/dropbox)
//                .penaltyDeath()         // 处罚通知:直接 Crash 掉当前应用程序
//                .penaltyFlashScreen()   // 处罚通知:屏幕闪烁,视设备是否可行
                .penaltyLog();          // 处罚通知:打印违规信息日志
        StrictMode.setThreadPolicy(threadPolicyBuilder.build());

        // VmPolicy
        StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder();
        vmPolicyBuilder
                .detectAll()
                .penaltyLog();
        StrictMode.setVmPolicy(vmPolicyBuilder.build());
    }
}

ThreadPolicy和VmPolicy里头都有一系列的detectXX方法、permitXX方法 和 penaltyXX方法。上述代码中拿ThreadPolicy来看,使用了detectAll()方法,表示为要检测所有的项目,我们可以看下它的源码:

public Builder detectAll() {
    detectDiskReads();
    detectDiskWrites();
    detectNetwork();

    final int targetSdk = VMRuntime.getRuntime().getTargetSdkVersion();
    if (targetSdk >= Build.VERSION_CODES.HONEYCOMB) {
        detectCustomSlowCalls();
    }
    if (targetSdk >= Build.VERSION_CODES.M) {
        detectResourceMismatches();
    }
    if (targetSdk >= Build.VERSION_CODES.O) {
        detectUnbufferedIo();
    }
    return this;
}

源码中可以看到detectAll方法中包含了数个detectXX方法,从它们的命名或注释中便能清楚它们的用途了,这里不作一一介绍。我们如果要单使用这些方法时一定要注意判断Android API版本号。

继续看回示例代码ThreadPolicy中有注释了permitAll()方法,同时它的内部逻辑也是就包含了数个permitXX方法,此类方法表示允许XX项,也就是不检测XX项,刚好跟detectXX系列方法相反。

penaltyXX系列方法代表处罚通知,一般情交下使用penaltyLog()方法表示使用日志输出处罚通知即可。

启动严格模式后,若再次使用上面“内部类导致外部无法被释放”中未经修改的TestActivity时,就会输出如下日志:

1.4 LeakCanary

LeakCanary是比较方便的用于检测内存泄漏的内嵌工具。它的使用也是非常的简单,只需要两个步骤。

首先在build.gradle中配置如下代码:

dependencies {
    ……
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.6'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.6'
}

其实就是初始化,一般会在Application中进行,如代码:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initLeakCanary();
    }

    private void initLeakCanary() {
        if (!BuildConfig.DEBUG) {
            return;
        }
        LeakCanary.install(this);
    }
}

准备工作完成后,我们还是拿上面“内部类导致外部无法被释放”中未经修改的TestActivity为反面示例,再次运行后,使内存泄漏场景生效后,便会在屏幕中间弹出如下的Toast:

Andorid性能优化(三) 之 如何定位内存泄漏_第16张图片

随后,弹出一条通知栏和在桌面中生成一个名字为“Leaks”的快捷方式,通知栏样式如图:

点击刚弹出的通知或点击新生成的桌面快捷图标便打开了一个详细定位问题页面,如下图。

Andorid性能优化(三) 之 如何定位内存泄漏_第17张图片

2 定位和分析内存泄漏的技巧

我们想要定位内存泄漏时,一般都是要对应用代码施加压力并尝试强制内存泄漏。 在应用中引发内存泄漏的一种方式是,先让其运行一段时间,然后再检查堆。 泄漏在堆中可能逐渐汇聚到分配顶部。 不过,泄漏越小,你越需要运行更长时间的应用才能看到泄漏。你还可以通过以下方式之一触发内存泄漏:

  1. 将设备从纵向旋转为横向,然后在不同的 Activity 状态下反复操作多次。 旋转设备经常会导致应用泄漏 Activity、Context 或 View 对象,因为系统会重新创建 Activity,而如果你的应用在其他地方保持对这些对象之一的引用,系统将无法对其进行垃圾回收。
  2. 处于不同的 Activity 状态时,在你的应用与另一个应用之间切换(导航到主屏幕,然后返回到你的应用)。

总之,请记住一句话:定位和分析内存泄漏需要耐心和细心!

 

你可能感兴趣的:(Android进阶与总结,Android性能优化,Memory,Profiler,Memory,Analyzer,Tool,MAT,Strict,Mode,LeakCanary)