安卓性能系列2---优化内存

针对内存优化,打算开两篇,一篇为介绍工具篇,利用现有的工具,来帮助我们更好的解决问题。
在有一定的实践经验后,再另外写一篇,介绍些原理,了解内存模型等内容,这有助于我们写出更好的程序。
先使用,再说原理,我认为这样的安排,才是符合人类认知的,我们总是先接触,对事物有个大概的认知,然后深入了解后,才会做出总结和归纳演绎等,把内容抽象化,得出些结论性的内容。再利用理论来更好的指导我们工作。

本次涉及到的工具有Android Studio自带的Memory Monitor,然后是些adb命令。
最后来看下Deep Memory Profile。

Memory Monitor

现在的Android Studio自带了不少工具,其中Android Monitor就非常好用,可以帮助我们快速的定位问题,这相对以前快捷易用不少,而且图形化。
谷歌有意识的把很多软件的功能集成到它的这个Android Studio IDE中来,例如Sdk Manager现在变成了设置中的一项了,不再是单独的直接另外打开一个窗口,虽然在底部有一个选择可以让我们继续看到原来的界面效果。原本在Android Device Monitor的内容,也部分被搬到了底部的Android Monitor去了。

好了,废话不多说,我们进入主题。

enter image description here

Memory Monitor可以帮助我们实时的看下现在的内存情况是怎样的,如上图所示,深蓝色是我们Allocated,然后透明的浅色是目前还剩余的大小。
有需要我们还可以dump内存情况来看,只需要点击按钮2 另外还可以记录跟踪,这时候点按钮3,这样就可以从我们点击的时刻开始记录。
同时我们可以主动的触发GC。只需要点击按钮1

Demo

以上就是他的基本使用点,在进一步介绍前,我们来写点代码,作为后面讲解用:
假设我们的app只有一个按钮,点击后就会去调用onBugClick函数

public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();          
    private ArrayList<Object> ourBugBeanLists;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }     

    public void onBugClick(View view) {    
        ourBugBeanLists = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            BugBean bugBean = new BugBean();
            bugBean.setIndex(i);
            ourBugBeanLists.add(bugBean);
        }
        Log.e(TAG, "onBugClick() size=" + ourBugBeanLists.size());
    }
}

接着是我们的bean

public class BugBean {

    private List<String> data;
    private int index;
    private String name;

    public BugBean() {
        // 为了能在内存上造成一个起伏,我们的直接来个3000个String
        data = new ArrayList<>(3000);
    }

    ...//getter & setter 
}

在这样的基础上,接着我们开始实际的使用,我们先看下下面的效果图,在大约启动后的第46秒的时候,我点击哪按钮,触发了onBugClick函数,效果很好,内存直接多了差不多1MB的大小。

enter image description here

假设我们实际不知道上面的代码内容,只是一般的在用APP,然后看到整个界面突然内存飞起来了,
然后我们想知道为何增加了这么多,而且多出来的内容实际是什么。

不过由于我们没有点击按钮3去跟踪,所以我们就先来直接dump Java Heap看下现在内存是怎样的,那些类用了内存,做个大概的推断。点击后弹出来的界面大致这样子的。

enter image description here

点击查看大图

面板介绍

现在我们来解释下顶部各栏的意思是什么:

列名 描述
Class Name The Java class responsible for the memory.
Total Count Total number of instances outstanding.
Heap Count Number of instances in the selected heap.
Sizeof Size of the instances (currently, 0 if the size is variable).
Shallow Size Total size of all instances in this heap.
Retained Size Size of memory that all instances of this class is dominating.
Instance A specific instance of the class.
Reference Tree References that point to the selected instance, as well as references pointing to the references.
Depth The shortest number of hops from any GC root to the selected instance.
Shallow Size Size of this instance.
Dominating Size Size of memory that this instance is dominating.

这里简单的说下,左上角可以版主我们整理下面的Class Name面板的排序情况。
包括App Heap和Zygote Heap两种,右边的有Package Tree View 和Class List View两种。

如果我们选中了Class List View,可以快速的看现在各种类的使用情况。
这个可以给我们一个依据,看现在是什么类霸占了较多的内存,一般估计是图片 ^_^。

最右边有一个Analyzer Task 面板,提供对Activity是否泄漏和字符串重复的检测。同时有对应的检测结果和解释可以看。这比生成了快照,然后跑去MAT查看是否感觉便捷多了呢?
虽然现在检测泄漏我用LeakCanary去做,不怎么用MAT了,毕竟用起来还是相对麻烦些。

Tips:

  1. 另外想提的是,这里dump的文件是不能给MAT直接用的,还需要装换下格式。
    步骤如下:
    In the Captures window, right-click a heap snapshot file and select Export to standard .hprof.
    In the Convert Android Java Heap Dump dialog, specify a filename and click OK.

  2. Dump文件已经自动保存好了,如果你后面如果还要看这些信息,可以点击Captures,(View > Tools Windows > Captures.)

静态寻找—Dump Java Heap

按Class List View

按照Retained Size排序,发现ArrayList占用了不少。
计划着我们看左边的Instance面板,也是按照Retained Size排序,点击开头那个最大的,
在底部的Reference Tree,我们看到是在MainActivity里面的ourBugBeanLists占用了很大空间。
这样我们可以某种程度怀疑就是这家伙导致内存上升了,然后去对应的界面看对应的代码是怎么回事。

不过有时候可以直接选对应的Activity,因为内存的增加,一般都是在界面上做了些什么操作导致的,那么我们去选中对应的Activity会是一个不错的方案。

例如像上面那样,直接选MainActivity会好些,因为当程序大了,ArrayList会在很多地方被调用,直接看会像右边那样有一大堆的Instance,如果大小差不多,要排查就不容易了。

当然以上只是提供一个思路,具体问题,灵活处理,以上只是基本套路。

enter image description here

按Package Tree View

除了上面的按类排序看,我们可以按照Packages看,这个我自己比较喜欢的目录树结构,就像我们的项目结构一样,我们可以清晰的看到整体的情况。

enter image description here

根据上图,我们发现的是在com包里面有不少内容,一层层看下去,到了我们的MainActivity用了很多,同时在右边的Instance面板,看到主要是我们的ourBugBeanLists霸占了1204560 ,约1MB。

小结

以上可以帮助我们快速的看内存的情况,不过没有调用的信息,我们不知道到底是哪些函数的调用导致了内存的增加,虽然根据对应的界面的对应变量信息,我们也可以挺快的定位到问题了。

Why Look at the Java Heap?

然后附加个官方说的,为何要看Java Heap

The Java heap display does the following:

  • Shows snapshots of a number of objects allocated by type.
  • Samples data every time a garbage collection event occurs naturally or is triggered by you.
  • Helps identify which object types might be involved in memory leaks.

However, you have to look for changes over time yourself by tracking what’s happening in the graph.

The HPROF Analyzer finds the following potential issues:

  • All destroyed activity instances that are reachable from garbage collection roots.
  • Where the target program has strings that repeat values.

A dominator is at the top of a tree. If you remove it, you also remove the branches of the tree it dominates, so it’s a potential way to free memory.(从根源消灭问题)

动态跟踪— Start Allocation Tracking

前面我们已经知道,像这样的直接看其实是不容易知道到底是哪里加了内存的!纯粹看下怎样就好了,
上面的点也解释了为何我们要看Java Heap的原因了。

我们现在来看下动态的跟踪情况。
我们在发送跳变前先点击下按钮3,大小标记,表示从这里开始跟踪,然后当我们觉得可以的时候再点多一次,表示这次跟踪到这里结束。
效果如下图所示:

enter image description here

然后系统会弹出来一个对话框内容如下

enter image description here

我们再还是那样,按照右边的Size栏排序,然后一路跟踪下去,发现是 OnBugClick()函数霸占了大部分的空间。如果你点击顶部的饼状图的按钮,下面还回出现多一个Panel,可以非常直观的看实际情况!
布局也有多种方式,请自己探索下,效果很绚丽!

在下面的框,当我选中了棕黄色那条时候,右边那一栏显示到了onBugclick()函数,是MainActivity的第196行代码!!!!顶部有显示对应的占用是1.07mb,然后最后面一排非常细小的模块组成的那根柱子,就是我们调用的那几百个对象!是不是很酷的效果!

小结

The Allocation Tracker does the following:

  • Shows when and where your code allocates object types, their size, allocating thread, and stack traces.
  • Helps recognize memory churn through recurring allocation/deallocation patterns.
  • Helps you track down memory leaks when used in combination with the HPROF Viewer. For example, if you see a bitmap object resident on the heap, you can find its allocation location with Allocation Tracker.

ADB

前面我们看的都是非常细的内容,为我们的某些对象的占用情况,不过关于整体性的,目前的内存占用整体用了多少,我们还可以有多少,什么类型的用得多,快满了要OOM了,需要调节下,我们都没有概念。
所以我们现在需要来对这里做个了解 。

adb shell dumpsys meminfo

利用下面这个指令,我们可以查看当前的内存情况。

adb shell dumpsys meminfo <package_name|pid> [-d]

另外这个ADB还有别的如adb shell dumpsys activity 等,这里就不细说了。
我们先打印下我们的demo内容:

adb shell dumpsys meminfo 2173 -d

在我们的终端打印的内容如下 ,数据单位为kilobytes

   ** MEMINFO in pid 2173 [com.example.sanjay.demo] **
                        Shared  Private     Heap     Heap     Heap
                  Pss    Dirty    Dirty     Size    Alloc     Free
               ------   ------   ------   ------   ------   ------
      Native     1038     1280      996     7024     7013       10
      Dalvik     2451    12012     2000     9300     9103      197
       Stack       72       12       72
      Cursor        0        0        0
      Ashmem        0        0        0
   Other dev        4       24        0
    .so mmap      721     5028      200
   .jar mmap        0        0        0
   .apk mmap       80        0        0
   .ttf mmap        1        0        0
   .dex mmap     1501        0       12
  Other mmap        6        8        4
     Unknown      132        4      132
       TOTAL     6006    18368     3416    16324    16116      207

Objects
              Views:       15         ViewRootImpl:        1
        AppContexts:        3           Activities:        1
             Assets:        2        AssetManagers:        2
      Local Binders:        7        Proxy Binders:       14
   Death Recipients:        0
    OpenSSL Sockets:        0

SQL
        MEMORY_USED:        0
 PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0

有意思的是,这个命令打印出来的内容变了挺多次的。我看到有列和行对换的,也有一些有一些别的列的。
例如官网的文档里面有Private Clean列。我的这个没有。看来不同系统版本的差异也体现在这里哈。
关于以上数据,我们可能比较关心的是Pss Total & Private Dirty 列,有时the Private Clean & Heap Alloc 也还是我们需要关心下的。具体含义在下面做介绍

解释含义

列名

看下我们第一行的列名意思

  • Proportional Set Size (PSS)
    可以简单的理解为就是一个进程所占用的内存,具体为占用的私有内存加上平分的共享内存。所以各进程的Pss相加基本等于实际被使用的物理内存。 一般安卓都会只开一个进程,所以某种程度是APP占用的大小
  • Private (Clean and Dirty) RAM
    你的进程独享的内存,非共享的,又不能换页出去(can not be paged to disk )的内存的大小。在我们的程序被杀后该内存也不会被释放掉,只是重新回到缓冲中去。我们的Dalvik和native heap的内存调用都会归到 Private Dirty RAM,所以这个空间比较昂贵,因为安卓在内存管理上不使用Swap,我们使用的内存page是不会做存储在硬盘的,是一直待在内存中的(估计是为了效率和实际考虑吧)。

  • Shared Dirty RAM
    它和上面的Private不一样的只在这是共享的内存,不过一样是不能换页的。

  • Heap Size / Alloc / Free
    这几个就是对应的堆大小,申请的,还省下多少,基本上是这样的:HeapSize =HeapAlloc + HeapFree。

一般来说,我们只需要关注Pss TotalPrivate Dirty columns.

行名

在看完了列,我们接下来看行信息

列名 意义
Native 和Dalvik 对应我们的JNI和java代码所霸占的内存信息。
Cursor Cursor消耗的内存(KB)
Ashmem 匿名共享内存用来提供共享内存通过分配一个多个进程 可以共享的带名称的内存块
Other dev /dev/内部driver占用的在 “Other dev”
.so mmap C 库代码占用的内存
.jar mmap Java 文件代码占用的内存
.apk mmap apk代码占用的内存
.ttf mmap ttf 文件代码占用的内存
.dex mmap Dex 文件代码占用的内存
Other mmap 其他文件占用的内存
Unknown 目前系统无法归类的内存页,主要是写native的调用,因为系统收集信息的时候由于ASLR,所以工具无法识别出这是什么 As with the Dalvik heap, the Pss Total for Unknown takes into account sharing with Zygote, and Private Dirty is unknown RAM dedicated to only your app.
TOTAL The total Proportional Set Size (PSS) RAM used by your process. This is the sum of all PSS fields above it. It indicates the overall memory weight of your process, which can be directly compared with other processes and the total available RAM.The Private Dirty and Private Clean are the total allocations within your process, which are not shared with other processes. Together (especially Private Dirty), this is the amount of RAM that will be released back to the system when your process is destroyed. Dirty RAM is pages that have been modified and so must stay committed to RAM (because there is no swap); clean RAM is pages that have been mapped from a persistent file (such as code being executed) and so can be paged out if not used for a while.

一般来说 Heap Alloc是大于Pss Total和Private Dirty 的,因为我们的程序是靠Zygote来生成的,因此还包括一些和别的app共享的信息。

而Total 的 PSS 这个值就是你的应用真正占据的内存大小,通过这个信息,你可以轻松判别手机中哪些程序占内存比较大了。

另外说下几个重要的参数:

  • ViewRootImpl
    The number of root views that are active in your process. Each root view is associated with a window, so this can help you identify memory leaks involving dialogs or other windows.

  • AppContexts and Activities
    The number of app Context and Activity objects that currently live in your process. This can be useful to quickly identify leaked Activity objects that can’t be garbage collected due to static references on them, which is common. These objects often have a lot of other allocations associated with them and so are a good way to track large memory leaks.

    Note: A View or Drawable object also holds a reference to the Activity
    that it’s from, so holding a View or Drawable object can also lead to
    your app leaking an Activity.

TIPS: 另外,如果我们在上面的命令尾部加多 -d 的flag,会有额外的信息:

小结

这命令帮助我们知道我们的调用的内存被 分成的各中RAM调用 ,知道目前的整体情况。

  • 如何指导我们做优化?
    前面这个命令可以怎么知道我们去做内存优化问题呢?我们用Memory Monitor可以非常细粒度的看到内存的占用情况,可以针对性的对一些霸占内存多的代码做优化。但现在的这条资料提供的内存信息非常的粗,没有细节,只有APP目前的整体的使用情况,到底怎么指导我们去做优化呢?
    虽然后面的Objects和SQL的内容可以帮助我们去看是否有泄漏的问题,不过前面的数据到底是什么意义呢?
    对这个问题,我再官方的文档看到这么一段:

    The process has now almost tripled in size, to 4MB, simply by showing some text in the UI. This leads to an important conclusion: If you are going to split your app into multiple processes, only one process should be responsible for UI. Other processes should avoid any UI, as this will quickly increase the RAM required by the process (especially once you start loading bitmap assets and other resources). It may then be hard or impossible to reduce the memory usage once the UI is drawn.
    Additionally, when running more than one process, it’s more important than ever
    that you keep your code as lean as possible, because any unnecessary RAM overhead
    for common implementations are now replicated in each process.

    建议我们如果你打算开多线程,应该只有一个负责UI的。不然你的RAM会用得很快哦。
    以上的内容,除了建议,个人感觉,对内存优化的指导意义在给我们一个大方向,告诉我们具体应该往那个方向尝试去花多点时间做优化。

adb shell top

当我们打下面其中一条命令时候

adb shell top 
 adb shell procrank

终端,会有类似下面的内容

User 89%, System 7%, IOW 0%, IRQ 2%
User 270 + Nice 0 + Sys 23 + Idle 2 + IOW 0 + IRQ 0 + SIRQ 7 = 302

  PID PR CPU% S  #THR     VSS     RSS PCY UID      Name
  370  0  89% S    80 631912K  61772K  fg system   system_server
   60  0   3% S    13  12968K    416K  fg root     /sbin/adbd    
  407  0   0% S     7  63800K   3820K  fg system   /system/bin/surfaceflinger
  472  0   0% S    19 553932K  55480K  fg u0_a44   com.android.systemui
    1  0   0% S     1    716K    484K  fg root     /init
   49  0   0% S     1      0K      0K  fg root     ext4-dio-unwrit
   46  0   0% S     4 509156K  47892K  fg root     zygote 
  173  0   0% S    10 525896K  34820K  bg u0_a74   com.example.sanjay.demo

除了上面提到的PSS外,我们看到另外几个新属性

VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

检测内存泄漏

检测泄漏目前有不少很好的工具,例如LeakCanary,MAT等!关于这两者就不做过多介绍,找下就有一堆资料。
另外来看下官方文档提到的两个小技巧:
You can also trigger a memory leak in one of the following ways:

  1. Rotate the device from portrait to landscape and back again multiple times while in different activity states. Rotating the device can often cause an app to leak an Activity, Context, or View object because the system recreates the Activity and if your app holds a reference to one of those objects somewhere else, the system can’t garbage collect it.
  2. Switch between your app and another app while in different activity states (navigate to the Home screen, then return to your app).

非常经典的两个哈!一个旋转屏幕,一个来回切换。

Tip: You can also perform the above steps by using the “monkey” test
framework. For more information on running the monkey test framework,
read the monkeyrunner documentation.

Deep Memory Profile

这个是谷歌自己做的一个工具,听名字就感觉有深度哈!一定很厉害啊!

ref

  1. Android Monitor Overview
  2. HPROF Viewer and Analyzer
  3. Allocation Tracker
  4. Managing Your App’s Memory

热评文章