Understanding Android memory usage (Google I/O '18)

视频观看笔记,如需转载,请注明出处


演讲人介绍

Rechard Uhler,Android Runtime 开发工程师。为便于写作,笔者将以第一人称视角对视频内容进行概述。

Understanding Android memory usage (Google I/O '18)

视频地址

1. 前言

理解 Android App 内存占用对 App 的性能优化以及在低内存手机上良好运行都很重要。不同的内存类型,包括 shared memory,dex memory 以及 gpu memory 都会对用户体验产生影响。本视频介绍 App 如何评估以及优化内存占用,以帮助开发者们能够优化自己的应用。

我在过去的三年时间里,都在致力于更好的理解 Android 内存占用。那么为什么作为一名 App 开发工程师,也要关注内存占用呢?对我而言,主要是因为 Android 生态系统。如果一个 Android App 在低内存设备(入门级手机)上运行时用户体验不好,比如卡顿什么的,那么 OEM(Original Entrusted Manufacture) 就不愿再生产这样的设备,这部分用户就会被排除在 Android 生态系统之外。

2. 课题内容介绍

本次课题主要讨论三点内容:

  • 低内存时 Android 系统的工作机制极其对用户的影响

  • 如何评估 App 内存占用以及影响评估的几个重要因素

  • 一些减少 App 内存占用的建议

3. 低内存时 Android 系统的工作机制

首先介绍物理内存的概念,然后引入 Android Low Memory Killer。

3.1 设备物理内存

设备的物理内存被分为很多页(Page),每页 4KB。不同的页用来做不同的事情:

Understanding Android memory usage (Google I/O '18)

橘色的是已使用页,黄色的是缓存页(数据在磁盘上有备份,所以 Cache Pages 是可以被回收的),绿色的是空闲页。

kswapd

这是一个 2G 内存的手机,X 轴表示使用时间,Y 轴表示内存使用情况。随着打开的应用越来越多,Used Pages 也越来越多,而 Cached Pages 和 Free Pages 则越来越少。当 Free Pages 低于 kswapd 的阈值时,Linux 内核就会通过 kswapd 进程对 Cached Pages 进行回收。当应用再次访问 Cached Pages 上的内容时,就需要从磁盘上重新加载。如果 Cached Pages 太少的话,设备就可能死机:

Understanding Android memory usage (Google I/O '18)

所以,在 Android 上我们有个机制叫 Low Memory Killer,当 Cached Pages 太少时,就会被触发。它的工作方式是挑一个进程杀掉,然后该进程占用的所有内存都会被回收。

3.2 Low Memory Killer

如果 LMK 杀掉的是用户关心的进程,那体验就非常不好,所以我们搞了一个表(由 SystemServer 进程维护),根据这张表来决定谁先被杀掉:

Understanding Android memory usage (Google I/O '18)

Perceptible 指的是非直接交互的进程,比如后台放歌的播放器进程。Previous 指的是切换到当前与用户交互的上一个应用进程。Cached 指缓存的进程,可能是退至后台的应用,也可能是已经退出的应用,主要是为了实现应用间的快速切换。Cached 也是最先被 LMK 杀掉的进程列表:

Understanding Android memory usage (Google I/O '18)

如上图所示,当已用内存超过 LMK 阈值时,LMK 将从 Cached 列表底部开始杀进程。如果可用内存还是不足,那么就按照上表一种向上杀,直到 SystemServer,此时手机会直接重启。

所以,你可以想象 LMK 在低内存手机上的情景:

Understanding Android memory usage (Google I/O '18)

如上图所示,LMK 将一直处于活跃状态,具体表现就是黑屏、桌面重启,应用打不开等等。如此,OEM 将不愿生产这些设备。

4. 如何评估 App 的内存占用

那么,我们怎么知道 App 使用了多少内存呢?

4.1 物理内存追踪

之前提到,设备的物理内存被分为很多页(Page),Linux Kernel 将会持续跟踪每个进程使用的 Pages,所以只要对进程使用的 Pages 进行计数即可:

Understanding Android memory usage (Google I/O '18)

但实际情况远比这要复杂的多,因为有些 Pages 是进程间共享的:

Understanding Android memory usage (Google I/O '18)

那么这部分的内存要怎么计算呢?

(1)RSS(Resident Set Size):App 完全负责:

Understanding Android memory usage (Google I/O '18)

(2)PSS(Proportional Set Size):App 按比例负责,比如下图所示两个进程共享,那就负责一半。如果三个进程共享,那就负责三分之一:

Understanding Android memory usage (Google I/O '18)

(3)USS(Unique Set Size):App 无责:

Understanding Android memory usage (Google I/O '18)

但实际上,至少需要系统级别才能知道 RSS 与 USS 的情况。所以通常都是使用 PSS 来计算,这还可以避免多记或者少记 Shared Pages。你可以使用:

adb shell dumpsys meminfo -s [process] 

命令来查看一个进程的 PSS 使用情况:

Understanding Android memory usage (Google I/O '18)

4.2 你的 App 应该使用多少内存?

如果你的 App 想要的功能越多,想要的页面越炫酷,那么你的 App 就需要更多的内存。既想马儿跑,又想马儿不吃草的事情是不存在的:

Understanding Android memory usage (Google I/O '18)

App 内存占用的影响因素

(1)应用使用场景:很好理解,哪个页面比较炫、动效多、或者使用了 webview,那这个时候 App 占用的内存就高:

Understanding Android memory usage (Google I/O '18)

(2)平台配置:很好理解,比如手机的分辨率越高,相同 dp 的图片占用的内存就越大,所以高档手机上,App 的内存占用肯定比低档手机高:

Understanding Android memory usage (Google I/O '18)

(3)设备内存压力:设备内存越紧张,越可能触发 GC,导致 App 占用内存比设备内存充裕时低:

Understanding Android memory usage (Google I/O '18)

所以,你应当在相同的内存压力下评估你的 App 内存占用:

Understanding Android memory usage (Google I/O '18)

由于内存压力不好控制,所以建议评估前,先一键清理所有进程,然后再测试。

5. 一些减少 App 内存占用的建议

使用 Android Studio 的 Memory Profiler,可以查看当前 Java 堆上分配了哪些对象极其引用关系等很多信息,我给你的建议是只关注 app heap,因为 image heap 和 zygote heap 是 App 启动时从 系统继承过来的,面对这些数据,你啥也干不了:

Understanding Android memory usage (Google I/O '18)

关于 Memory Profiler 的细节我不会讲太多,因为明天中午 12:30 Esteban 将会详细讲解 Profiler 的用法,这玩意儿是他们团队开发的。所以我强力推荐你们也去参加明天的宣讲会。

5.1 除了 Java Heap,其它的是什么玩意儿?

上面提到,TOTAL 是 PSS,那么这张图中,除了 Java Heap,其它的是什么意思呢?对于这部分内存占用,我们又能做什么呢?

Understanding Android memory usage (Google I/O '18)

这就比较好玩了,因为这部分大多是由 Android 平台产生的,如果你真的想理解他们,那么你需要了解很多知识。比如 Framework 是如何实现 View 系统及 Resource 管理的,Native Code 是如何执行的,WebView 是如何工作的,Android Runtime 是如何执行你的代码的,HAL 是如何管理你的 Graphics
的以及 Linux 内核的虚拟内存管理方式等。

顺便说一下,我就在这儿,这个橘黄色的方块里:

Understanding Android memory usage (Google I/O '18)

5.2 既然这些来自 Android 平台,那么我们需要使用工具来诊断吗?

首先,我们可以使用:

adb shell dumpsys meminfo -a [process]

来查看更详细的信息(以下数据为笔者自己开发的 App 的内存占用情况):

Applications Memory Usage (in Kilobytes):
Uptime: 498024399 Realtime: 1230430304

** MEMINFO in pid 10898 [com.yuloran.wanandroid_java] **
                   Pss      Pss   Shared  Private   Shared  Private  SwapPss     Heap     Heap     Heap
                 Total    Clean    Dirty    Dirty    Clean    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap    35822        0      824    35764       32       24     8740    75776    38786    36989
  Dalvik Heap     4001        0      304     3552       72      412      240     6847     3424     3423
 Dalvik Other     5256        0       48     5256        0        0        0                           
        Stack      120        0        4      120        0        0        0                           
       Ashmem      130        0        4      128        4        0        0                           
      Gfx dev     2596        0        0     2596        0        0        0                           
    Other dev       16        0      104        0        0       16        0                           
     .so mmap    23782    22188     1132      504    13320    22188       15                           
    .jar mmap       68        0        8       68        0        0        0                           
    .apk mmap     8029       24        0     7684     1872       24        0                           
    .ttf mmap      223       20        0        0      956       20        0                           
    .dex mmap    21974    19864        0       20    13080    19864        0                           
    .oat mmap      377       64        0        0     3620       64        0                           
    .art mmap     6547      404      868     5852     7584      404       24                           
   Other mmap      408        0       12        8      644      376        0                           
   EGL mtrack    24660        0        0    24660        0        0        0                           
    GL mtrack     4524        0        0     4524        0        0        0                           
      Unknown     2130        0      184     2124        0        0        0                           
        TOTAL   140702    42564     3492    92860    41184    43392       39    82623    42210    40412
 
 Dalvik Details
        .Heap     3308        0        0     3308        0        0        0                           
         .LOS       42        0       16       12        4       28        4                           
 .LinearAlloc     4020        0       20     4020        0        0        0                           
          .GC      384        0       16      384        0        0        0                           
    .JITCache      596        0        0      596        0        0        0                           
      .Zygote      583        0      288      164       68      384        0                           
   .NonMoving       68        0        0       68        0        0        0                           
 .IndirectRef      256        0       12      256        0        0        0                           
 
 App Summary
                       Pss(KB)
                        ------
           Java Heap:     9808
         Native Heap:    35764
                Code:    50436
               Stack:      120
            Graphics:    31780
       Private Other:     8344
              System:     4450
 
               TOTAL:   140702       TOTAL SWAP PSS:       39
 
 Objects
               Views:      207         ViewRootImpl:        1
         AppContexts:        3           Activities:        1
              Assets:       18        AssetManagers:        3
       Local Binders:       24        Proxy Binders:       23
       Parcel memory:        8         Parcel count:       34
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
 
 SQL
         MEMORY_USED:      345
  PAGECACHE_OVERFLOW:       55          MALLOC_SIZE:      117
 
 DATABASES
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4       20             41        17/38/5  /data/user/0/com.yuloran.wanandroid_java/databases/app_database.db
         4       12                         0/0/0    (attached) temp
         4       20             40         3/19/4  /data/user/0/com.yuloran.wanandroid_java/databases/app_database.db (1)

Private Dirty Memory 类似于之前说过的 Used Memory,Private Clean Memory 类似于 之前说过的 Cached Memory。

下面又介绍了几种工具,showmap、ahat、debug malloc等,略。。。因为他下面说到:

Understanding Android memory usage (Google I/O '18)

可以,但没必要。因为这需要很强的专业知识,而且很多数据,即使你能看见,你也无法控制。

5.3 内存优化建议

1. 优化 App Java Heap:

很多内存虽然不在 Java 堆分配,但是其生命周期跟 Java 堆上分配的对象绑在了一起:

Understanding Android memory usage (Google I/O '18)

所以,优化 Java Heap 上的对象,也有助于其它类型内存的回收。

2. 减小 apk 体积:

因为很多在 apk 中占据磁盘空间的文件,在运行期也会占据内存空间:

Understanding Android memory usage (Google I/O '18)

压缩 apk 大小比降低内存占用更容易!更多 apk 大小优化方法请查看 Best Practices to Slim Down Your App Size

6. 结语

本期视频主要讲述了 Android 的 Low Memory Killer 机制、如何评估 App 的内存信息以及如何优化 App 的内存占用,而且是 Android Runtime 工程师亲自讲解,所以还是非常靠谱的。

就笔者自身的开发经验来看,内存泄露比较容易解决,只是有的泄露是由于第三方 SDK 或者 Framework 导致的,此时只能通过反射来修复。如果反射也修复不了,但是不存在持续泄露,即仅泄露一次,也可以不作处理,或者通过商务推动去解决。而减少内存占用则比较困难,毕竟要想 App 功能丰富,那势必会占用更多的内存。而且现在很多项目是多人团队开发,每个人可能只负责一小块,对整个应用的掌控能力不足,进行内存调休就更困难了。所以,内存调优工作需要丰富的编程经验及架构经验,除了 Java 以外,还需要对 Android 的很多 UI 控件有比较深入的理解,因为在 Android 平台上,内存占用大头永远是 UI,主要是 Bitmap。

内存优化,任重而道远。