如何检查 Android 应用的内存使用情况

时间:2021-12-13 02:45:16

Android是为移动设备而设计的,所以应该关注应用的内存使用情况。尽管Android的Dalvik虚拟机会定期执行垃圾回收操作,但这也不意味着就可以忽视应用在何时何处进行内存分配和释放。为了提供良好的用户体验,做到系统在不同应用间流畅切换,当用户和应用无交互时,避免应用不必要的内存消耗是很重要的。

尽管在开发过程中很好的遵守了《管理应用内存》Managing Your App Memory )中的原则(也是应该遵守的),仍然可能会有对象泄露或引入其他的内存bug。对此的安全性,可以采取的措施就是Android代码加密,本地数据保护等一系列安全的加密技术。想详细了解的可以关注下爱加密,专业的移动应用安全智能服务提供商!唯一来确定应用使用了尽可能少的内存的方法,就是使用工具来分析应用的内存使用情况。

解析日志信息

最简单的调查应用内存使用情况的地方就是Dalvik日志信息。可以在logcat(输出信息可以在Device Monitor或者IDE中查看到,例如Eclipse和Android Studio)中找到这些日志信息。每次有垃圾回收发生,logcat会打印出带有下面信息的日志消息:

[objc] view plaincopy
  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>  

GC原因

触发垃圾回收执行的原因和垃圾回收的类型。原因主要包括:

GC_CONCURRENT

并发垃圾回收,当堆开始填满时触发来释放内存。

GC_FOR_MALLOC

堆已经满了时应用再去尝试分配内存触发的垃圾回收,这时系统必须暂停应用运行来回收内存。

GC_HPROF_DUMP_HEAP

创建HPROF文件来分析应用时触发的垃圾回收。

GC_EXPLICIT

显式垃圾回收,例如当调用 gc()(应该避免手动调用而是要让垃圾回收器在需要时主动调用)时会触发。

GC_EXTERNAL_ALLOC

这种只会在API 10和更低的版本(新版本内存都只在Dalvik堆中分配)中会有。回收外部分配的内存(例如存储在本地内存或NIO字节缓冲区的像素数据)。

释放数量

执行垃圾回收后内存释放的数量。

堆状态

空闲的百分比和(活动对象的数量)/(总的堆大小)。

外部内存状态

API 10和更低版本中的外部分配的内存(分配的内存大小)/(回收发生时的限制值)。

暂停时间

越大的堆的暂停时间就越长。并发回收暂停时间分为两部分:一部分在回收开始时,另一部分在回收将近结束时。

例如:

[objc] view plaincopy
  1. D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms  

随着这些日志消息的增多,注意堆状态(上面例子中的3571K/9991K)的变化。如果值一直增大并且不会减小下来,那么就可能有内存泄露了。

查看堆的更新

为了得到应用内存的使用类型和时间,可以在Device Monitor中实时查看应用堆的更新:

1.打开Device Monitor。

从<sdk>/tools/路径下加载monitor工具。

2.在Debug Monitor窗口,从左边的进程列表中选择要查看的应用进程。

3.点击进程列表上面的Update Heap

4.在右侧面板中选择Heap标签页。

 

Heap视图显示了堆内存使用的基本状况,每次垃圾回收后会更新。要看更新后的状态,点击Gause GC按钮。

如何检查 Android 应用的内存使用情况

图1.Device Monitor工具显示[1] Update Heap和 [2] Cause GC按钮。右边的Heap标签页显示堆的情况。

跟踪内存分配

当要减少内存问题时,应该使用Allocation Tracker来更好的了解内存消耗大户在哪分配。Allocation Tracker不仅在查看内存的具体使用上很有用,也可以分析应用中的关键代码路径,例如滑动。

例如,在应用中滑动列表时跟踪内存分配,可以看到内存分配的动作,包括在哪些线程上分配和哪里进行的分配。这对优化代码路径来减轻工作量和改善UI流畅性都极其有用。

使用Allocation Tracker:

1.打开Device Monitor 。

从<sdk>/tools/路径下加载monitor工具。

2.在DDMS窗口,从左侧面板选择应用进程。
3.在右侧面板中选择Allocation Tracker标签页。
4.点击Start Tracking
5.执行应用到需要分析的代码路径处。
6.点击Get Allocations来更新分配列表。

列表显示了所有的当前分配和512大小限制的环形缓冲区的情况。点击行可以查看分配的堆栈跟踪信息。堆栈不只显示了分配的对象类型,还显示了属于哪个线程哪个类哪个文件和哪一行。

如何检查 Android 应用的内存使用情况
图2. Device Monitor工具显示了在Allocation Tracker中当前应用的内存分配和堆栈跟踪的情况。

注意:总会有一些分配是来自与 DdmVmInternal 和 allocation tracker本身。

尽管移除掉所有严重影响性能的代码是不必要的(也是不可能的),但是allocation tracker还是可以帮助定位代码中的严重问题。例如,应用可能在每个draw操作上创建新的Paint对象。把对象改成全局变量就是一个很简单的改善性能的修改。

查看总体内存分配

为了进一步的分析,查看应用内存中不同内存类型的分配情况,可以使用下面的 adb  命令:

[objc] view plaincopy
  1. adb shell dumpsys meminfo <package_name>  

应用当前的内存分配输出列表,单位是千字节。

当查看这些信息时,应当熟悉下面的分配类型:

私有(Clean and Dirty) 内存

进程独占的内存。也就是应用进程销毁时系统可以直接回收的内存容量。通常来说,“private dirty”内存是其最重要的部分,因为只被自己的进程使用。它只在内存中存储,因此不能做分页存储到外存(Android不支持swap)。所有分配的Dalvik堆和本地堆都是“private dirty”内存;Dalvik堆和本地堆中和Zygote进程共享的部分是共享dirty内存。

 实际使用内存 (PSS)

这是另一种应用内存使用的计算方式,把跨进程的共享页也计算在内。任何独占的内存页直接计算它的PSS值,而和其它进程共享的页则按照共享的比例计算PSS值。例如,在两个进程间共享的页,计算进每个进程PPS的值是它的一半大小。

PSS计算方式的一个好处是:把所有进程的PSS值加起来就可以确定所有进程总共占用的内存。这意味着用PSS来计算进程的实际内存使用、进程间对比内存使用和总共剩余内存大小是很好的方式。

例如,下面是平板设备中Gmail进程的输出信息。它显示了很多信息,但是具体要讲解的是下面列出的一些关键信息。

注意:实际看到的信息可能和这里的稍有不同,输出的详细信息可能会根据平台版本的不同而不同。

[objc] view plaincopy
  1. ** MEMINFO in pid 9953 [com.google.android.gm] **  
  2.                  Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap  
  3.                Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free  
  4.               ------  ------  ------  ------  ------  ------  ------  ------  ------  
  5.   Native Heap      0       0       0       0       0       0    7800    7637(6)  126  
  6.   Dalvik Heap   5110(3)    0    4136    4988(3)    0       0    9168    8958(6)  210  
  7.  Dalvik Other   2850       0    2684    2772       0       0  
  8.         Stack     36       0       8      36       0       0  
  9.        Cursor    136       0       0     136       0       0  
  10.        Ashmem     12       0      28       0       0       0  
  11.     Other dev    380       0      24     376       0       4  
  12.      .so mmap   5443(51996    2584    2664(55788    1996(5)  
  13.     .apk mmap    235      32       0       0    1252      32  
  14.     .ttf mmap     36      12       0       0      88      12  
  15.     .dex mmap   3019(52148       0       0    8936    2148(5)  
  16.    Other mmap    107       0       8       8     324      68  
  17.       Unknown   6994(4)    0     252    6992(4)    0       0  
  18.         TOTAL  24358(14188    9724   17972(2)16388    4260(2)16968   16595     336  
  19.    
  20.  Objects  
  21.                Views:    426         ViewRootImpl:        3(8)  
  22.          AppContexts:      6(7)        Activities:        2(7)  
  23.               Assets:      2        AssetManagers:        2  
  24.        Local Binders:     64        Proxy Binders:       34  
  25.     Death Recipients:      0  
  26.      OpenSSL Sockets:      1  
  27.    
  28.  SQL  
  29.          MEMORY_USED:   1739  
  30.   PAGECACHE_OVERFLOW:   1164          MALLOC_SIZE:       62  


通常来说,只需关心Pss Total列和Private Dirty列就可以了。在一些情况下,Private Clean列和Heap Alloc列也会提供很有用的信息。下面是一些应该查看的内存分配类型(行中列出的类型):

Dalvik Heap

应用中Dalvik分配使用的内存。Pss Total包含所有的Zygote分配(如上面PSS定义所描述的,共享跨进程的加权)。Private Dirty是应用堆独占的内存大小,包含了独自分配的部分和应用进程从Zygote复制分裂时被修改的Zygote分配的内存页。

 注意:新平台版本有Dalvik Other这一项。Dalvik Heap中的Pss Total和Private Dirty不包括Dalvik的开销,例如即时编译(JIT)和垃圾回收(GC),然而老版本都包含在Dalvik的开销里面。

 Heap Alloc是应用中Dalvik堆和本地堆已经分配使用的大小。它的值比Pss Total和Private Dirty大,因为进程是从Zygote中复制分裂出来的,包含了进程共享的分配部分。

 .so mmap和.dex mmap

mmap映射的.so(本地) 和.dex(Dalvik)代码使用的内存。Pss Total 包含了跨应用共享的平台代码;Private Clean是应用独享的代码。通常来说,实际映射的内存大小要大一点——这里显示的内存大小是执行了当前操作后应用使用的内存大小。然而,.so mmap 的private dirty比较大,这是由于在加载到最终地址时已经为本地代码分配好了内存空间。

 Unknown

无法归类到其它项的内存页。目前,这主要包含大部分的本地分配,就是那些在工具收集数据时由于地址空间布局随机化(Address Space Layout Randomization ,ASLR)不能被计算在内的部分。和Dalvik堆一样, Unknown中的Pss Total把和Zygote共享的部分计算在内,Unknown中的Private Dirty只计算应用独自使用的内存。

TOTAL

进程总使用的实际使用内存(PSS),是上面所有PSS项的总和。它表明了进程总的内存使用量,可以直接用来和其它进程或总的可以内存进行比较。

Private Dirty和Private Clean是进程独自占用的总内存,不会和其它进程共享。当进程销毁时,它们(特别是Private Dirty)占用的内存会重新释放回系统。Dirty内存是已经被修改的内存页,因此必须常驻内存(因为没有swap);Clean内存是已经映射持久文件使用的内存页(例如正在被执行的代码),因此一段时间不使用的话就可以置换出去。

ViewRootImpl

进程中活动的根视图的数量。每个根视图与一个窗口关联,因此可以帮助确定涉及对话框和窗口的内存泄露。

AppContexts和Activities

当前驻留在进程中的ContextActivity对象的数量。可以很快的确认常见的由于静态引用而不能被垃圾回收的泄露的 Activity对象。这些对象通常有很多其它相关联的分配,因此这是追查大的内存泄露的很好办法。

注意:View 和 Drawable 对象也持有所在Activity的引用,因此,持有View 或 Drawable 对象也可能会导致应用Activity泄露。

获取堆转储

堆转储是应用堆中所有对象的快照,以二进制文件HPROF的形式存储。应用堆转储提供了应用堆的整体状态,因此在查看堆更新的同时,可以跟踪可能已经确认的问题。

检索堆转储:

1.打开Device Monitor。

从<sdk>/tools/路径下加载monitor工具。

2.在DDMS窗口,从左侧面板选择应用进程。

3.点击Dump HPROF file,显示见图3。

4.在弹出的窗口中,命名HPROF文件,选择存放位置,然后点击Save

如何检查 Android 应用的内存使用情况

图3.Device Monitor工具显示了[1] Dump HPROF file按钮。

如果需要能更精确定位问题的堆转储,可以在应用代码中调用dumpHprofData()来生成堆转储。

堆转储的格式基本相同,但与Java HPROF文件不完全相同。Android堆转储的主要不同是由于很多的内存分配是在Zygote进程中。但是由于Zygote的内存分配是所有应用进程共享的,这些对分析应用堆没什么关系。

为了分析堆转储,你需要像jhat或Eclipse内存分析工具(MAT)一样的标准工具。当然,第一步需要做的是把HPROF文件从Android的文件格式转换成J2SE HRPOF的文件格式。可以使用<sdk>/platform-tools/路径下的hprof-conv工具来转换。hprof-conv的使用很简单,只要带上两个参数就可以:原始的HPROF文件和转换后的HPROF文件的存放位置。例如:

[objc] view plaincopy
  1. hprof-conv heap-original.hprof heap-converted.hprof  

注意:如果使用的是集成在Eclipse中的DDMS,那么就不需要再执行HPROF转换操作——默认已经转换过了。

现在就可以在MAT中加载转换过的HPROF文件了,或者是在可以解析J2SE HPROF格式的其它堆分析工具中加载。

分析应用堆时,应该查找由下导致的内存泄露:

  • 对Activity、Context、View、Drawable的长期引用,以及其它可能持有Activity或Context容器引用的对象
  • 非静态内部类(例如持有Activity实例的Runnable)
  • 不必要的长期持有对象的缓存

使用Eclipse内存分析工具

Eclipse内存分析工具(MAT)是一个可以分析堆转储的工具。它是一个功能相当强大的工具,功能远远超过这篇文档的介绍,这里只是一些入门的介绍。

 

在MAT中打开类型转换过的HPROF文件,在总览界面会看到一张饼状图,它展示了占用堆的最大对象。在图表下面是几个功能的链接:

  •  Histogram view显示所有类的列表和每个类有多少实例。

正常来说类的实例的数量应该是确定的,可以用这个视图找到额外的类的实例。例如,一个常见的源码泄露就是Activity类有额外的实例,而正确的是在同一时间应该只有一个实例。要找到特定类的实例,在列表顶部的<Regex>域中输入类名查找。

当一个类有太多的实例时,右击选择List objects>with incoming references。在显示的列表中,通过右击选择Path To GC Roots> exclude weak references来确定保留的实例。

  • Dominator tree是按照保留堆大小来显示的对象列表。

应该注意的是那些保留的部分堆大小粗略等于通过GC logsheap updatesallocation tracker观察到的泄露大小的对象。

当看到可疑项时,右击选择Path To GC Roots>exclude weak references。打开新的标签页,标签页中列出了可疑泄露的对象的引用。

注意:在靠近饼状图中大块堆的顶部,大部分应用会显示Resources的实例,但这通常只是因为在应用使用了很多res/路径下的资源。

如何检查 Android 应用的内存使用情况

图4.MAT显示了Histogram view和搜索”MainActivity”的结果。

想要获得更多关于MAT的信息,请观看2011年Google I/O大会的演讲–《Android 应用内存管理》(Memory management for Android apps),在大约21:10 的时候有关于MAT的实战演讲。也可以参考文档《Eclipse 内存分析文档》(Eclipse Memory Analyzer documentation)。

对比堆转储

为了查看内存分配的变化,比较不同时间点应用的堆状态是很有用的方法。对比两个堆转储可以使用MAT:

1.按照上面描述得到两个HPROF文件,具体查看获取堆转储章节。

2.在MAT中打开第一个HPROF文件(File>Open Heap Dump)。

3.在Navigation History视图(如果不可见,选择Window>Navigation History),右击Histogram,选择Add to Comp are Basket

4.打开第二个HRPOF文件,重复步骤2和3。

5.切换到Compare Basket视图,点击Compare the Results(在视图右上角的红色“!”图标)。

触发内存泄露

使用上述描述工具的同时,还应该对应用代码做压力测试来尝试复现内存泄露。一个检查应用潜在内存泄露的方法,就是在检查堆之前先运行一会。泄露会慢慢达到分配堆的大小的上限值。当然,泄露越小,就要运行应用越长的时间来复现。

也可以使用下面的方法来触发内存泄露:

1.在不同Activity状态时,重复做横竖屏切换操作。旋转屏幕可能导致应用泄露 ActivityContext 或 View对象,因为系统会重新创建 Activity,如果应用在其它地方持有这些对象的引用,那么系统就不能回收它们。

2.在不同Activity状态时,做切换应用操作(切换到主屏幕,然后回到应用中)。

提示:也可以使用monkey测试来执行上述步骤。想要获得更多运行 monkey 测试的信息,请查阅 monkeyrunner 文档。