zoukankan      html  css  js  c++  java
  • 程序性能优化之内存优化(三)下篇

    阿里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对应的区域选择接的设备和需要监控的进程:

     
    19956127-cf3b84c2b163e28e.png
     

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

     
    19956127-21b8efffdd78874f.png
     

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

     
    19956127-ee51ac9e692c07d8.png
     

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

     
    19956127-ef2ee96fef98b149.png
     

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

    1.2.2 界面介绍

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

     
    19956127-693a23594ee0eb3b.png
     

    其中:
    标注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统计内存的种类如下图所示:

     
    19956127-35bd750bb7bee5c3.png
     

    如上图所示,其中:
    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分配内存的情况:

     
    19956127-2349295779d346c8.png
     

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

    1.3.2 查看内存占用情况

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

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

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

     
    19956127-b069d874ece9899e.png
     

    图中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,内存占用情况如下图所示:

     
    19956127-dbf6f6878dcba0f5.png
     

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

     
    19956127-ce3bec92d404958e.png
     

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

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

     
    19956127-9d843c96b69cb3ea.png
     

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

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

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

     
    19956127-6b1d58bba3386152.png
     

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

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

     
    19956127-68f49e6de263b6da.png
     

    发现内存几乎和扫描前一致,按照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)按钮,然后等到一段时间,生成有点慢。

     
    19956127-6592fa8f5a36eab6.png
     


    生成的Java Heap文件会在新建窗口打开。

     
    19956127-b3e5a5e6eb87ad98.png
     


    第二步:分析内存泄漏的Activity
    点击Analyzer Tasks的Perform Analysis(1)按钮,然后等待几秒十几秒不等,即可找出内存泄漏的Activity(2)。

     
    19956127-dcbed0de855a0979.png
     


    那么我们就可以知道内存泄漏的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文件。

     
    19956127-b50976519e659a0e.png
     


    第四步:MAT打开hprof文件

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

     
    19956127-5823f3bb21f2336b.png
     


    搜索到了目标的Activity

     
    19956127-87d810b3377533dd.png
     


    右击搜索出来的类名,选择Merge Shortest Paths to GC Roots的exclude all phantom/weak/soft etc. references,来到这一步,就可以看到内存泄漏的原因,我们就需要根据内存泄漏的信息集合我们的代码去分析原因。

     
    19956127-61723249ddebf7e3.png
     


    第六步:根据内存泄漏信息和代码分析原因
    使用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

  • 相关阅读:
    几种流行Webservice框架性能对比
    java实现的单点登录
    SQL Server性能调优——报表数据库与业务数据库分离
    控制台console输出信息原理理解
    查看程序是否启动或者关闭--比如查看Tomcat是否开启!直接用ps命令查看进程就行了啊
    Tomcat日志问题
    客户端用httpurlconnection来进行http连接的
    一个tomcat上放多个webapp问题,那这多个webapp会不会竞争端口呢?不会!安全两码事
    ps -ef能列出进程号,和端口号没任何关系
    Tomcat打印运行时日志(控制台),访问日志,启动日志
  • 原文地址:https://www.cnblogs.com/Android-Alvin/p/11958692.html
Copyright © 2011-2022 走看看