Android 应用内存数据分析方法
目录
内存优化是 Android 开发中一个非常重要的环节,如果不注意就可能出现>内存泄漏,内存溢出,应用运行缓慢,效率低下等问题,严重影响用户体验。本文主要介绍 Android 应用内存的抓取和内存数据分析方法。
一. Android Profiler 分析工具
1. Profiler 概览信息
使用 Android Studio profiler 工具对进程内存进行追踪,可以看到如下图的内存概览信息。

从概览信息中可以看到 Java, Code 部分内存占用较高,可以参考 Android 官网 memory-profiler 的相关内容。
内存计数中的类别如下所示:
- Java:从 Java 或 Kotlin 代码分配的对象内存。 
- Native:从 C 或 C++ 代码分配的对象内存。 
即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。
- Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。) 
- Stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。 
- Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。 
- Other:您的应用使用的系统不确定如何分类的内存。 
- Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。 
2. Profiler 内存分析

点击左上角 dump java heap 按钮,或者使用 CTRL+D 快捷键,可以抓取进程的内存信息,在这里可以快速查看对象的分配数量及占用的大小和一些调用的堆栈。这里的功能比较有限,我们需要使用 Export Heap Dump 按钮将内存数据导出为 hprof 文件然后再使用 MAT 工具分析。使用 MAT 前,需要使用 SDK 中的 hprof-conv 工具转换版本:
hprof-conv memory-20190429T103715.hprof a.hprof 
二. MAT 内存分析工具
1. 查看内存分配
MAT 是一个强大的 Java 内存分析工具,可以在 eclipse 官网下载。打开 Heap Dump a.hprof 文件,可以看到内存泄漏的概要信息。

在 Overview 界面点击 Leak Suspects,可以查看内存泄漏的详细信息。

在 Overview 界面点击 Histogram,可以查看内存分配的详细信息,可以看到对象分配的个数,占用的内存大小等。可以在这个界面查看分析为什么创建了大量的对象,如果不是必要的,可以考虑减少对象的分配来优化内存占用。

可以看到内存中分配了超过 7 万个 String 对象,可以考虑优化,减少 String 对象的创建。
点击 Dominator tree 按钮,可以以堆栈的形式查看内存的占用信息

从上图中可以看到 Bitmap 对象占用了大量的内存空间,可以考虑在低内存设备上优化。
2. 定位及导出 Bitmap 图片数据
对于没有堆栈的对象,可以使用 Path TO GC Roots 菜单,快速定位引用的位置,

通过引用的调用关系,就可以快速定位到代码位置了。

也可以通过导出内存中的 Bitmap 图片二进制数据,然后查看图片文件来直观的定位图片。
首先点击 Bitmap 对象,可以在 Inspector 中查看属性,可以看到宽度和长度信息 。

然后再在 byte 对象上点击右键,Copy - Save Value To File 保存图片数据到文件 a.data。

之后通过开源图片处理工具 GIMP 打开刚才导出的 a.data 文件,图像类型选择 RGB Alpha,填入上一步 Bitmap 属性中看到的长度和高度,然后点击打开即可打开图片。

3. 定位及导出字符串
从 Histogram 中看到内存中分配了大量的 String 对象,首先使用菜单中的 List objects - withoutgoing references 查看所有的字符串对象。

之后使用 Path to GC Roots 菜单即可查看引用关系和调用栈:


也可以将这些 String 内容导出到 txt 文件,然后查看内存中具体是哪些字符串。首先在字符串列表选项卡使用快捷键 CTRL + A 全选,再点击右键使用 Copy - Save Value to File 菜单即可将文本内容导出到 txt 文件,之后使用常用的文本文档工具查看即可。

4. 内存数据安全的思考
从上述导出 Bitmap 图片和 String 字符串的方法可以看出,如果拿到应用进程内存的拷贝,可以很轻易的拿到敏感的应用数据。例如,如果用户的密码或者应用的 API key 等被储存并以 String 等对象在内存中引用,则可以轻易被获取到。
针对这个安全问题,应该避免用户密码的本地保存,可以使用 Json web token 等认证机制避免和服务器交互时用户密码的使用,同时服务端的 API 可以通过应用签名等额外信息作为和客户端通信的辅助验证信息。
三. 抓取内存的方法
除了通过 Android Studio Profiler 外,还可以通过以下几个方法抓取应用memory dump。
1. 通过 adb 命令
可以使用下面命令导出 hprof 文件,需要应用 apk 是 debug 版,如果 apk 是 release 版,则需要手机系统版本是 userdebug 或者 eng 版。
adb shell am dumpheap com.tencent.mm /data/local/tmp/a.hprof
adb pull /data/local/tmp/a.hprof
hprof-conv a.hprof b.hprof
从上面命令可以看出,只要使用模拟器或者手机是 eng 或 userdebug 版就可以轻易抓取 release 版本的应用内存,例如微信、支付宝等应用,可以看到内存安全问题应该得到重视。
2. 应用内主动抓取
可以在应用内使用 android.os.Debug.dumpHprofData(String fileName) 接口主动抓取应用的内存堆栈。只需要在程序运行时调用 dumpHprofData(String fileName) 静态接口即可将应用内存导出到文件。如下是一段在应用异常退出时抓取内存的示例代码仅供参考:
public class CaptureHeapDumpsApplication extends Application {
    private static final String FILE_NAME = "/data/local/tmp/heap-dump.hprof";
    @Override
    public void onCreate() {
        super.onCreate();
        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                String absolutePath = new File(FILE_NAME).getAbsolutePath();
                try {
                    Debug.dumpHprofData(absolutePath);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        });
    }
}