Oom Adjuster 设计

1. Oom Adjuster 目的

Android 操作系统运行在一个硬件资源非常有限的情况下,即:CPU RAM 电量等。为了有更好的性能体验,引入 oom Adjuster 来调整以下三个主要因素:

  • 进程状态

    system server 广泛使用,即通过判断是否在前台来改变GC 行为等等。 定义在 ActivityManager#PROCESS_STATE_*

  • Oom Adj 得分

    lmkd 使用去决定那一个进程在内存压力强况下会被清除。定义在 ProcessList#*_ADJ

  • 调度组

    用于调整进程组/线程优先级。顶层进程被安排在一个专用的大核上运行,而前台进程使用其他大核,后台进程使用小核。

2. 进程能力

除了上面三个主要因素,Android R 引入了进程能力 ActivityManager#PROCESS_CAPABILITY_*. 这是进程记录的一个新属性,主要设计来支持 while-in-use 权限模型。

– 除了传统的 Android 权限之外,进程是否可以访问给定的 API,也将受到其当前进程状态的保护。 OomAdjuster 将在更新 oom adj 期间计算过程能力。同时,标志ActivityManager#BIND_INCLUDE_CAPABILITIES能够将能力从客户端进程“转移”到它所绑定的服务进程。

3. Oom Adjuster 的基本原理

System server 保持了一个最近使用的应用进程的列表。考虑到 Android 可以处理的 4 种实体类型:ActivityServiceContent ProviderBroadcast Receiver,系统服务器必须根据实体的状态调整上述 3 个因素以给用户最佳性能。一个典型的例子是:前台app A为了服务用户,绑定了后台服务B,在内存压力的情况下,应该避免后台服务B被清除,因为这会导致用户感知的服务中断. Oom 调整器将针对这些应用程序流程调整上述 3 个因素。

更新 Oom Adj 分数的时机至关重要:假设后台的相机进程启动到前台,启动相机通常会导致高内存压力,这可能会导致低内存杀死 - 如果相机进程没有移出后台 adj组,它可能会被 lmkd 杀死。因此必须非常频繁地调用更新:如果有活动启动、服务绑定等。

更新过程基本上包括 3 部分:

  1. 找出要更新的进程记录;

    updateOomAdjLocked 有两类:一类是更新目标进程记录,另一类是更新所有进程记录。

    除此之外,在计算 Oom Adj 分数的同时,也会评估当前流程记录的服务连接的客户端或内容提供者,这些记录实际上形成了流程依赖关系图。

    Android R开始,针对特定的进程记录进行更新时,做了一个优化,只有进程依赖图中从该进程记录开始的可达进程记录才会被重新评估。

    cached Oom Adj 分数分组在bucket 中,用于隔离进程:它们可以相关联 - 假设一个隔离的 Chrome 进程的 Oom Adj 得分为 920,另一个为 980;后一个可能比前一个更早被清除,这是没有意义的;对于这种情况,将它们分组将是一个很大的缓解。

  2. 计算 Oom Adj 的值;

    如果分数发生变化,则此过程返回 true,如果没有,则返回 false。

    进程记录中的 curAdj 字段作为计算过程中的中间值。

    将进程状态初始化为PROCESS_STATE_CACHED_EMPTY,这是重要性最低的。

    根据各种因素计算分数:

    • 如果不允许低于ProcessList#FOREGROUND_APP_ADJ,这意味着它可能是一个持久化进程,这里没有太多可做的。

    • 检查进程是否为顶层应用,运行远程动画,运行检测,接收广播,执行服务,运行在顶层但睡眠(屏幕关闭),更新中间值。

    • 询问窗口管理器(是的,ActivityTaskManager 现在与 WindowManager 一起)告诉每个活动的可见性信息。

    • 检查进程是否有最近的任务,检查它是否正在托管前台服务、覆盖 UItoast 等。注意前台服务,如果它处于前台状态,让它在内存中保持较高的等级一段时间:假设相机拍照案例,其中相机应用程序在切换出前景时仍在处理图片 - 保持它在内存中保持较高的等级将确保图片正确保留。

    • 检查进程是否为重量级进程,重量级进程是说那些启动/退出会很慢,最好将其保存在内存中。请注意,整个系统中应该只有一个重量级进程。

    • 当然,也不应该经常清除 Home 进程。

    • 接下来的两个因素要么是前一个对用户具有可见 UI 的进程,要么是备份代理。

    • 然后对service 连接和 content providers 进行大量搜索,每个客户端都会被评估,并且 Oom Adj 分数可以根据其客户端的分数进行更新。但是,有一堆服务绑定标志可能会影响结果:

      • 下表总结了给定各种 service 绑定状态的结果:

      • 下表捕获了给定各种 content providers 绑定状态的结果:

      • 在上述调整之后的一些额外调整:

  3. 应用 Oom adj 的得分结果,包括:写入内核 sysfs 条目以更新 Oom Adj 分数;调用内核API设置线程优先级,然后告诉全系统新的进程状态;

4. 环,环,环

Oom Adjuster 的另一个有趣方面是依赖关系的循环。一个简单的例子如下图所示,进程 A 正在托管一个由进程 B 绑定的服务;同时进程 B 正在托管一个由进程 A 绑定的服务。

1
2
3
4
  +-------------+           +-------------+
  |  Process A  | <-------- |  Process B  |
  | (service 1) | --------> | (service 2) |
  +-------------+           +-------------+

可能有非常复杂的情况,可能涉及多个循环,并且在依赖图中,每个流程记录节点可能具有不同的重要性。

1
2
3
4
  +-------------+           +-------------+           +-------------+           +-------------+           +-------------+
  |  Process D  | --------> |  Process A  | <-------- |  Process B  | <-------- |  Process C  | <-------- |  Process A  |
  |             |           | (service 1) |           | (service 2) |           | (service 3) |           | (service 1) |
  +-------------+           +-------------+           +-------------+           +-------------+           +-------------+

Oom Adjuster 维护一个全局序列 ID mAdjSeq 来跟踪当前的 Oom Adjuster调用。并且每个流程记录都有一个字段来跟踪评估流程记录的顺序。如果在 Oom Adj 计算过程中,一个进程记录的序列 ID 与当前全局序列 ID 相同,这将意味着检测到一个循环;在这种情况下:

  • 如果有循环,则递减每个进程的序列 ID

  • 重新评估循环中的每个进程记录,直到没有任何提升。

  • 从最不重要到最重要的过程迭代。

  • 最大重试次数为 10 次,而在实际操作中,最大重试次数只能达到 2 到 3 次。