3

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

 3 years ago
source link: http://www.androidchina.net/2469.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

解析日志信息

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

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和更低版本中的外部分配的内存(分配的内存大小)/(回收发生时的限制值)。

暂停时间

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

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按钮。

图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大小限制的环形缓冲区的情况。点击行可以查看分配的堆栈跟踪信息。堆栈不只显示了分配的对象类型,还显示了属于哪个线程哪个类哪个文件和哪一行。

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

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

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

查看总体内存分配

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

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进程的输出信息。它显示了很多信息,但是具体要讲解的是下面列出的一些关键信息。

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

** MEMINFO in pid 9953 [com.google.android.gm] **
Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap
Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free
------  ------  ------  ------  ------  ------  ------  ------  ------
Native Heap      0       0       0       0       0       0    7800    7637(6)  126
Dalvik Heap   5110(3)    0    4136    4988(3)    0       0    9168    8958(6)  210
Dalvik Other   2850       0    2684    2772       0       0
Stack     36       0       8      36       0       0
Cursor    136       0       0     136       0       0
Ashmem     12       0      28       0       0       0
Other dev    380       0      24     376       0       4
.so mmap   5443(5) 1996    2584    2664(5) 5788    1996(5)
.apk mmap    235      32       0       0    1252      32
.ttf mmap     36      12       0       0      88      12
.dex mmap   3019(5) 2148       0       0    8936    2148(5)
Other mmap    107       0       8       8     324      68
Unknown   6994(4)    0     252    6992(4)    0       0
TOTAL  24358(1) 4188    9724   17972(2)16388    4260(2)16968   16595     336

Objects
Views:    426         ViewRootImpl:        3(8)
AppContexts:      6(7)        Activities:        2(7)
Assets:      2        AssetManagers:        2
Local Binders:     64        Proxy Binders:       34
Death Recipients:      0
OpenSSL Sockets:      1

SQL
MEMORY_USED:   1739
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。

图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文件的存放位置。例如:

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/路径下的资源。

图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 文档。

转载请注明:Android开发中文站 » 如何检查 Android 应用的内存使用情况


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK