程序性能优化之内存优化(三)下篇

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

本篇文章将最后从以下两个方面来介绍布局检测与优化:

  • 【Android内存分析工具:Memory Profiler】
  • 【利用Android Studio、MAT对Android进行内存泄漏检测】

一、Android内存分析工具:Memory Profiler

1.1 前言

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

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

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

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

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

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

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

1.2、基本介绍

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

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

1.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,如下图所示:

程序性能优化之内存优化(三)下篇_第1张图片

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


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


程序性能优化之内存优化(三)下篇_第2张图片

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

1.2.2 界面介绍

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


程序性能优化之内存优化(三)下篇_第3张图片

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

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

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

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

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

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

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

1.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启动后分配对象的数量)。

1.3、基本用法

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

1.3.1 查看内存分配情况

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

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

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


程序性能优化之内存优化(三)下篇_第4张图片

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

1.3.2 查看内存占用情况

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

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

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


程序性能优化之内存优化(三)下篇_第5张图片

图中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按键。

1.4、使用示例

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


程序性能优化之内存优化(三)下篇_第6张图片

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


程序性能优化之内存优化(三)下篇_第7张图片

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

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


程序性能优化之内存优化(三)下篇_第8张图片

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

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

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


程序性能优化之内存优化(三)下篇_第9张图片

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

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


程序性能优化之内存优化(三)下篇_第10张图片

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

1.5、总结

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

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

二、利用Android Studio、MAT对Android进行内存泄漏检测

Android开发中难免会遇到各种内存泄漏,如果不及时发现处理,会导致出现内存越用越大,可能会因为内存泄漏导致出现各种奇怪的crash,甚至可能出现因内存不足而导致APP崩溃。

2.1内存泄漏分析工具

Android的内存泄漏分析工具常用有Android Studio和基于eclipse的MAT(Memory Analyzer Tool)。通过两者配合,可以发挥出奇妙的效果。Android Studio能够快速定位内存泄漏的Activity,MAT能根据已知的Activity快速找出内存泄漏的根源。

第一步:强制GC,生成Java Heap文件
我们都知道Java有一个非常强大的垃圾回收机制,会帮我回收无引用的对象,这些无引用的对象不在我们内存泄漏分析的范畴,Android Studio有一个Android Monitors帮助我们进行强制GC,获取Java Heap文件。

强制GC:点击Initate GC(1)按钮,建议点击后等待几秒后再次点击,尝试多次,让GC更加充分。然后点击Dump Java Heap(2)按钮,然后等到一段时间,生成有点慢。

程序性能优化之内存优化(三)下篇_第11张图片

生成的Java Heap文件会在新建窗口打开。
程序性能优化之内存优化(三)下篇_第12张图片

第二步:分析内存泄漏的Activity
点击Analyzer Tasks的Perform Analysis(1)按钮,然后等待几秒十几秒不等,即可找出内存泄漏的Activity(2)。
程序性能优化之内存优化(三)下篇_第13张图片

那么我们就可以知道内存泄漏的Activity,因为这个例子比较简单,其实在(3)就已经可以看到问题所在,如果比较复杂的问题Android Studio并不够直观,不够MAT方便,如果Android Studio无法解决我们的问题,就建议使用MAT来分析,所以下一步我们就生成标准的hprof文件,通过MAT来找出泄漏的根源。

第三步:转换成标准的hprof文件
刚才生成的Heap文件不是标准的Java Heap,所以MAT无法打开,我们需要转换成标准的Java Heap文件,这个工具Android Studio就有提供,叫做Captures,右击选中的hprof,Export to standard .hprof选择保存的位置,即可生成一个标准的hprof文件。

程序性能优化之内存优化(三)下篇_第14张图片

第四步:MAT打开hprof文件

MAT的下载地址,使用方式和eclipse一样,这里就不多说了,打开刚才生成的hprof文件。点击(1)按钮打开Histogram。(2)这里是支持正则表达式,我们直接输入Activity名称,点击enter键即可。

程序性能优化之内存优化(三)下篇_第15张图片

搜索到了目标的Activity
程序性能优化之内存优化(三)下篇_第16张图片

右击搜索出来的类名,选择Merge Shortest Paths to GC Roots的exclude all phantom/weak/soft etc. references,来到这一步,就可以看到内存泄漏的原因,我们就需要根据内存泄漏的信息集合我们的代码去分析原因。
程序性能优化之内存优化(三)下篇_第17张图片

第六步:根据内存泄漏信息和代码分析原因
使用Handler案例分析,给出的信息是Thread和android.os.Message,这个Thread和Message配合通常是在Handler使用,结合代码,所以我猜测是Handler导致内存泄漏问题,查看代码,直接就在函数中定义了一个final的Handler用来定时任务,在Activity的onDestroy后,这个Handler还在不断地工作,导致Activity无法正常回收。

// 导致内存泄漏的代码
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
final Handler handler = new Handler();
handler.post(new Runnable() {
 @Override
 public void run() {
   textView.setText(String.valueOf(timer++));
   handler.postDelayed(this, 1000);
  }
 });
}

修改代码,避免内存泄漏

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
handler.post(new Runnable() {
 @Override
 public void run() {
  textView.setText(String.valueOf(timer++));
  if (handler != null) {
   handler.postDelayed(this, 1000);
  }
 }
});
}
private Handler handler = new Handler();
@Override
protected void onDestroy() {
 super.onDestroy();
 // 避免Handler导致内存泄漏
 handler.removeCallbacksAndMessages(null);
 handler = null;
}

重新测试,确保问题已经解决。
参考:https://www.cnblogs.com/taoweiji/p/5760537.html
https://blog.csdn.net/gaugamela/article/details/79027538

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

你可能感兴趣的:(程序性能优化之内存优化(三)下篇)