Android Memory Profiler、CPU Profiler、MAT 解决内存问题实战
一、前言
内存问题的分析工具有很多种,这里我们选择常见的三种进行学习和实战。
二、工具使用
2.1 Memory Profiler
如上图所示,我们可以获取指定时间的当前内存分配情况。在左下角的类列表中,您可以查看以下信息:
- Allocations:堆中的分配数。
- Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存。
- Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)。
- Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
点击一个类名称可在右侧打开 Instance View 窗口(如图 6 所示)。列出的每个实例都包含以下信息:
- Depth:从任意 GC 根到选定实例的最短跳数。
- Native Size:原生内存中此实例的大小。 只有在使用 Android 7.0 及更高版本时,才会看到此列。
- Shallow Size:Java 内存中此实例的大小。
- Retained Size:此实例所支配内存的大小。
我们也可以通过 堆转储导出到 HPROF 文件:
并通过 Export 进行保存。这里我们保存为 1.hprof 文件,后面会用到。
关于 Memory Profiler 更详细的操作说明推荐看官网的文档学习。
2.2 MAT
MAT(全称 Eclipse Memory Analysis Tools)是一个分析 Java堆数据的专业工具,可以计算出内存中对象的实例数量、占用空间大小、引用关系等,看看是谁阻止了垃圾收集器的回收工作,从而定位内存泄漏的原因。下载地址:https://www.eclipse.org/mat/。
在 Memory Profiler 中导出的 hprof 文件需要进行一个转换之后才能在 mat 工具中打开,该工具在 Android SDK 工具包里面,具体的路径在 …/sdk/platform-tools,里面有一个hprof-conv工具,使用如下的命令
hprof-conv 旧的hprof文件路径 新生成的hprof文件路径
在 mat 中打开这个新生成的 hprof 文件即可:
- Histogram:直方图,可以列出内存中每个对象的名字、数量以及大小。
- Dominator Tree:会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。
在 Histogram 视图中,我们可以搜索 Activity 相关的对象。点击右键可以看到 List objects:
- List objects -> with outgoing references :表示该对象的出节点(被该对象引用的对象)
- List objects -> with incoming references:表示该对象的入节点(引用到该对象的对象)
还可以看到 Merge Shortest Paths to GC Roots:
表示查看 GC Roots 到这个对象的路径,右边是要选择的引用类型,一般我们选择导出第二个即排除软、弱、虚引用,只保留强引用。
2.3 CPU Profiler
手机上运行要分析 APP,再在 AS 的 Profiler 窗口选择要调试的进程,打开 CPU Profiler。然后在AS上点击 Record ,再点击 Stop recording。得到如下图所示信息。
Call Chart 标签提供函数跟踪的图形表示形式,其中水平轴表示函数耗费的时间,垂直轴显示其被调用者。
- 对系统 API 的函数调用显示为橙色
- 对应用自有函数的调用显示为绿色
- 对第三方 API(包括 Java 语言 API)的函数调用显示为蓝色。
更详细的使用可以看Android性能优化之CPU Profiler。
三、实战
3.1 内存泄漏解决实战
Android 内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了, 但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。
在 Memory Profiler 上表现为可用内存逐渐变少或者内存抖动。其中内存抖动可能是因为代码逻辑问题导致内存被不断地进行分配和回收,当然一个地方它的内存一直在抖动, 还有可能是由于内存泄漏引起的, 比如说,内存泄漏导致可用内存逐渐减少, 这时候系统为了增加可用内存,就会一直不断地进行GC, 导致内存一直在抖动。
3.1.1 Activity 溢出
Activity 的溢出比较容易分析,下面模拟一段 Activity 无法回收的代码:
public class MemoryLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_shake);
processBiz();
}
private void processBiz() {
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "------postDelayed------");
}
}, 800000L);
}
}
在 MainActivity 中我们可以再反复进入这个 MemoryLeakActivity,然后在 Memory Profiler 中强制 GC,等待几秒后观察堆内存信息,筛选出 Activity,就可以看到指定 Activity 的实例了。
也可以堆转储然后到 MAT 中去分析:
找出 GC Root 引用:
3.1.2 对象溢出
举例一个典型的分析内存泄漏的过程:
- 使用 Heap查看当前堆大小为 23.00M
- 添加一个页后堆大小变为 23.40M
- 将添加的一个页删除,堆大小为 23.40M
- 多次操作,结果仍相似,说明添加/删除页存在内存泄漏 (也应注意排除其它因素的影响)
- Dump 出操作前后的 hprof 文件 (1.hprof,2.hprof),用 mat打开,并得到 histgram结果
- 使用 HomePage字段过滤 histgram结果,并列出该类的对象实例列表,看到两个表中的对象集合大小不同,操作后比操作前多出一个 HomePage,说明确实存在泄漏
- 将两个列表进行对比,找出多出的一个对象,用查找 GC Root的方法找出是谁串起了这条引用线路,定位结束
很多时候堆增大是 Bitmap引起的,Bitmap在 Histogram中的类型是 byte [],对比两个 Histogram 中的 byte[] 对象就可以找出哪些 Bitmap有差异。另外多使用排序功能,对找出差异很有用。
对象溢出一般是用对象被 static 修饰而无法释放。为了更有效率的找出内存泄露的对象,一般会获取两个堆转储文件(先dump一个,隔段时间再dump一个),通过对比后的结果可以很方便定位。找出差异后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。
3.2 内存抖动解决实战
内存抖动即内存频繁分配和回收导致内存不稳定。频繁创建对象,导致内存不足或者产生内存碎片,内存碎片即内存不连续,有内存空洞, 某两个正在使用的内存中间有一个间隔, 这个间隔虽然也被算在可用内存里面, 但实际上因为它过小, 当我们申请内存的时候,经常是需要申请一定量的连续内存, 而这些碎片小内存不符合要求,是不能拿来使用的。频繁GC会导致卡顿,随后不连续的内存片无法被分配,可分配的内存减少,便最终可能导致OOM。
下面我们来模拟一段导致内存抖动的代码:
public class MemoryShakeActivity extends AppCompatActivity {
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 创造内存抖动(编写耗内存的操作)
for (int index = 0; index <= 100; index++) {
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_shake);
findViewById(R.id.bt_memory).setOnClickListener(v -> mHandler.sendEmptyMessage(0));
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
运行起来后,内存是平稳的:
点击按钮后,开始出现锯齿状:
可以看到上面的连续垃圾桶就代表不断地在GC,我们获取一个区域的内存信息:
我们可以看到锯齿的位置,String[] 的分配是相对比较大的; Shallow Size是该类型实例的总大小(以字节为单位)。于是现在可以锁定,String[] 是最可疑的引起内存抖动的原因, 点击左边的String[]行项,工具会在右边,弹出另外一个窗口, 窗口上边是分配出来的该类型的所有实例(<工具右上>), 点击任意一个实例, 又会在下边弹出一个该实例的内存分配的堆栈信息(<工具右下>——Allocation Call Stack), 信息即这个实例占有的这块内存是在哪里分配的。
我们可以看到, Memory Profiler 工具的右下表格显示出来了右上角选中的对应的实例的分配内存的位置—— “handlerMessage方法中,MemoryShakeActivity 文件的第23行”。 右键选中 Jump to Source, 直接在IDE代码编辑界面,跳转追踪到,可疑诱因String[]的创建源码处 / 位置。 然后便发现原因,进行代码的修改。
也可以通过 CPU Profiler 进行分析,运行程序以及 CPU Profiler 工具, 使用 Record 按钮开始记录某一段CPU执行的时间, 接着点击 Stop 停止对这段时间记录,跟踪这一段 CPU执行的时间, 上述 Record 记录完毕之后会在工具下侧弹出图表界面,如Call Chart,依据这些图表数据如果发现某一段(应用自有函数的调用)代码(即绿色的条形段)在反复地被执行,便是内存抖动的位置。
双击 Call Chart 中的一段绿色条形, 可以直接在IDE代码编辑界面跳转追踪到可疑诱因 String[] 的分配执行函数源码处 / 位置, 然后便发现原因,进行代码的修改。
内存抖动的解决技巧:重点关注循环或者频繁调用的地方, 因为内存抖动就是内存在被不断地回收及分配, 这种情况的话经常是出现在循环或者频繁调用的地方。
- 使用 Memory Profiler 初步排查该工具的图表显示方式非常直观,可以清楚地看到内存的使用情况。也可以很方便地发现 APP 在使用过程中,内存分配图形是不是一个锯齿状,有没有内存抖动的表现。
- 使用 Memory Profiler 的堆转储 / 跟踪分配内存 功能,借助 Instance View 追踪到分配内存较高/分配实例较多的实例类型, 跟踪该实例类型的某几个具体实例的 创建/分配 位置 (或者使用 CPU Profiler,跟踪一段CPU执行的时间,如果发现某一段应用自有函数的调用代码, 即 Call Chart 栏下的绿色条形在反复地被执行,便是内存抖动的位置,追踪这些绿色条形到重复执行的可疑函数的位置), 然后结合代码进行排查,找到诱因位置。
本文参考
视频:
国内Top团队大牛带你玩转Android性能分析与优化
博客:
Android | App内存优化 之 内存抖动解决实战
Android | App内存优化 之 全面理解MAT
Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战
Android | App内存优化 之 全面理解MAT
使用Android Studio和MAT进行内存泄漏分析
官网:
使用 Memory Profiler 查看 Java 堆和内存分配