低内存配置

简介

Android 现支持内存为 512MB 的设备。本文档旨在帮助 OEM 优化和配置 Android 4.4,使其能够在低内存设备上运行。在下文所述的优化措施中,有几项非常通用,甚至也可应用于以前的版本。

Android 4.4 平台优化

改善了内存管理

  • 采用了经验证可节省内存的内核配置:交换到 ZRAM。
  • 终止那些即将被取消缓存且过大的缓存进程。
  • 不允许大型服务自行返回至 A 服务状态(以免导致启动器终止)。
  • 终止那些处于空闲维护状态中的过大进程(甚至终止当前 IME 等通常不可终止的进程)。
  • 对后台服务的启动进行序列化。
  • 优化了低内存设备的内存使用方式:采用更严格的内存不足 (OOM) 调整级别、缩减图形缓存大小,等等。

减少了系统内存占用

  • 删减了 system_server 和 SystemUI 进程(节省了几兆的内存)。
  • 在 Dalvik 中预加载 dex 缓存(节省了几兆的内存)。
  • 采用了经验证的 JIT-off 选项(每个进程最多可节省 1.5MB 的内存)。
  • 减少了各进程的字体缓存开销。
  • 引入了占用内存更少的 ArrayMap/ArraySet,并在框架中广泛地使用其来替代 HashMap/HashSet。

Procstats

新增了一个开发者选项,以显示内存状态和应用内存使用情况(按照运行频率和所耗内存量排序)。

API

新增了 ActivityManager.isLowRamDevice(),使应用不仅能够检测是否是在低内存设备上运行,还能选择停用那些占用内存较多的功能。

内存跟踪

新的 memtrack HAL 可以跟踪图形内存分配情况、dumpsys meminfo 中的更多信息,以及 meminfo 中的阐明性总结(例如,所报告的可用内存包括缓存进程占用的内存,这样 OEM 就不会搞错优化对象)。

编译时配置

启用低内存设备标志

我们引入了 ActivityManager.isLowRamDevice() 这个新 API,以便应用确定是否应关闭在低内存设备上表现非常差的某些内存密集型功能。

对于内存为 512MB 的设备,该 API 应返回 true。可以通过在设备 makefile 中使用以下系统属性来启用该 API:

PRODUCT_PROPERTY_OVERRIDES += ro.config.low_ram=true

启动器配置

请务必确保启动器的默认壁纸设置使用动态壁纸。低内存设备不应预装任何动态壁纸。

内核配置

优化内核/ActivityManager 以减少直接回收

当进程或内核尝试分配(直接分配或因新页面中存在故障而分配)内存页面并且内核已用尽所有可用内存时,就会发生直接回收。在这种情况下,内核需要释放一个页面,并在此过程中阻断分配操作。而这通常又需要磁盘 I/O 清理一个有文件支持的脏页或等待 lowmemorykiller 终止一个进程。最终可能会导致任意线程(包括界面线程)中出现额外 I/O。

为避免出现直接回收,内核已配有可触发 kswapd 或后台回收的水印。此线程会尝试释放页面,以便下次分配的真实线程能够快速顺利启动。

用于触发后台回收的默认阈值相当低 - 在 2GB 设备上约为 2MB,在 512MB 设备上约为 636KB。而且,内核通过后台回收仅能回收几兆的内存。这意味着,任何快速分配超过几 MB 内容的进程都会快速导致直接回收。

在 android-3.4 内核分支中,我们通过补丁程序 92189d47f66c67e5fd92eafaa287e153197a454f(“添加用于扩展可用内存空间的可调选项”)添加了对新内核可调选项的支持。如果您选择将该补丁程序添加到设备内核中,ActivityManager 会告知内核尝试保留能容纳 3 个全屏 32 bpp 缓冲区的可用内存空间。

这些阈值可通过框架 config.xml 进行配置

<!-- Device configuration setting the /proc/sys/vm/extra_free_kbytes tunable
in the kernel (if it exists).  A high value will increase the amount of memory
that the kernel tries to keep free, reducing allocation time and causing the
lowmemorykiller to kill earlier.  A low value allows more memory to be used by
processes but may cause more allocations to block waiting on disk I/O or
lowmemorykiller.  Overrides the default value chosen by ActivityManager based
on screen size.  0 prevents keeping any extra memory over what the kernel keeps
by default.  -1 keeps the default. -->
<integer name="config_extraFreeKbytesAbsolute">-1</integer>
<!-- Device configuration adjusting the /proc/sys/vm/extra_free_kbytes
tunable in the kernel (if it exists).  0 uses the default value chosen by
ActivityManager.  A positive value  will increase the amount of memory that the
kernel tries to keep free, reducing allocation time and causing the
lowmemorykiller to kill earlier.  A negative value allows more memory to be
used by processes but may cause more allocations to block waiting on disk I/O
or lowmemorykiller.  Directly added to the default value chosen by
ActivityManager based on screen size. -->
<integer name="config_extraFreeKbytesAdjust">0</integer>

优化 LowMemoryKiller

ActivityManager 可配置 LowMemoryKiller 的阈值,使其符合它对在每个优先级分段中运行进程时所需的文件支持页面(缓存页面)工作集的预期。如果设备对工作集有很高的要求(例如:如果供应商界面需要更多内存,或者如果添加了更多服务),则可增大阈值。

如果为文件支持页面预留了太多内存,则可减小阈值,以便系统能够在因缓存变得过小而导致磁盘超负荷之前就终止后台进程。

<!-- Device configuration setting the minfree tunable in the lowmemorykiller
in the kernel.  A high value will cause the lowmemorykiller to fire earlier,
keeping more memory in the file cache and preventing I/O thrashing, but
allowing fewer processes to stay in memory.  A low value will keep more
processes in memory but may cause thrashing if set too low.  Overrides the
default value chosen by ActivityManager based on screen size and total memory
for the largest lowmemorykiller bucket, and scaled proportionally to the
smaller buckets.  -1 keeps the default. -->
<integer name="config_lowMemoryKillerMinFreeKbytesAbsolute">-1</integer>
<!-- Device configuration adjusting the minfree tunable in the
lowmemorykiller in the kernel.  A high value will cause the lowmemorykiller to
fire earlier, keeping more memory in the file cache and preventing I/O
thrashing, but allowing fewer processes to stay in memory.  A low value will
keep more processes in memory but may cause thrashing if set too low.  Directly
added to the default value chosen by          ActivityManager based on screen
size and total memory for the largest lowmemorykiller bucket, and scaled
proportionally to the smaller buckets. 0 keeps the default. -->
<integer name="config_lowMemoryKillerMinFreeKbytesAdjust">0</integer>

交换到 zRAM

zRAM 交换可通过压缩内存页面并将其放入动态分配的内存交换区来增加系统中的可用内存量。

由于这是以牺牲 CPU 时间为代价来增加少量内存,因此您应仔细权衡 zRAM 交换会对您系统的性能造成的负面影响。

Android 会在多个层面上处理 zRAM 交换:

  • 首先,必须启用以下内核选项,才能有效地使用 zRAM 交换:
    • CONFIG_SWAP
    • CONFIG_CGROUP_MEM_RES_CTLR
    • CONFIG_CGROUP_MEM_RES_CTLR_SWAP
    • CONFIG_ZRAM
  • 然后,您应将一行与下列类似的内容添加到 fstab 中:
    /dev/block/zram0 none swap defaults zramsize=<size in bytes>,swapprio=<swap partition priority>
    
    • zramsize 是必要内容,表示您希望 zram 区域占用多少未压缩内存。压缩比通常介于 30-50% 之间。
    • swapprio 是可选内容;如果您没有多个交换区,则无需使用此项。

    您还应确保在特定于设备的 sepolicy/file_contexts 中将关联的块设备标记为 swap_block_device,以便 SELinux 适当地对其进行处理。

    /dev/block/zram0 u:object_r:swap_block_device:s0
    
  • 默认情况下,Linux 内核每次会换入 8 页内存。当使用 ZRAM 时,每次读取 1 页内存产生的增量开销微乎其微,因此在设备承受着极大的内存压力时可能有所助益。要想每次只读取 1 页内存,请将以下内容添加到 init.rc 中:
    write /proc/sys/vm/page-cluster 0
    
  • init.rc 中的 mount_all /fstab.X 行后面,添加以下内容:
    swapon_all /fstab.X
    
  • 如果在内核中启用了此功能,系统便会在启动时自动配置内存 cgroup。
  • 如果内存 cgroup 可用,ActivityManager 就会将优先级较低的线程标为比其他线程更适合交换。在需要内存时,Android 内核会开始将内存页面迁移到 zRAM 交换区,并会优先处理那些已被 ActivityManager 标记的内存页面。

Carveout、Ion 和连续内存分配 (CMA)

对于低内存设备,需要特别注意 carveout,尤其是不会一直得到充分利用的 carveout,例如用于安全地播放视频的 carveout。有几种解决方案可最大限度地减小 carveout 区域的影响,具体取决于硬件的确切要求。

如果硬件允许不连续的内存分配,则可利用 Ion 系统堆从系统内存中分配内存,这样便无需使用 carveout。它还会尝试增大分配的内存空间以消除外围设备上的 TLB 压力。如果内存区域必须连续或必须限定在某个特定地址范围内,则可以使用连续内存分配器 (CMA)。

以这种方式创建的 carveout 也可供系统用于处理可移动页面。当需要该区域时,可移动页面就会从中移出,以便系统将处于空闲状态的大型 carveout 用于其他目的。您可以直接使用 CMA,也可以借助 Ion(通过使用 Ion CMA 堆)更轻松地使用 CMA。

应用优化提示

了解 Android 中的各种进程状态

  • SERVICE - SERVICE_RESTARTING
    因自身原因而自行转入后台运行的应用。这是频繁在后台运行的应用最常出现的问题。%duration * pss 很可能是一个绝佳的“不良”指标,但该组合的目标过于明确,也许只依靠 %duration 就能更好地筛选出我们完全不希望运行的应用。

  • IMPORTANT_FOREGROUND - RECEIVER
    因任何原因而在后台运行的应用(不直接与用户交互)。这些应用都会增加系统的内存负载。在这种情况下,(%duration * pss) 不良值很可能是对这些进程进行排序的最佳方式:许多此类进程都会因合理原因而需要一直运行,因此它们的 pss 大小将会是它们的内存负载的重要组成部分。

  • PERSISTENT
    持续的系统进程。跟踪 pss 可监视这些进程是否会变得过大。

  • TOP
    正与用户交互的进程。pss 在此又成为了重要指标,可显示应用在被使用的过程中产生的内存负载。

  • HOME - CACHED_EMPTY
    所有此类底部进程均是系统保留的备用进程;此类进程可随时终止,并可根据需要重建。这些进程是我们计算内存状态时参照的依据 -“正常”、“中等”、“低”、“严重”均是基于系统可以保留多少个此类进程而定的。pss 亦是这些进程的关键指标;当这些进程处于该状态时,它们应尽量减少自身的内存占用空间,以便系统能够保留尽可能多的进程。一般来说,与在 TOP 状态下相比,运行状况良好的应用在该状态下的 pss 占用空间明显更小。

  • TOP 与 CACHED_ACTIVITY-CACHED_ACTIVITY_CLIENT 的差异
    进程处于 TOP 状态时的 pss 和进程处于上述任一种缓存状态时的 pss 之间会存在差异,而这种差异是表明进程在进入后台运行时的内存释放能力的最佳数据。排除 CACHED_EMPTY 状态可以改善该数据,因为这项操作会排除因某些原因(不仅仅是为了呈现界面)而启动进程的情况,因此无需在与用户交互时处理所有的界面开销。

分析

分析应用启动时间

配合 -P--start-profiler 选项使用 $ adb shell am start,以便在应用启动的时候运行分析器。这样一来,当您的进程从 zygote 分岔之后,分析器就会立即启动,而此时您的任何代码都还未加载到其中。

根据错误报告进行分析

错误报告现包含可用于进行调试的各种信息。相关服务包括 batterystatsnetstatsprocstatsusagestats。您可以在如下所示的报告行中找到对应的信息:

------ CHECKIN BATTERYSTATS (dumpsys batterystats --checkin) ------
7,0,h,-2558644,97,1946288161,3,2,0,340,4183
7,0,h,-2553041,97,1946288161,3,2,0,340,4183

检查是否存在任何持续进程

重新启动设备并检查进程情况。
让设备运行几个小时,然后再次检查进程情况。不应存在任何长时间运行的进程。

运行长时测试

让设备运行较长时间,并跟踪进程的内存占用情况。内存占用是增加了,还是保持不变?请拟订规范的使用情形,并针对这些情形运行长时测试。