App内存优化

内存问题

内存直接会影响两个问题:html

  • 异常。OOM、内存分配失败、应用被杀、设备重启等。
  • 卡顿。频繁 GC,在 Dalvik 上尤其明显,虽然 ART 作了优化,可是依然会形成设备卡顿。另外,频繁的出发 LMK 也会形成卡顿。

ART 比 Dalvik 在内存分配和 GC 效率上,提高了 5~10 倍。java

扩展问题:OOM 能够被 try-catch 住吗?分状况,若是在 try 语句中存在申请内存的操做,此时触发的 OOM 是能够被 catch 住的。可是并无用,由于 OOM 的触发一般是压死骆驼最后一根稻草,此处 catch 住了,在别的地方可能也会崩溃。一个有效的方案,是在 catch 块中,释放内存,来保证当前运行环境的健康。python

GC 的性能,能够经过发送 SIGQUIT 信号获取 ANR 日志,可是在高版本中,获取 traces.txt 文件须要 Root 权限,这里不推荐。android

另一个分析 GC 的工具是使用 systrace。systrace 在 Google IO 2017 被推荐,是分析 Android 设备性能的主要工具。它其实是对 atrace 的主机端封装容器,用于控制用户空间和设置 ftrace 的设备端可执行文件。推荐阅读《了解 Systrace》《手把手教你使用Systrace》《systrace》c++

内存问题的两个误区:

  • 内存占用越少越好。
  • Native 内存不用管。

误区一:内存占用越少越好。

VSS、PSS、Java 堆内存不足均可能会引发异常和卡顿。web

可是到底占用多少内存,这是由实际状况,例如:设备、系统、当时运行环境等多个因素影响的。因此不存在一个标准的数值。可以作到的就是“用时分配,及时释放”。正则表达式

上图这样的就算是好的效果,在须要时,能够被释放,而不是一直涨。shell

"一张图,毁十优",在 Android 中,Bitmap 是内存占用的大户。数组

简单回顾 Bitmap 在不一样 Android 版本中的区别:浏览器

Android 3.0 以前,Bitmap 对象放在 Java 堆,像素数据放在 Native 内存。若是不手动调用 recycle,Bitmap Native 内存的回收彻底依赖 finalize 函数调用,这个时机是不可控的。

Android 3.0~7.0,Bitmap 对象和像素数据统一放在 Java 堆中。这样就算咱们不调用 recycle,Bitmap 内存也会随着对象一块儿被回收。虽然这样方便回收,可是 Bitmap 放在 Java 堆中会引起大量的 GC。

Android 8.0,利用 NativeAllocationRegistry 来保证 Bitmap 的像素数据放在 Native 内存,可是依然能够和对象一块儿被快速释放。8.0 还新增了硬件位图 Hardware Bitmap,它能够减小图片内存并提高绘制效率。

误区二:Native 内存不用管

当内存不足的时候,LMK 就开始工做了,按照优先级,从后台、桌面、服务、前台、直到手机重启。

Fresco 会把 Bitmap 放在 Native 内存中,它的原理是利用 libandroid_runtime.so 在 Native 中建立一个 Bitmap 对象,再按正常流程将 Bitmap 加载到 Java 堆中,接下来再把它绘制入前面申请的空的 Native Bitmap 中,而后释放 Java 内存中的图片,实现转换的效果。

注意这样很是规的操做,会带来两个问题:

  • 不一样 Android 系统版本的兼容性问题。
  • 频繁申请、释放 Java Bitmap 致使内存抖动。

测量方法

1、Java 内存分配

Java 内存分配分析工具,经常使用的有 Allocation Tracker 和 MAT。

Allocation Tracker 的三个缺点:

获取信息过于分散,数据中夹杂其余信息。 没法自动化分析。 中止时会把全部数据 dump 出来,此过程可能会形成手机彻底卡死,甚至可能引起 ANR。 Allocation Tracker 的开启方式:

// dalvik
bool dvmEnableAllocTracker()
// art
void setAllocTrackingEnable()
复制代码

2、Native 内存分配

Native 内存能够使用 AddressSanitizer(ASan),Android 以前的版本对 ASan 的支持不太好,须要 Root 和一些额外的操做,自 8.0 开始,就方便不少了,而且无需 Root。

ASan 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。ASan 会检测堆栈和全局对象是否移除,可是不会检测未初始化的读取和内存泄露。ASan 在 Android 下的使用,参考指南。

调试 Native 内存,如今有两种方法:

Malloc 调试 Malloc 钩子 Malloc 调试能够去调试 Native 内存的一些使用问题,例如堆破坏、内存泄露、非法地址等。

可是 Mallock 调试时,App 会变卡,有产生 ANR 的风险。

Malloc 调试在 8.0 以前须要 Root 权限,最简单的方法是找一台 8.0 的设备进行调试。

adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
复制代码

Malloc 钩子是在 Android P 以后才有的。Android 的 libc 支持拦截在程序执行期间发生的全部分配/释放调用,方便咱们构建自定义的内存检测工具。

adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
复制代码

优化

内存优化,从三个维度入手:

  • 设备分级
  • Bitmap 优化
  • 内存泄露

1、设备分级

内存优化首先须要根据设备环境来综合考虑。内存占用并不是越少越好,而是应该在占用内存和设备内存之间取平衡。

资源加载在内存中,意味着下次使用的时候会更快。所以咱们可让高端设备使用更多的内存,作到针对设备性能的好坏使用不一样的内存分配和回收策略。

一、设备分级

使用内存状况对设备分级,相似 device-year-class 的策略,对低端设备能够关闭复杂动画或某些功能;使用 565 格式的图片,使用更小的缓存内存等。

二、缓存管理

有一套统一的缓存管理机制,能够适当地使用内存,当内存不足时,能够有效的归还内存。能够使用 onTrimMemort 回调,根据不一样状态决定释放多少内存。

三、进程模型

一个空的进程,也会占用 10MB 的内存,而有些应用启动就有十几个进程。能够经过减小应用启动的进程数、减小常驻进程。

四、安装包大小。

安装包中的代码、资源、图片以及 so 库的说起,和他们运行时占用的内存有关系。减小 APK 的大小,也能够优化低端机上的运行效果。例如各大 App 会推出 Xxx Lite 版本。

2、Bitmap优化

Bitmap 是内存优化的“永恒主题”。

方法一:统一图片库

图片内存优化的前提是收拢图片的调用,这样能够作总体的控制策略。

Glide、Fresco 或者是自研的图片库,不管用什么,要作到统一。不然各个库内部本身维护缓存,单看其实作的都很好,让内存保持在一个良好的环境下,可是加起来就可能致使内存爆表。

方法二:统一监控

要对一些特殊的场景,作好监控。

大图片监控。当知道显示图片控件的宽高时,是能够算出这个图片占用的合理内存的。若是大于此合理值,就应该通报出来,提醒开发者,例如弹窗。 重复图片监控。指的是 Bitmap 像素数据彻底一致,可是有多个不一样对象存在。 图片总内存。由于使用统一图片库,咱们很容易统计应用全部图片占用的内存。在发生 OOM 时,也能够把图片占用的总内存,Top N 的图片写到 Log 中,帮助咱们排查。

3、内存泄露

内存泄露简单来讲,就是再也不使用的内存,没法被回收。

内存泄露主要分两种状况:

  • 同一对象泄露。
  • 每次都泄露新的对象。

能够经过集中策略监控内存泄露:

  • Java 内存泄露。使用相似 LeakCanary 自动化检测方案,至少作到在 Java 层不存在内存泄露。
  • OOM 监控。OOM 时,记录信息或者内存快照,达到后期分析的目的。 Native 内存泄露监控。推荐《微信 Android 终端内存优化实践》
  • 针对没法重编 so 的状况。使用 PLT Hook 拦截库的内存分配函数,就能够重定向到本身的实现后记录分配的内存地址、大小、来源 so 库等信息。
  • 针对可重编的 so 状况。经过 GCC 的 "-finstrument-functions"参数给全部函数插装,庄中模拟调用栈,入栈出栈操做;经过 id 的 “--wrap”参数拦截内存分配和释放函数。

内存泄露监控,Android 下占时只有 Java 层的内存泄露有成熟的技术方案,剩下的都是实验室方案,能够在开发阶段使用,不建议在线上 App内集成发布。

内存监控

一、采集方式

针对部分用户,每 5 分钟采集一次 PSS、Java 堆、图片总内存。

PSS:实际使用物理内存。

二、计算指标

经过标准的计算指标,来分析内存是否良好。

全部的优化,都是针对某一个指标进行优化。内存优化也是如此。须要有一个标准的指标来辅助咱们识别当前的状况,以及优化的效果。

内存异常率:

内存 UV 异常率 = PPS 超过 400MB 的 UV / 采集 UV

触顶率:

统计超过 85% 的内存占用状况。

内存 UV 触顶率 = Java 堆占用超过 85% 的 UV / 采集 UV

long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 内存使用超过最大限制的 85%
float proportion = (float) javaUsed / javaMax;
复制代码

三、GC 监控

在开发阶段,能够经过 Debug.startAllocCounting 来监控 Java 内存分配和 GC 状况。

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
复制代码

Android 6.0 以上能够拿到更精准的数据。

// 运行的 GC 次数
Debug.getRuntimeStat("art.gc.gc-count");
// GC 使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式 GC 的次数
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式 GC 的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");
阻塞式 GC 会暂停 App 线程,可能致使卡顿。
复制代码

日志

Dalvik 日志消息

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
复制代码
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
复制代码

垃圾回收缘由

什么触发了垃圾回收以及是哪一种回收。可能出现的缘由包括:

  • GC_CONCURRENT 在您的堆开始占用内存时能够释放内存的并发垃圾回收。
  • GC_FOR_MALLOC 堆已满而系统不得不中止您的应用并回收内存时,您的应用尝试分配内存而引发的垃圾回收。
  • GC_HPROF_DUMP_HEAP 当您请求建立 HPROF 文件来分析堆时出现的垃圾回收。
  • GC_EXPLICIT 显式垃圾回收,例如当您调用 gc() 时(您应避免调用,而应信任垃圾回收会根据须要运行)。
  • GC_EXTERNAL_ALLOC 这仅适用于 API 级别 10 及更低级别(更新版本会在 Dalvik 堆中分配任何内存)。外部分配内存的垃圾回收(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。

释放量

今后次垃圾回收中回收的内存量。

堆统计数据

堆的可用空间百分比与(活动对象数量)/(堆总大小)。

外部内存统计数据

API 级别 10 及更低级别的外部分配内存(已分配内存量)/(发生回收的限值)。

暂停时间

堆越大,暂停时间越长。并发暂停时间显示了两个暂停:一个出如今回收开始时,另外一个出如今回收快要完成时。

在这些日志消息积聚时,请注意堆统计数据的增大(上面示例中的 3571K/9991K 值)。若是此值继续增大,可能会出现内存泄漏。

ART 日志消息

与 Dalvik 不一样,ART 不会为未明确请求的垃圾回收记录消息。只有在认为垃圾回收速度较慢时才会打印垃圾回收。更确切地说,仅在垃圾回收暂停时间超过 5ms 或垃圾回收持续时间超过 100ms 时。若是应用未处于可察觉的暂停进程状态,那么其垃圾回收不会被视为较慢。始终会记录显式垃圾回收。

ART 会在其垃圾回收日志消息中包含如下信息:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
复制代码
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
复制代码

垃圾回收缘由

什么触发了垃圾回收以及是哪一种回收。可能出现的缘由包括:

  • Concurrent 不会暂停应用线程的并发垃圾回收。此垃圾回收在后台线程中运行,并且不会阻止分配。
  • Alloc 您的应用在堆已满时尝试分配内存引发的垃圾回收。在这种状况下,分配线程中发生了垃圾回收。
  • Explicit 由应用明确请求的垃圾回收,例如,经过调用 gc() 或 gc()。与 Dalvik 相同,在 ART 中,最佳作法是您应信任垃圾回收并避免请求显式垃圾回收(若是可能)。不建议使用显式垃圾回收,由于它们会阻止分配线程并没必要要地浪费 CPU 周期。若是显式垃圾回收致使其余线程被抢占,那么它们也可能会致使卡顿(应用中出现间断、抖动或暂停)。
  • NativeAlloc 原生分配(如位图或 RenderScript 分配对象)致使出现原生内存压力,进而引发的回收。
  • CollectorTransition 由堆转换引发的回收;此回收由运行时切换垃圾回收引发。回收器转换包括将全部对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,回收器转换仅在如下状况下出现:在 RAM 较小的设备上,应用将进程状态从可察觉的暂停状态变动为可察觉的非暂停状态(反之亦然)。
  • HomogeneousSpaceCompact 齐性空间压缩是空闲列表空间到空闲列表空间压缩,一般在应用进入到可察觉的暂停进程状态时发生。这样作的主要缘由是减小 RAM 使用量并对堆进行碎片整理。
  • DisableMovingGc 这不是真正的垃圾回收缘由,但请注意,发生并发堆压缩时,因为使用了 GetPrimitiveArrayCritical,回收遭到阻止。通常状况下,强烈建议不要使用 GetPrimitiveArrayCritical,由于它在移动回收器方面具备限制。
  • HeapTrim 这不是垃圾回收缘由,但请注意,堆修剪完成以前回收会一直受到阻止。

垃圾回收名称

ART 具备能够运行的多种不一样的垃圾回收。

Concurrent mark sweep (CMS)

整个堆回收器,会释放和回收映像空间之外的全部其余空间。

Concurrent partial mark sweep

几乎整个堆回收器,会回收除了映像空间和 zygote 空间之外的全部其余空间。

Concurrent sticky mark sweep

生成回收器,只能释放自上次垃圾回收以来分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,由于它更快速且暂停时间更短。

Marksweep + semispace

非并发、复制垃圾回收,用于堆转换以及齐性空间压缩(对堆进行碎片整理)。

释放的对象

这次垃圾回收从非大型对象空间回收的对象数量。

释放的大小

这次垃圾回收从非大型对象空间回收的字节数量。

释放的大型对象

这次垃圾回收从大型对象空间回收的对象数量。

释放的大型对象大小

这次垃圾回收从大型对象空间回收的字节数量。

堆统计数据

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

暂停时间

一般状况下,暂停时间与垃圾回收运行时修改的对象引用数量成正比。当前,ART CMS 垃圾回收仅在垃圾回收即将完成时暂停一次。移动的垃圾回收暂停时间较长,会在大部分垃圾回收期间持续出现。

若是您在 logcat 中看到大量的垃圾回收,请注意堆统计数据的增大(上面示例中的 25MB/38MB 值)。若是此值继续增大,且始终没有变小的趋势,则可能会出现内存泄漏。或者,若是您看到缘由为“Alloc”的垃圾回收,那么您的操做已经快要达到堆容量,而且将很快出现 OOM 异常。

总体内存分配

使用下面的 adb 命令观察应用内存在不一样类型的 RAM 分配之间的划分状况:

adb shell dumpsys meminfo <package_name|pid> [-d]
复制代码

-d 标志会打印与 Dalvik 和 ART 内存使用状况相关的更多信息。

输出列出了应用的全部当前分配,单位为千字节。

检查此信息时,您应熟悉下列类型的分配:

私有(干净和脏)RAM

这是仅由您的进程使用的内存。这是您的应用进程被破坏时系统能够回收的 RAM 量。一般状况下,最重要的部分是私有脏 RAM,它的开销最大,由于只有您的进程使用它,并且其内容仅存在于 RAM 中,因此没法被分页以进行存储(由于 Android 不使用交换)。全部的 Dalvik 和您进行的原生堆分配都将是私有脏 RAM;您与 Zygote 进程共享的 Dalvik 和原生分配是共享的脏 RAM。

按比例分配占用内存 (PSS)

这表示您的应用的 RAM 使用状况,考虑了在各进程之间共享 RAM 页的状况。您的进程独有的任何 RAM 页会直接影响其 PSS 值,而与其余进程共享的 RAM 页仅影响与共享量成比例的 PSS 值。例如,两个进程之间共享的 RAM 页会将其一半的大小贡献给每一个进程的 PSS。

PSS 结果一个比较好的特性是,您能够将全部进程的 PSS 相加来肯定全部进程正在使用的实际内存。这意味着 PSS 适合测定进程的实际 RAM 比重和比较其余进程的 RAM 使用状况与可用总 RAM。

例如,下面是 Nexus 5 设备上地图进程的输出。此处信息较多,但讨论的关键点以下所示。

adb shell dumpsys meminfo com.google.android.apps.maps -d
复制代码
** MEMINFO in pid 18227 [com.google.android.apps.maps] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    10468    10408        0        0    20480    14462     6017
  Dalvik Heap    34340    33816        0        0    62436    53883     8553
 Dalvik Other      972      972        0        0
        Stack     1144     1144        0        0
      Gfx dev    35300    35300        0        0
    Other dev        5        0        4        0
     .so mmap     1943      504      188        0
    .apk mmap      598        0      136        0
    .ttf mmap      134        0       68        0
    .dex mmap     3908        0     3904        0
    .oat mmap     1344        0       56        0
    .art mmap     2037     1784       28        0
   Other mmap       30        4        0        0
   EGL mtrack    73072    73072        0        0
    GL mtrack    51044    51044        0        0
      Unknown      185      184        0        0
        TOTAL   216524   208232     4384        0    82916    68345    14570

 Dalvik Details
        .Heap     6568     6568        0        0
         .LOS    24771    24404        0        0
          .GC      500      500        0        0
    .JITCache      428      428        0        0
      .Zygote     1093      936        0        0
   .NonMoving     1908     1908        0        0
 .IndirectRef       44       44        0        0

 Objects
               Views:       90         ViewRootImpl:        1
         AppContexts:        4           Activities:        1
              Assets:        2        AssetManagers:        2
       Local Binders:       21        Proxy Binders:       28
       Parcel memory:       18         Parcel count:       74
    Death Recipients:        2      OpenSSL Sockets:        2
复制代码

下面是 Gmail 应用的 Dalvik 上一个较旧版本的 dumpsys:

** 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 分配占用的 RAM。Pss Total 包括全部 Zygote 分配(如上述 PSS 定义所述,经过进程之间的共享内存量来衡量)。Private Dirty 数值是仅分配到您应用的堆的实际 RAM,由您本身的分配和任何 Zygote 分配页组成,这些分配页自从 Zygote 派生应用进程以来已被修改。 注:在包含 Dalvik Other 部分的更新的平台版本上,Dalvik 堆的 Pss Total 和 Private Dirty 数值不包括 Dalvik 开销(例如即时 (JIT) 编译和垃圾回收记录),而较旧的版本会在 Dalvik 中将其一并列出。

Heap Alloc 是 Dalvik 和原生堆分配器为您的应用跟踪的内存量。此值大于 Pss Total 和 Private Dirty,由于您的进程从 Zygote 派生,且包含您的进程与全部其余进程共享的分配。

.so mmap 和 .dex mmap

映射的 .so(原生)和 .dex(Dalvik 或 ART)代码占用的 RAM。Pss Total 数值包括应用之间共享的平台代码;Private Clean 是您的应用本身的代码。一般状况下,实际映射的内存更大 - 此处的 RAM 仅为应用执行的代码当前所需的 RAM。不过,.so mmap 具备较大的私有脏 RAM,由于在加载到其最终地址时对原生代码进行了修改。

.oat mmap

这是代码映像占用的 RAM 量,根据多个应用一般使用的预加载类计算。此映像在全部应用之间共享,不受特定应用影响。

.art mmap

这是堆映像占用的 RAM 量,根据多个应用一般使用的预加载类计算。此映像在全部应用之间共享,不受特定应用影响。尽管 ART 映像包含 Object 实例,它仍然不会计入您的堆大小。

.Heap(仅带有 -d 标志)

这是您的应用的堆内存量。不包括映像中的对象和大型对象空间,但包括 zygote 空间和非移动空间。

.LOS(仅带有 -d 标志)

这是由 ART 大型对象空间占用的 RAM 量。包括 zygote 大型对象。大型对象是全部大于 12KB 的原语数组分配。

.GC(仅带有 -d 标志)

这是内部垃圾回收量(考虑了应用开销)。真的没有任何办法减小这一开销。

.JITCache(仅带有 -d 标志)

这是 JIT 数据和代码缓存占用的内存量。一般状况下为 0,由于全部的应用都会在安装时编译。

.Zygote(仅带有 -d 标志)

这是 zygote 空间占用的内存量。zygote 空间在设备启动时建立且永远不会被分配。

.NonMoving(仅带有 -d 标志)

这是由 ART 非移动空间占用的 RAM 量。非移动空间包含特殊的不可移动对象,例如字段和方法。您能够经过在应用中使用更少的字段和方法来减小这一部分。

.IndirectRef(仅带有 -d 标志)

这是由 ART 间接引用表占用的 RAM 量。一般状况下,此量较小,但若是很高,能够经过减小使用的本地和全局 JNI 引用数量来减小此 RAM 量。

Unknown

系统没法将其分类到其余更具体的一个项中的任何 RAM 页。当前,此类 RAM 页主要包含原生分配,因为地址空间布局随机化 (ASLR) 而没法在收集此数据时经过工具识别。与 Dalvik 堆相同,Unknown 的 Pss Total 考虑了与 Zygote 的共享,且 Private Dirty 是仅由您的应用占有的未知 RAM。

TOTAL

您的进程占用的按比例分配占用内存 (PSS) 总量。等于上方全部 PSS 字段的总和。表示您的进程占用的内存量占总体内存的比重,能够直接与其余进程和可用总 RAM 比较。 Private Dirty 和 Private Clean 是您的进程中的总分配,未与其余进程共享。它们(尤为是 Private Dirty)等于您的进程被破坏后将释放回系统中的 RAM 量。脏 RAM 是由于已被修改而必须保持在 RAM 中的 RAM 页(由于没有交换);干净 RAM 是已从某个持久性文件(例如正在执行的代码)映射的 RAM 页,若是一段时间不用,能够移出分页。

ViewRootImpl

您的进程中当前活动的根视图数量。每一个根视图都与一个窗口关联,所以有助于您肯定涉及对话框或其余窗口的内存泄漏。

AppContexts 和 Activities

您的进程中当前活动的应用 Context 和 Activity 对象数量。这能够帮助您快速肯定因为存在静态引用(比较常见)而没法进行垃圾回收的已泄漏 Activity 对象。这些对象常常拥有不少关联的其余分配,所以成为跟踪大型内存泄漏的一种不错的方式。

注:View 或 Drawable 对象也会保持对其源 Activity 的引用,所以保持 View 或 Drawable 对象也会致使您的应用泄漏 Activity。

Memory Profiler

Memory Profiler 是 Android Profiler 中的一个组件,可帮助您识别致使应用卡顿、冻结甚至崩溃的内存泄漏和流失[memory leaks and memory churn]。它显示一个应用内存使用量的实时图表[It shows a realtime graph of your app's memory use],让您能够捕获堆转储[capture a heap dump]、强制执行垃圾回收[force garbage collections]以及跟踪内存分配[track memory allocations]。

要打开 Memory Profiler,请按如下步骤操做:

点击 View > Tool Windows > Android Profiler(也能够点击工具栏中的 Android Profiler )。 从 Android Profiler 工具栏中选择您想要分析的设备和应用进程。 点击 MEMORY 时间线中的任意位置可打开 Memory Profiler。

Android 提供一个 托管内存环境(managed memory environment) —当它肯定您的应用再也不使用某些对象时,垃圾回收器会将未使用的内存释放回堆中。 虽然 Android 查找未使用内存的方式在不断改进,但对于全部 Android 版本,系统都必须在某个时间点短暂地暂停您的代码。 大多数状况下,这些暂停难以察觉。 不过,若是您的应用分配内存的速度比系统回收内存的速度快,则当收集器释放足够的内存以知足您的分配须要时,您的应用可能会延迟。 此延迟可能会致使您的应用跳帧[skip frames],并使系统明显变慢。

尽管您的应用不会表现出变慢,但若是存在内存泄漏,则即便应用在后台运行也会保留该内存。 此行为会强制执行没必要要的垃圾回收 Event,于是拖慢系统的内存性能。 最后,系统被迫终止您的应用进程以回收内存。 而后,当用户返回您的应用时,它必须彻底重启。

为帮助防止这些问题,您应使用 Memory Profiler 执行如下操做:

在时间线[timeline]中查找可能会致使性能问题的不理想的内存分配模式[undesirable memory allocation patterns]。 转储 Java 堆以查看在任何给定时间哪些对象耗尽了使用内存。 长时间进行多个堆转储可帮助识别内存泄漏。 记录正经常使用户交互和极端用户交互期间的内存分配以准确识别您的代码在何处短期分配了过多对象,或分配了泄漏的对象[allocating objects that become leaked]。

Memory Profiler 概览

当您首次打开 Memory Profiler 时,您将看到一条表示应用内存使用量的详细时间线,并可访问用于强制执行垃圾回收、捕捉堆转储和记录内存分配的各类工具。

如图 1 所示,Memory Profiler 的默认视图包括如下各项:

  1. 用于强制执行垃圾回收 Event 的按钮。
  2. 用于捕获堆转储的按钮。
  3. 用于记录内存分配状况的按钮。 此按钮仅在链接至运行 Android 7.1 或更低版本的设备时才会显示。
  4. 用于放大/缩小时间线的按钮。
  5. 用于跳转至实时内存数据的按钮。
  6. Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。
  7. 内存使用量时间线,其包含如下内容:
  • 一个显示每一个内存类别使用多少内存的堆叠图表[stacked graph],如左侧的 y 轴以及顶部的彩色键所示。
  • 虚线表示分配的对象数,如右侧的 y 轴所示。
  • 用于表示每一个垃圾回收 Event 的图标。

不过,若是您使用的是运行 Android 7.1 或更低版本的设备,则默认状况下,并非全部分析数据都可见。 若是您看到一条消息,其显示“Advanced profiling is unavailable for the selected process”,则须要 启用高级分析 以查看下列内容:

  • Event 时间线
  • 分配的对象数
  • 垃圾回收 Event

在 Android 8.0 及更高版本上,始终为可调试应用启用高级分析。

如何计算内存

您在 Memory Profiler(图 2)顶部看到的数字取决于您的应用根据 Android 系统机制所提交的全部私有内存页面数[private memory pages]。 此计数不包含与系统或其余应用共享的页面。

内存计数中的类别以下所示:

  • Java:从 Java 或 Kotlin 代码分配的对象内存。
  • Native:从 C 或 C++ 代码分配的对象内存。 即便您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,由于 Android 框架使用原生内存表明您处理各类任务[handle various tasks on your behalf],如处理图像资源和其余图形时,即便您编写的代码采用 Java 或 Kotlin 语言。
  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这一般与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。
  • Other:您的应用使用的系统不肯定如何分类的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。 (当链接至运行 Android 7.1 及更低版本的设备时,此分配仅在 Memory Profiler 链接至您运行的应用时才开始计数。 所以,您开始分析以前分配的任何对象都不会被计入。 不过,Android 8.0 附带一个设备内置分析工具,该工具可记录全部分配,所以,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。)

与之前的 Android Monitor 工具中的内存计数相比,新的 Memory Profiler 以不一样的方式记录您的内存,所以,您的内存使用量如今看上去可能会更高些。 Memory Profiler 监控的类别更多,这会增长总的内存使用量,但若是您仅关心 Java 堆内存,则“Java”项的数字应与之前工具中的数值类似。

然而,Java 数字可能与您在 Android Monitor 中看到的数字并不是彻底相同,这是由于应用的 Java 堆是从 Zygote 启动的,而新数字则计入了为它分配的全部物理内存页面。 所以,它能够准确反映您的应用实际使用了多少物理内存。

注:目前,Memory Profiler 还会显示应用中的一些误报的原生内存使用量,而这些内存其实是分析工具使用的。 对于大约 100000 个对象,最多会使报告的内存使用量增长 10MB。 在这些工具的将来版本中,这些数字将从您的数据中过滤掉。

查看内存分配

内存分配显示内存中每一个对象是_如何_分配的。 具体而言,Memory Profiler 可为您显示有关对象分配的如下信息:

  • 分配哪些类型的对象以及它们使用多少空间。
  • 每一个分配的堆叠追踪[stack trace],包括在哪一个线程中。
  • 对象在什么时候_被取消分配_(仅当使用运行 Android 8.0 或更高版本的设备时)。

若是您的设备运行 Android 8.0 或更高版本,您能够随时按照下述方法查看您的对象分配: 只需点击并按住时间线,并拖动选择您想要查看分配的区域(如视频 1 中所示)。 不须要开始记录会话,由于 Android 8.0 及更高版本附带设备内置分析工具,可持续跟踪您的应用分配。

若是您的设备运行 Android 7.1 或更低版本,则在 Memory Profiler 工具栏中点击 Record memory allocations 。 记录时,Android Monitor 将跟踪您的应用中进行的全部分配。 操做完成后,点击 Stop recording (同一个按钮;请参阅视频 2)以查看分配。

在选择一个时间线区域后(或当您使用运行 Android 7.1 或更低版本的设备完成记录会话时),已分配对象的列表将显示在时间线下方,按类名称[class name]进行分组,并按其堆计数[heap count]排序。

注:在 Android 7.1 及更低版本上,您最多能够记录 65535 个分配。 若是您的记录会话超出此限值,则记录中仅保存最新的 65535 个分配。 (在 Android 8.0 及更高版本中,则没有实际的限制。)

要检查分配记录,请按如下步骤操做:

  • 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 - 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 而后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每一个实例,如图 3 中所示。
  • 在 Instance View 窗格中,点击一个实例。 此时下方将出现 Call Stack 标签,显示该实例被分配到何处以及哪一个线程中。
  • 在 Call Stack 标签中,点击任意行以在编辑器中跳转到该代码。

默认状况下,左侧的分配列表按类名称排列。 在列表顶部,您能够使用右侧的下拉列表在如下排列方式之间进行切换:

  • Arrange by class:基于类名称对全部分配进行分组。
  • Arrange by package:基于软件包名称对全部分配进行分组。
  • Arrange by callstack:将全部分配分组到其对应的调用堆栈[Groups all allocations into their corresponding call stack]。

在分析时提升应用程序性能

为了在分析时提升应用程序性能,内存分析器默认状况下会按期对内存分配进行采样[samples ]。 在运行API级别26或更高级别的设备上进行测试时,能够使用“Allocation Tracking”下拉列表更改此行为。

可用选项以下:

  • Full:捕获内存中的全部对象分配。 这是Android Studio 3.2及更早版本中的默认行为。 若是您有一个分配了大量对象的应用程序,您可能会在分析时观察到应用程序的可见速度降低[observe visible slowdowns]。
  • Sampled:按期在内存中采样对象分配。 这是默认选项,在分析时对应用程序性能的影响较小。 在很短的时间内分配大量对象的应用程序仍然可能会出现明显的减速。
  • Off:中止跟踪应用的内存分配。

注意:默认状况下,Android Studio会在执行CPU录制时中止跟踪实时分配,并在CPU录制完成后从新打开。 您能够在CPU录制配置对话框中更改此行为。

查看全局JNI引用

Java Native Interface(JNI)是一个容许Java代码和 native code 相互调用的框架。

JNI引用由 native code 手动管理,所以 native code 使用的Java对象可能会保持活动太长时间。若是在没有先明确删除[first being explicitly deleted]的状况下丢弃JNI引用,Java堆上的某些对象可能没法访问。此外,可能耗尽[exhaust]全局JNI引用限制。

要解决此类问题,请使用Memory Profiler中的 JNI heap 视图浏览全部全局JNI引用,并按Java类型和本机调用堆栈对其进行过滤。经过此信息,您能够找到建立和删除全局JNI引用的时间和位置。

在您的应用程序运行时,选择要检查的时间轴的一部分,而后从 class list 上方的下拉菜单中选择JNI堆。而后,您能够像往常同样检查堆中的对象,而后双击 Allocation Call Stack 选项卡中的对象,以查看在代码中分配和释放JNI引用的位置,如图4所示。

要检查应用程序的JNI代码的内存分配,您必须将应用程序部署到运行Android 8.0或更高版本的设备。

捕获堆转储

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存。 特别是在长时间的用户会话后,堆转储会显示您认为不该再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。 在捕获堆转储后,您能够查看如下信息:

您的应用已分配哪些类型的对象,以及每一个类型分配多少。 每一个对象正在使用多少内存。 在代码中的何处仍在引用每一个对象。 对象所分配到的调用堆栈。(目前,若是您在记录分配时捕获堆转储,则只有在 Android 7.1 及更低版本中,堆转储才能使用调用堆栈。)

在类列表中,您能够查看如下信息:

Allocations: 堆中分配数 Native Size: 此对象类型使用的native内存总量。 此列仅适用于Android 7.0及更高版本。您将在这里看到一些用Java分配内存的对象,由于Android使用native内存来处理某些框架类,例如Bitmap。 Shallow Size: 此对象类型使用的Java内存总量 Retained Size: 所以类的全部实例而保留的内存总大小

您能够使用已分配对象列表上方的两个菜单来选择要检查的堆转储以及如何组织数据。

从左侧的菜单中,选择要检查的堆:

Default heap:系统未指定堆时。 App heap:您的应用在其中分配内存的主堆[primary heap]。 Image heap:系统启动映像[system boot image],包含启动期间预加载[preloaded]的类。 此处的分配保证毫不会移动或消失。 Zygote heap:copy-on-write heap,其中的应用进程是从 Android 系统中派生[forked]的。 从右侧菜单中选择如何排列分配:

Arrange by class:基于类名称对全部分配进行分组。 Arrange by package:基于软件包名称对全部分配进行分组。 Arrange by callstack:将全部分配分组到其对应的调用堆栈。此选项仅在记录分配[recording allocations]期间捕获堆转储[capture the heap dump]时才有效。即便如此,堆中的对象也极可能是在您开始记录以前分配的,所以这些分配会首先显示,且只按类名称列出。 默认状况下,此列表按 Retained Size 列排序。 您能够点击任意列标题以更改列表的排序方式。

在 Instance View 中,每一个实例都包含如下信息:

Depth:从任意 GC root 到所选实例的最短 hops 数。 Native Size: native内存中此实例的大小。此列仅适用于Android 7.0及更高版本。 Shallow Size:此实例Java内存的大小。 Retained Size:此实例支配[dominator]的内存大小(根据 [dominator 树](en.wikipedia.org/wiki/Domina…

要检查您的堆,请按如下步骤操做:

一、浏览列表以查找堆计数[heap counts]异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 而后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每一个实例,如图 5 中所示。

或者,您能够经过单击 Filter 或按 Control + F 并在搜索字段中输入类名或包名来快速定位对象。 也能够从下拉菜单中选择 Arrange by callstack 来按方法名称搜索。若是要使用正则表达式,请选中Regex旁边的框。若是您的搜索查询区分大小写,请选中匹配大小写旁边的框。

二、在 Instance View 窗格中,点击一个实例。此时下方将出现 References,显示该对象的每一个引用。或者,点击实例名称旁的箭头以查看其全部字段,而后点击一个字段名称查看其全部引用。 若是您要查看某个字段的实例详情,右键点击该字段并选择 Go to Instance。

三、在 References 标签中,若是您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance。 这将从堆转储中选择对应的实例,显示您本身的实例数据。

在您的堆转储中,请注意由下列任意状况引发的内存泄漏:

长时间引用 Activity、Context、View、Drawable 和其余对象,可能会保持对 Activity 或 Context容器的引用。 能够保持 Activity 实例的非静态内部类,如 Runnable。 对象保持时间超出所需时间的缓存。

将堆转储另存为 HPROF

在捕获堆转储后,仅当分析器运行时才能在 Memory Profiler 中查看数据。 当您退出分析会话时,您将丢失堆转储。 所以,若是您要保存堆转储以供往后查看,可经过点击时间线下方工具栏中的 Export heap dump as HPROF file,将堆转储导出到一个 HPROF 文件中。 在显示的对话框中,确保使用 .hprof 后缀保存文件。

而后,经过将此文件拖到一个空的编辑器窗口(或将其拖到文件标签栏中),您能够在 Android Studio 中从新打开该文件。

要使用其余 HPROF 分析器(如 jhat),您须要将 HPROF 文件从 Android 格式转换为 Java SE HPROF 格式。 您能够使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv 工具执行此操做。 运行包括如下两个参数的 hprof-conv 命令:原始 HPROF 文件和转换后 HPROF 文件的写入位置。 例如:

hprof-conv heap-original.hprof heap-converted.hprof

导入堆转储文件 要导入HPROF(.hprof)文件,请单击 Sessions 窗格中 Load from file,而后从文件浏览器中选择该文件。

您还能够经过将 HPROF 文件从文件浏览器拖到编辑器窗口中来导入HPROF文件。

分析内存的技巧

使用 Memory Profiler 时,您应对应用代码施加压力[stress your app code]并尝试强制内存泄漏。在应用中引起内存泄漏的一种方式是,先让其运行一段时间,而后再检查堆。泄漏在堆中可能逐渐汇聚到分配顶部[Leaks might trickle up to the top of the allocations in the heap]。不过,泄漏越小,您越须要运行更长时间才能看到泄漏。

您还能够经过如下方式之一触发内存泄漏:

将设备从纵向旋转为横向,而后在不一样的 Activity 状态下反复操做屡次。 旋转设备常常会致使应用泄漏 Activity、Context、View 对象,由于系统会从新建立 Activity,而若是您的应用在其余地方保持对这些对象之一的引用,系统将没法对其进行垃圾回收。 处于不一样的 Activity 状态时,在您的应用与另外一个应用之间切换(导航到主屏幕,而后返回到您的应用)。

MAT

MAT,Memory Analyzer Tool,它能够帮助咱们查找内存泄漏和查看内存消耗状况。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工做,并能够经过报表直观的查看到可能形成这种结果的对象。

能够在应用内使用 android.os.Debug.dumpHprofData(String fileName) 接口主动抓取应用的内存堆栈。只须要在程序运行时调用 dumpHprofData(String fileName) 静态接口便可将应用内存导出到文件。以下是一段在应用异常退出时抓取内存的示例代码仅供参考:

public class CaptureHeapDumpsApplication extends Application {
    private static final String FILE_NAME = "/data/local/tmp/heap-dump.hprof";

    @Override
    public void onCreate() {
        super.onCreate();
        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                String absolutePath = new File(FILE_NAME).getAbsolutePath();
                try {
                    Debug.dumpHprofData(absolutePath);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        });
    }
}
复制代码

获取及打开 .hprof 文件

使用MAT既能够打开一个已有的堆快照,也能够经过MAT直接从活动Java程序中导出堆快照。

HPROF文件是MAT能识别的文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不一样的格式来存储这些数据,总的来讲包含了快照被触发时java对象和类在heap中的状况。因为快照只是一瞬间的事情,因此heap dump中没法包含一个对象在什么时候、何地(哪一个方法中)被分配这样的信息。

若是HPROF文件是经过AndroidStudio的profile工具导出的,因为这个不是 mat 工具用到的标准文件,咱们须要使用 sdk 自带的platform-tools/hprof-conv.exe工具进行转换,命令为:

hprof-conv -z 1.hprof 1_mat.hprof

注意:最好将.hprof文件放在一个单独的文件夹内打开,由于你在操做过程当中,会生成大量的临时文件。

工具栏

Overview:主界面

Histogram:直方图

Dominator Tree:支配树

OQL:Object Query Language studio

Thread OvewView:查看这个应用全部的Thread信息

Run Expert System Test:运行专家系统测试

Query Browser:查询浏览器

Find Object By Address

Group:在Histogram和Domiantor Tree界面,能够选择将结果用另外一种Group的方式显示(默认是Group by Object),切换到Group by package能够更好地查看具体是哪一个包里的类占用内存大,也很容易定位到本身的应用程序。

Calculate Retained Size:点击后,会出现Retained Size这一列

主界面 Overview

咱们须要关注的是下面Actions区域,介绍4种分析方法:

Histogram: Lists number of instances per class 列出内存中的对象,对象的个数以及大小

Dominator Tree: List the biggest objects and what they keep alive. 列出最大的对象以及其依赖存活的Object,大小是以Retained Heap为标准排序的

Top Consumers: Print the most expensive objects grouped by class and by package. 经过图形列出最大的object

Duplicate Classes: Detect classes loaded by multiple class loaders. 经过MAT自动分析泄漏的缘由

default_report 窗口

该窗口列出了可能有问题的代码片断。点击每一个问题中的Details能够查看相关的详情。

详情页面包含以下内容

Description:问题简要描述 Shortest Paths To the Accumulation Point:在此列表中,咱们能够追溯到问题代码的类树的结构,并找到本身代码中的类。 Accumulated Objects in Dominator Tree:在此列表中,咱们能够看见建立的大量的对象 Accumulated Objects by Class in Dominator Tree:在此列表中,咱们能看见建立大量对象相关的类。 All Accumulated Objects by Class:在此列表中,会按类别划分的全部累计对象。

两个重要概念

Shallow heap:自己占用内存 Shallow size就是对象自己占用内存的大小,不包含其引用的对象。

常规对象(非数组)的Shallow size由其成员变量的数量和类型决定 数组类型的对象的shallow size由数组元素的类型(对象类型、基本类型)和数组长度决定 注意:由于不像c++的对象自己能够存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],因此咱们若是只看对象自己的内存,那么数量都很小。因此咱们看到 Histogram 图是以Shallow size进行排序的,排在第一位的通常都是byte[]。

Retained Heap:引用占用内存 Retained Heap的概念,它表示若是一个对象被释放掉,那么该对象引用的全部对象,包括被递归引用的对象,被释放的内存。

例如,若是一个对象的某个成员new了一大块int数组,那这个int数组也能够计算到这个对象中。与shallow heap比较,Retained heap能够更精确的反映一个对象实际占用的大小,由于若是该对象释放,retained heap均可以被释放。

可是,Retained Heap并不老是那么有效。 例如,我在A里new了一块内存,赋值给A的一个成员变量,同时我让B也指向这块内存。此时,由于A和B都引用到这块内存,因此A释放时,该内存不会被释放。因此这块内存不会被计算到A或者B的Retained Heap中。

为了纠正这点,MAT中的 Leading Object(例如A或者B)不必定只是一个对象,也能够是多个对象。此时,(A,B)这个组合的Retained Set就包含那块大内存了。对应到MAT的UI中,在Histogram中,能够选择 Group By class, superclass or package来选择这个组。

为了计算Retained Memory,MAT引入了Dominator Tree。

例如,对象A引用B和C,B和C又都引用到D,计算Retained Memory时:

A的包括A自己和B,C,D。 B和C由于共同引用D,因此B,C 的Retained Memory都只是他们自己。 D固然也只是本身。 在这里例子中,树根是A,而B,C,D是他的三个儿子,B,C,D再也不有相互关系。

我以为是为了加快计算的速度,MAT将对象引用图转换成对象引用树。把引用图变成引用树后,计算Retained Heap就会很是方便,显示也很是方便。对应到 MAT UI 上,在 dominator tree 这个view中,显示了每一个对象的 shallow heap 和 retained heap。而后能够以该节点为树根,一步步的细化看看 retained heap 究竟是用在什么地方了。

这种从图到树的转换确实方便了内存分析,但有时候会让人有些疑惑。原本对象B是对象A的一个成员,但由于B还被C引用,因此B在树中并不在A下面,而极可能是平级。

为了纠正这点,MAT中点击右键,能够 List objects 中选择 with outgoing references 和 with incoming references。这是个真正的引用图的概念,

outgoing references :表示该对象的出节点(被该对象引用的对象) incoming references :表示该对象的入节点(引用到该对象的对象) 为了更好地理解 Retained Heap,下面引用一个例子来讲明:

把内存中的对象当作下图中的节点,而且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain(引用链)的起点:

上图中蓝色节点表明仅仅只有经过obj1才能直接或间接访问的对象。由于能够经过GC Roots访问,因此上图的obj3不是蓝色节点。所以上图中obj1的retained size是obj一、obj二、obj4的shallow size总和。

上图obj1的retained size是obj一、obj二、obj三、obj4的shallow size总和。而obj2的retained size是obj三、obj4的shallow size总和。

Histogram 和 Dominator Tree

Histogram的主要做用是查看一个instance的数量,通常用来查看本身建立的类的实例的个数。

能够很容易的找出占用内存最多的几个对象,根据百分比(Percentage)来排序。

能够分不一样维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package

Dominator Tree和Histogram的区别是站的角度不同,Histogram是站在类的角度上去看,Dominator Tree是站的对象实例的角度上看,Dominator Tree能够更方便的看出其引用关系。

经过查看Object的个数,结合代码就能够找出存在内存泄露的类(便可达可是无用的对象,或者是能够重用可是从新建立的对象)

Histogram中还能够对对象进行Group,更方便查看本身Package中的对象信息。

右键菜单:Query Browser

Search Queries:搜索列出全部Queries选项的具体含义,包含搜索区、输入关键字后匹配的Queries选项列表区,点击选项后的具体含义解释区。

能够看到,全部的命令其实就是配置不一样的SQL查询语句,好比咱们最经常使用的:

List objects -> with incoming references:查看这个对象持有的外部对象引用 List objects -> with outcoming references:查看这个对象被哪些外部对象引用 Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用。从GC上说,除了强引用外,其余的引用在JVM须要的状况下是均可以 被GC掉的,若是一个对象始终没法被GC,就是由于强引用的存在,从而致使在GC的过程当中一直得不到回收,所以就内存溢出了。 Merge Shortest path to GC root:找到从GC根节点到一个对象或一组对象的共同路径

systrace

systrace 命令能够容许咱们在系统层面上,对设备里全部进程的耗时信息进行收集和调研。systrace 结合的分析数据来源于 Android 内核,如 CPU 调度器、磁盘活动和 App 的线程等,会生成一份网页报告以下所示:

这是一份简单的 systrace 网页报告,显示了和 App 5 秒钟的交互,报告中高亮的帧是 systrace 认为没有合理渲染的地方。

事实上,生成的报告在给定的时间区间,描述了 Android 设备系统进程的全局图。同时,报告检查了抓到的 tracing 信息,并高亮化其观察到的问题,如展现动做或动画时的 UI 卡顿,会提供一些如何修复的建议。此外,systrace 也有它自身的局限性,其没法收集到 App 进程内的代码执行信息。为了获取更多的详细信息,如 App 运行时正在执行的方法、App 使用的 CPU 资源等,使用 Android Studio 内置的 CPU profiler,或生成的 trace 日志,而后使用 Traceview 查看。

具体使用

用户界面调用

打开Android studio,Tools -- Android -- Android Device Monitor,或则直接在sdk中的tools中找到Android Device Monitor打开

在弹出的Android Device Monitor中,左侧Devices选项卡的下面一排,点击下图中圈出的位置

配置trace信息,点击OK后,会在对应目录下生成html文件

命令

为了生成所须要的网页报告,须要在命令行使用下面的命令运行 systrace,即进入android-sdk/platform-tools/systrace/后,执行:

python systrace.py [options] [categories]
复制代码

举个例子,下面运行 systrace 来记录 10 秒期间设备的进程,包括图像进程,而后生成一份命名为 mynewtrace 的网页报告,执行:

python systrace.py --time=10 -o mynewtrace.html gfx
复制代码

若没指定任何类别或者选项,systrace 会生成一份报告,包括全部可用的种类,同时使用默认的设置。

通用的选项

命令和命令选项

-e <DEVICE_SERIAL> 或 --serial=<DEVICE_SERIAL> 指定设备的序列号

-d 或 --disk 追踪activity disk的输入和输出,须要root设备

-i 或 --cpu-idle 追踪CPU的idel事件

-l 或 --cpu-load 追踪CPU的加载

-s或--no-cpu-sched 防止CPU调度的追踪,经过下降trace buffer的速率达到加长的trace时长的目的

-u 或 --bus-utilization 追踪bus的使用,须要root设备

-w 或 --workqueue 追踪work queue,须要root设备

对于上面的--set-tags配置,可选项以下:

gfx 图形图像

input 输入

view 视图

webview

wm Window Manager

am Activity Manager

sync Synchronization Manager

audio

video

camera

注意:设置tag后,须要重启framework(’adb shell stop;adb shell start‘)暴躁配置生效。

调研 UI 的性能问题

systrace 尤为在调研 App 的 UI 性能上表现突出,由于其能够分析代码和帧率,来断定问题出现的区域,并给出可能的建议。大体步骤以下:

连上设备,运行 App

在android-sdk/platform-tools/systrace/路径下,执行跑 systrace 的命令如:

python systrace.py view --time=10
复制代码

手动与 App 交互,如滑动等。10 秒后,systrace 会生成一份网页报告

使用浏览器打开生成的网页报告

能够点击报告,来查看记录期间设备 CPU 的使用。下面给出怎样在报告中调研信息,来找到并修复 UI 的性能问题。

审查帧和警告

以下图所示,报告中列出了渲染 UI 帧的每一个进程,显示了时间线里每幅渲染的帧。绿色帧圈的代表,在要求的 16.6 毫秒内保持稳定的 60 帧/秒渲染帧像;黄色或红色帧圈的代表,渲染帧像时花费了超过 16.6 毫秒。

注意,在 Android 5.0 (API level 21) 及更高的设备上,渲染帧像的工做被分隔于 UI 主线程和渲染线程。之前的版本,建立帧像的工做都在 UI 主线程完成。

点击帧圈能够高亮化,提供系统完成渲染帧像额外的信息,包括警告。它也展现渲染帧像时系统正在执行的方法,所以能够根据这些方法来找出 UI 卡顿的缘由。选择有问题的帧,trace 报告下面会展现问题详情的一个 alert。

一目了然,trace 中会给出相关事件的连接,来解释在此期间系统运行的详情。

要看 trace 中工具发现的 alert,以及设备触发每一个警告的次数,能够点击右上边上的 Alerts tab。Alerts 栏能够显示 trace 里发生的每个问题和它们致使卡顿的频次。考虑到栏目里一系列待修复的 bugs,一片区域里,一个微小的变化或改进,能够忽略 App 中整个类的警告。

若看到 UI 主线程中作了太多的工做,须要咱们本身找出消耗 CPU 时间的那些方法,方法之一是在认为致使性能瓶颈的地方,添加 trace 标记,来查看 trace 中出现的调用方法。若不肯定哪些方法或许致使 UI 主线程的瓶颈,使用 Android Studio 内置的 CPU profiler,或生成的 trace 日志,而后使用 Traceview 查看。

植入你 App 的代码

因为 systrace 只在系统层级显示进程的信息,所以很难经过网页报告知道给定的时间内,App 究竟执行了哪些方法。在 Android 4.3 (API level 18) 及以上,能够在代码中使用 Trace 类来在网页报告中标记执行的事件。事实上,不须要植入代码中,用 systrace 记录 traces,这样作能帮助咱们定位,代码的哪一块也许是形成线程阻塞或 UI 卡顿的缘由。这个方法和 Debug 类不一样,Trace 类只是简单地在 systrace 报告中添加标记,而 Debug 类经过生成 .trace 文件帮助咱们审查使用 App 时 CPU 详细的使用信息。

注意,为了生成包括 trace 事件的 systrace 网页报告,须要指定 -a 或 - -app 的命令来运行 systrace,这样来指定 App 的包名。

下面给出如何使用 Trace 类来标记执行方法的示例,包括两个嵌套的代码块:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
    ...
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
			// try...catch 语句中, 老是要调用 endSection(), 在 finally 语句
          	// 中调用以确保即便抛出异常时 endSection() 也能执行
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }
...
}
复制代码

注意,当屡次调用 beginSection() 时,调用 endSection() 只终止最近一次调用的 beginSection() 方法。所以,对于嵌套调用,如上面的代码所示,确保每次调用 beginSection(),一样恰当地调用到 endSection()。此外,不能在一个线程调用 beginSection(),而后从另外一个线程终止,而应该在相同的线程调用到 endSection() 方法。

了解 systrace

在开发应用时,一般使用60fps的帧率来检测交互是否流畅,若是中途出错了,或者发生了掉帧,解决这个问题的第一步应当是搞清楚当前系统在作什么。

systrace工具能够在程序运行的时候收集实时的信息,记录时间以及CPU的分配状况,记录每一个线程和进程在任意时间的运行状况,能够自动分析出一些重要的缘由,而且给出建议。

预览

Systrace能够帮助你分析应用是如何设备上运行起来的,它将系统和应用程序线程集中在一个共同的时间轴上,分析systrace的第一步须要在程序运行的时间段中抓取trace log,在抓取到的trace文件中,包含了这段时间中你想要的关键信息,交互状况。

图1显示的是当一个app在滑动时出现了卡顿的现象,默认的界面下,横轴是时间,纵向为trace event,trace event 先按进程分组,而后再按线程分组.

从上到下的信息分别为Kernel,SurfaceFlinger,应用包名。经过配置trace的分类,能够根据配置状况记录每一个应用程序的全部线程信息以及trace event的层次结构信息。

systrace 的工做原理

systrace是一个分析android性能问题的基础工具,但本质上是其余某些工具的封装,包括:PC端的atrace,设备端的可执行文件(用于控制用户控件的追踪以及配置ftrace,即Linux内核中的主要跟踪机制)。Systrace使用atrace开启追踪,而后读取ftrace的缓存,而且把它从新转换成HTML格式。

ftrace相比与systrace和atrace具备更多的功能,而且包含一些对调试性能问题相当重要的高级功能。 (这些功能须要root权限)

运行systrace

./systrace.py sched freq idle am wm gfx view sync binder_driver irq workq input -b 96000
复制代码

当systrace与GPU和显示管道活动所需的附加跟踪点结合使用时,您能够跟踪全部从用户输入到屏幕显示的帧。设置大的缓冲区能够避免事件的丢失(一般表现为某些CPU在跟踪中的某个点以后没有任何事件)。

当使用systrace时,注意每一个事件都是在CPU上触发的。

注意:硬件中断不受CPU控制也不会在ftrace中触发事件,实际提交到跟踪日志是由中断处理程序完成的,但一些损坏的驱动会形成中断的延迟,所以最关键点因素仍是CPU自己。

由于systrace构建在ftrace之上,ftrace在CPU上运行,因此硬件改变log必然会写入到的ftrace缓冲区。这就意味着若是你好奇为何显示栏改变了,那么你能够看CPU在该转换点上的运行内容(CPU上运行的事件均被保存到log中)。这个概念是使用systrace分析性能的基础。

例子:working frame

这是一个描述正常UI管道过程的systrace,请事先下载好zip文件,点击下载zip文件,解压并在浏览器中打开systrace_tutorial.html,注意:这个文件要比通常的html文件大得多。

对于一个持续的按期的工做负载,例如TouchLatency,UI管道,一般包含如下阶段:

SurfaceFlinger中的EventThread唤醒了应用程序UI线程,代表如今是渲染新帧的时候了。

应用程序使用CPU和GPU资源在UI线程,RenderThread和hwuiTasks中渲染帧。这部分暂UI的大部分。

应用程序经过binder将绘制好的帧发送到SurfaceFlinger并进入睡眠状态。

SurfaceFlinger中的第二个EventThread唤醒SurfaceFlinger来触发组合和显示输出。若是SurfaceFlinger肯定没有任何工做要完成,它将返回睡眠状态。

SurfaceFlinger经过HWC / HWC2或GL处理组合。 HWC / HWC2组合更快,更低的功耗,但会受到SOC的限制。这一步一般须要4-6ms,可是 能够与步骤2重叠,由于Android应用程序老是三重缓冲。 (虽然应用程序老是三重缓冲,但在SurfaceFlinger中只能有一个待处理帧,所以和双重缓存差很少。)

SurfaceFlinger经过供应商驱动程序调度最终输出,并返回睡眠状态,等待EventThread唤醒。

让咱们从15409ms开始查看帧:

正常的UI以及EventThread

图1是正常的帧(对应阶段1),要了解UI管道如何工做这是一个很好的示范。 TouchLatency的UI线程行在不一样时间包含不一样的颜色。 不一样的线表明线程的不一样状态:

灰色: 睡眠。

蓝色: 能够运行(它能够运行,但还未被调度运行)。

绿色: 正在运行(调度程序认为它正在运行)。

注意:中断处理程序没有CPU时间轴中显示,所以在线程运行的过程当中虽然没有显示中断,但实际上你可能已经执行了中断或者softirqs,最终须要经过检查trace(进程0)来判断中断是否发生。

红色: 不间断的睡眠(一般发生在内核锁上), 指出I / O负载,对于性能问题的调试很是有用。

橙色: 因为I / O负载致使的不间断睡眠。 要查看不间断睡眠的缘由(可从sched_blocked_reason跟踪点获取),请选择红色不间断睡眠切片。

当EventThread正在运行时,TouchLatency的UI Thread就变成了能够运行的蓝色。 要查看是什么,请点击蓝色部分:

TouchLatency的UI线程

图2(对应阶段1) 显示的是EventThread运行后,TouchLatency的tid6843唤醒UIThread为其工做,这一个信息能够从下方的“wakeup from tid:6843”看出

(对应阶段2)UI线程被唤醒,开始渲染帧,而后插入SurfaceFlinger的绘制帧队列

若是'binder_drivier'tag被开启后,你能够选择binder transaction来查看整个过程。

对应阶段3) Binder transaction

图4(对应阶段3) Binder transaction 如图4所示,在15423ms处,SurfaceFlinger的Binder:6832_1因为pid9579的调用变为了可运行状态。在binder tracsaction的两侧你也能够看到缓冲队列。 在SurfaceFlinger的queueBuffer中,TouchLtency的待绘制帧数量从1变为了2.

对应阶段3) 待处理帧从1到2

图5显示了三重缓冲,其中有两个完整的帧,应用程序将很快开始渲染第三个帧。 这是由于咱们已经删除了一些帧,因此应用程序会保留两个挂起的帧而不是一个帧,以免跳帧。

随后,SurfaceFlinger的主线程被第二个EventThread唤醒,所以它能够将的待处理帧输出到显示器:

阶段4) SurfaceFlinger的主线程由第二个EventThread唤醒

SurfaceFlinger首先锁定较早的待绘制缓冲区,这将致使挂起的缓冲区数从2减为1:

(阶段4)SurfaceFlinger绘制最先的待绘制缓冲帧

锁定缓冲区后,SurfaceFlinger进行组装并显示新帧。

阶段5). SurfaceFlinger进行组装并提交最终的框架

接下来,'mdss_fb0'在CPU 0上唤醒。'mdss_fb0'是显示管道的内核线程,用于将渲染的帧输出到显示器。 咱们能够看到’mdss_fb0‘的信息(向下滚动查看)。

(阶段6)'mdss_fb0'唤醒CPU0

使用 Systrace

在介绍使用以前,先简单说明一下Systrace的原理:它的思想很朴素,在系统的一些关键链路(好比System Service,虚拟机,Binder驱动)插入一些信息(我这里称之为Label),经过Label的开始和结束来肯定某个核心过程的执行时间,而后把这些Label信息收集起来获得系统关键路径的运行时间信息,进而获得整个系统的运行性能信息。Android Framework里面一些重要的模块都插入了Label信息(Java层的经过android.os.Trace类完成,native层经过ATrace宏完成),用户App中能够添加自定义的Label,这样就组成了一个完成的性能分析系统。

TraceView试图收集某个阶段全部函数的运行信息(sampling的也是基于此思路),它但愿在你并不知道哪一个函数有问题的时候直接定位到关键函数;但惋惜的是,收集全部信息 这个是不现实的,它的运行时开销严重干扰了运行环境;一我的犯罪了,你要把全国人民都抓起来审问一遍吗?Systrace的思路是反过来的,在不清楚问题的状况下,你压根儿没法下手,只有掌握了一些基本的信息,经过假设-分析-验证 的过程一步一步找出问题的缘由;TraceView那种一招吃遍天下鲜的方式,讲道理是不符合科学依据的(固然在特定的场合TraceView有他的用途)。

首先,在手机端准备好你须要分析的过程的环境;好比假设你要分析App的冷启动过程,那就先把App进程杀掉,切换到Launcher中有你的App 图标的那个页面,随时准备点击图标启动进程;假设你要分析某个Activity的卡顿状况,那就先在手机上进入到上一个Activity,随时准备点按钮切换到待分析的Activity中。

由于Systrace没办法自由滴控制开始和结束(下面有一个办法能够缓解),而trace获得的数据有可能很是多,所以咱们须要手工缩小须要分析的数据集合;否则你可能被一堆眼花缭乱的数据和图像弄得晕头转向,而后什么有用的结论也分析不出来。记住哦,手动缩小范围,会帮助你加速收敛问题的分析过程,进而快速地定位和解决问题。

./systrace.py -t 10 sched gfx view wm am app webview -a <package-name>
复制代码

这样,systrace.py 这个脚本就经过adb给手机发送了收集trace的通知;与此同时,切换到手机上进行你须要分析的操做,好比点击Launcher中App的Icon启动App,或者进入某个Activity开始滑动ListView/RecyclerView。通过你指定的时间以后(以上是10s),就会有trace数据生成在当前目录,默认是 trace.html;用Chrome浏览器打开便可。

如上文所述,systrace没有办法在代码中控制Trace运行的开始和结束;那么,若是咱们要分析App的启动性能,我点了桌面图标,把Trace时间设置为10s,我怎么知道这10s中,哪段时间是我App的启动过程?若是不知道咱们须要分析的时间段,那后续不是扯淡么?

咱们能够用自定义Trace Label解决;Android SDK中提供了android.os.Trace#beginSectionandroid.os.Trace#endSection 这两个接口;咱们能够在代码中插入这些代码来分析某个特定的过程;好比咱们以为Fragment的onCreateView过程有问题,那就在onCreateView 中加上代码:

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            Bundle savedInstanceState) {
    Trace.beginSection("Fragement_onCreateView");
    // .. 其余代码
    // ...
    // .. 结束处
    Trace.endSection();
}
复制代码

这样,在Trace的分析结果中就会带上Fragement_onCreateView 这个过程的运行时间段信息(固然你得开启 -a 选项!),以下:

咱们能够在任意本身感兴趣的地方添加自定义的Label;通常来讲,分析过程就是,你怀疑哪里有问题,就在那那个函数加上Label,运行一遍抓一个Trace,看看本身的猜想对不对;若是猜想正确,进一步加Label缩小范围,定位到具体的自定义函数,函数最终调用到系统内部,那就开启系统相关模块的Trace,继续定位;若是猜想错误,那就转移目标,一步步缩小范围,直至问题收敛。

回到正题,咱们如何控制Systace的开始和结束?事实上这是无法办到的,不过控制开始和结束的目的是什么?其实就是获得开始和结束这段时间内的Trace信息。要达到这个目的,咱们只须要在指望开始和结束的地方加上自定义的Label就能够了。好比你要分析App的冷启动过程,那就在Application类的attachBaseContext调用Trace.beginSection("Boot Procedure"),而后在App首页的onWindowFocusChanged 或者你认为别的合适的启动结束点调用Trace.endSection就能够到启动过程的信息;好比下图是个人Label:

从bindApplication到activityStart,到Phase2,Phase3;这几个过程组合就是我感兴趣的启动过程。从图中能够直观地看出来,从Application到activityStart占用了启动一半的时间,activityStart下面那有一大段空白是在干什么?这是个问题。

systrace官方文档说待trace的App必须是debuggable的,可是官方又说,debuggable的App与非debuggable的性能有较大差异;由于系统为了支持debug开启了一些列功能而且关闭掉了某些重要的优化。

若是咱们想要待分析的App尽量接近真实状况,那么必需要在非Debug的App中能启用systrace功能;由于相同状况下Debug的App性能比非Debuggable的差,你没法确保在debuggable版本上分析出来的结论能准确推广到非debuggable的版本上。

分析systrace源码以后 ,发现这个条件只是个障眼法而已;咱们能够手动开启App的自定义Label的Trace功能,方法也很简单,调用一个函数便可;可是这个函数是SDK @hide的,咱们须要反射调用:

Class<?> trace = Class.forName("android.os.Trace");
Method setAppTracingAllowed = trace.getDeclaredMethod("setAppTracingAllowed", boolean.class);
setAppTracingAllowed.invoke(null, true);
复制代码

把这段代码放在Application的attachBaseContext 中,这样就能够手动开启App自定义Label的Trace功能,在非debuggable的版本中也适用!

MAT 使用

内存优化

  1. UI 不可见时释放资源
  • 在 onStop 中关闭网络链接、注销广播接收器、释放传感器等资源;
  • 在 onTrimMemory() 回调方法中监听 TRIM_MEMORY_UI_HIDDEN 级别的信号,此时可在 Activity 中释放 UI 使用的资源,大符减小应用占用的内存,从而避免被系统清除出内存。
  1. 内存紧张时释放资源

运行中的程序,若是内存紧张,会在 onTrimMemory(int level) 回调方法中接收到如下级别的信号:

  • TRIM_MEMORY_RUNNING_MODERATE:系统可用内存较低,正在杀掉 LRU 缓存中的进程。你的进程正在运行,没有被杀掉的危险。
  • TRIM_MEMORY_RUNNING_LOW:系统可用内存更加紧张,程序虽然暂没有被杀死的危险,可是应该尽可能释放一些资源,以提高系统的性能(这也会直接影响你程序的性能)。
  • TRIM_MEMORY_RUNNING_CRITICAL:系统内存极度紧张,而 LRU 缓存中的大部分进程已被杀死,若是仍然没法得到足够的资源的话,接下来会清理掉 LRU 中的全部进程,而且开始杀死一些系统一般会保留的进程,好比后台运行的服务等。关于进程的优先级,参考这里(developer.android.com/guide/compo…

当程序未在运行,保留在 LRU 缓存中时, onTrimMemory(int level) 中会返回如下级别的信号:

  • TRIM_MEMORY_BACKGROUND:系统可用内存低,而你的程序处在 LRU 的顶端,所以暂时不会被杀死,可是此时应释放一些程序再次打开时比较容易恢复的 UI 资源。
  • TRIM_MEMORY_MODERATE:系统可用内存低,程序处于 LRU 的中部位置,若是内存状态得不到缓解,程序会有被杀死的可能。
  • TRIM_MEMORY_COMPLETE:系统可用内存低,你的程序处于 LRU 尾部,若是系统仍然没法回收足够的内存资源,你的程序将首先被杀死。此时应释放无助于恢复程序状态的全部资源。

注:该 API 在版本 14 中加入。旧版本的 onLowMemory() 方法,大体至关于 onTrimMemory(int level) 中接收到 TRIM_MEMORY_COMPLETE 级别的信号。

另:尽管系统主要按照 LRU 中顺序来杀进程,不过系统也会考虑程序占用的内存多少,那些占用内存高的进程有更高的可能性会被首先杀死。

  1. 肯定你的程序应该占用多少内存

能够经过 getMemoryClass()来获取你的程序被分配的可用内存,以 M 为单位。

你能够经过在 标签下将 largeHeap 属性设为 true 来要求更多的内存,这时经过 getLargeMemoryClass() 方法来获取可用内存。

大部分应用程序不须要使用此功能,所以使用该标签前,确认你的程序是否真的须要更多内存。使用更多内存会对整个系统的性能产生影响,并且当程序进入 LRU 时会更容易首先被系统清理掉。

  1. 正确使用 Bipmap,避免浪费内存

若是你的 ImageViwe 的尺寸只有 100 * 100,那么没有必要将一张 2560 * 1600 的图片整个加载入内存。关于如何加载图片,参考这里(developer.android.com/topic/perfo…)。

  1. 使用 Android 提供的优化过的数据结构

如 SparseArray, SparseBooleanArray, LongSparseArray 等,相比 Java 提供的 HashMap,这些结构更节省内存。

  1. 始终对内存使用状况保持关注
  • 枚举类型 Enum 会比静态常量占用更多的内存;
  • Java 中每一个类(包括匿名内部类)都占用至少 500 字节左右的代码;
  • 每一个类的实例会在 RAM 中占用大约 12 ~ 16 字节的内存;
  • 每向 HashMap 中添加一个 Entry 时,新生成的 Entry 占用大约 32 个字节。
相关文章
相关标签/搜索