二、基本介绍 Memory Profiler是Android Profiler的一个组件, 用于帮助分析内存泄露和内存抖动的问题。 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 界面介绍
其中: 标注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的隐私,这里就不再附图了)。
不同于LeakCanary, Android Memory Profiler更侧重于宏观场景下的内存分析。