Android内存分析工具:Memory Profiler

一、前言
我们知道,Android系统检测到app有不再使用对象时,就会进行内存回收相关的工作。

尽管Android检测无用对象、回收内存的方法在不断改进,
但在目前所有的Android版本中,进行上述工作时,系统仍需要短暂地停止app的运行。

在大多数情况下,系统进行内存回收的行为是无法被用户察觉到的。
然而,如果应用分配内存的速度大于系统回收的速度,
那么app进程的正常运行可能就回受到影响。

毕竟,系统必须回收到足够的供app需要的内存,才会恢复处于暂停状态的app。
在这种情况下,app就可能出现掉帧、卡顿等现象。

在更严重的情况下,如果出现了内存泄露的问题,那么系统中就可能堆积无法释放的内存,
使得系统必须更加频繁地进行内存回收,从而降低系统的性能。

甚至在极端条件下,系统不得不杀死部分正在后台运行的app进程。
于是用户将后台应用移到前台时,却发现应用无故重启,这显然带来了较差的用户体验。

由此可见,内存对于app而言,是极其关键的性能指标。
目前,分析app内存的工具有很多,
本文主要记录一下Android Studio内置的内存分析工具Memory Profiler。

二、基本介绍
Memory Profiler是Android Profiler的一个组件, 用于帮助分析内存泄露和内存抖动的问题。
当PC连接Android L以上的设备时,该工具才能够正常使用。

Memory Profiler的功能包括:
展示应用内存使用情况的实时图像、抓取内存的dump信息、强制垃圾回收及追踪内存分配。

2.1 开启步骤
打开Memory Profiler的步骤为:
1、 依次点击Android Studio的View → Tool Windows → Android Profiler,
或直接点击工具栏Android Profiler对应的图标;

2、 PC连接Android终端后,在Android Profiler对应的区域选择接的设备和需要监控的进程:

3、 点击Android Profiler界面中MEMORY区域的任意位置,即可开启Memory Profiler,如下图所示:

需要注意的是,如果PC连接Android 7.1以下的设备时,有些关键数据可能无法被Android Profiler统计,
此时Android Profiler会显示如下信息:

这时我们需要依次点击Android Studio的Run → Edit Configurations → Profiling 按键,选中app后点击Enabled advanced profiling,
如下图所示:

为了支持该功能,要求app对应的gradle版本必须在2.4以上。

2.2 界面介绍
打开Memory Profiler后,主界面如下所示(为了方便,这里直接盗取Android技术文档中的图):

其中:
标注1对应的按键用于强制内存回收。

标注2对应的按键用于抓取进程内存的dump信息。

标注3对应的按键用于记录内存的分配信息(连接Android 7.1及以下才会有此按键)。
初次点击时,对应统计的开始时间点;再次点击时,对应统计的结束时间点。
进程在两个时间点之间的内存分配信息,将被Memory Profiler记录和分析。

标注4对应的区域用于缩放时间轴。

标注5对应的按键用于显示实时的内存数据。

标注6对应的区域用于记录事件发生的时间点及大致持续的时间(例如activity状态改变、用户操作界面等事件)。

标注7对应的区域用于显示内存使用情况对应的时间轴(与标注6结合,就可以看出各事件带来的内存变化情况)。
需要说明的是,标注7对应区域显示的内容包括:
不同类型内存占用情况对应的图像;
分配对象数量对应的短画线;
内存回收事件发生的时机。

2.3 统计的数据类型及含义
Memory Profiler主要根据Android系统提供的信息,
统计app独自占用内存,即不统计app与系统或其它app共有的内存。

Memory Profiler统计内存的种类如下图所示:

如上图所示,其中:
Java表示Java代码或Kotlin代码分配的内存;

Native表示C或C++代码分配的内存(即使App没有native层,调用framework代码时,也有可能触发分配native内存);

Graphics表示图像相关缓存队列占用的内存;

Stack表示native和java占用的栈内存;

Code表示代码、资源文件、库文件等占用的内存;

Others表示无法明确分类的内存;

Allocated表示Java或Kotlin分配对象的数量(Android8.0以下时,仅统计Memory Profiler启动后,进程再分配的对象数量;
8.0以上时,由于系统内置了统计工具,Memory Profiler可以得到整个app启动后分配对象的数量)。

三、基本用法
对Memory Profiler有了基本的了解后,我们来看看它的基本用法。

3.1 查看内存分配情况
Memory Profiler可以查看两个时间点之间的内存分配情况,包括:
对象的类型、占用内存的大小、栈信息等。
连接8.0以上的设备时,Memory Profiler还可以显示对象被回收的时间。

PC连接8.0以上的设备时,在内存统计的时间线上,直接点击和拖动就可以选择观察区域;
连接低版本的设备时,则需要点击Record Memory allocations按键(2.2小结介绍的标注3)选择观察区域。

选定观察区域后, Memory Profiler就可以统计这段时间内app分配内存的情况:

从图中可以看出,Memory Profiler可以显示分配对象的类名;
点击类后,会在Instance View显示具体的对象;
点击具体对象后,会在Call back区域显示调用栈。
点击调用栈信息后,就会跳转到具体的代码。

3.2 查看内存占用情况
点击2.2小结介绍的标注2,即可抓取点击后一段时间内app占用内存的dump信息。

通过dump信息,我们可以看到app当前仍存在于内存中的对象。
结合代码,我们可以分析是否有本应被析构却仍存活的泄露对象。

与统计内存分配信息一样,内存占用信息同样会显示对象的类型、数量、占用内存的大小、引用关系等。
如下图所示:

图中Alloc Count表示堆中分配对象的数量;
Shallow Size表示对象使用Java内存的大小,单位为byte;
Retained Size表示对象占用的实际内存大小,大于等于Shallow Size;
7.0及以上版本的设备,还会显示对象占用的Native Size。

点击具体的对象时,也会显示Instance View。
此时,Instance View显示的信息变多了,包括:
Depth表示当前对象到任一GC root的最短跳数;
Shallow Size、Retained Size的含义与前文一致。
同样,7.0及以上版本的设备,还会显示对象占用的Native Size。

从图中可以看出,Instance View不会显示栈信息。
如果想获得栈信息的话,必须先点击Record Memory allocations按键。

四、使用示例
利用Memory Profiler,我分析了一下某反病毒引擎SDK的内存占用情况。
随便写了个demo,继承SDK后启动app,内存占用情况如下图所示:

然后,通过操作UI初始化SDK,发现稳定后内存占用情况如下图所示:

比对前后两图,不考虑界面UI变化消耗的内存,
可以看出:内存增加的主体部分来自于Native函数,其它类型的内存变化几乎可以忽略。
这是因为该SDK初始化时,最主要的工作是在Native层加载底层的so文件。

接下来,我们在demo里调用SDK的接口,批量扫描样本,统计内存消耗情况如下图所示:

从图中可以看出app消耗的内存飙升到了104M,
与初始化后的内存相比,其中增加主要是code和native类型的内存。

根据2.3小结的描述,我们知道code类型统计的是app进程需要的资源文件、库文件等,
因此这部分内存主要是SDK中引擎加载病毒库等消耗掉的内存;
而Native内存的消耗,应该也是由于引擎扫描文件导致的。

按照3.1小结的描述,我们查看了一下批量扫描这段时间内,
app进程内存分配的情况,如下图所示:

容易看出,在这段时间内,除去native内存外,整个app分配内存并不大,
且按照对象占用内存的大小排序,排在前列的都是基本数据类型。
因此,这段时间内app进程的内存应该是比较正常的。

我们进行GC操作,内存情况如下图所示:

发现内存几乎和扫描前一致,按照3.2小结进行dump分析,没有发现泄露对象。
因此,可以确认SDK不存在内存泄露的问题(dump信息含有sdk的隐私,这里就不再附图了)。

五、总结
至此,我们已经介绍了Android Memory Profiler工具的基本情况,
并简单列举了该工具的使用方式。

个人感觉,不同于LeakCanary,
Android Memory Profiler更侧重于宏观场景下的内存分析。

猜你喜欢

转载自blog.csdn.net/gaugamela/article/details/79027538