区块链技术博客
www.b2bchain.cn

Android Detail:进程篇——进程内存分配与优先级求职学习资料

D0b2wT.gif

本文介绍了Android Detail:进程篇——进程内存分配与优先级求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

前言

很高兴见到你!👋

本文是进程篇的第二篇,前文 介绍了 Android 进程的一些核心概念,而本文将沿着两条线继续介绍进程相关的内容。

第一部分介绍 Android 中内存是如何分配的以及内存不足时的管理策略;第二部分介绍内存不足时清理内存的依据——进程优先级。

了解这些内容,再去看应用的生命周期,Activity 的生命周期等内容就会有不一样的理解。

阅读本文,你将了解:

  • Android 是如何进行进程间的内存分配的
  • 如何统计应用的内存占用
  • 当内存不足时系统使用哪两种手段来释放内存
  • 常用的进程类型
  • 进程的优先级
  • ADJ 与 procstate
  • 通过操作 + 日志直观感受 ADJ 与 procstate 的变化
  • 我就想知道进程是怎么没滴(范伟老师脸😆)
  • 通过一个案例分析流氓软件的恶心行为

文章目录一览

  • 前言
  • 文章目录一览
  • 推荐阅读
  • 谈谈 Android 对内存使用的设计理念
  • 进程间的内存分配
    • 内存类型
    • 内存页
    • 统计内存占用
  • 内存不足管理
    • kernel swap daemon
    • Low-memory killer
  • 常用的进程类型
    • 前台进程(foreground process)
    • 可见进程(visible process)
    • 服务进程(service process)
    • 缓存进程(cached process)
  • 进程优先级
    • ADJ
    • procstate
    • 如何查询进程优先级
      • adb shell dumpsys meminfo

推荐阅读

以下内容与本文搭配阅读效果更佳。😉

  • 图解操作系统内存管理

👆 强烈建议阅读

  • 解读Android进程优先级ADJ算法

谈谈 Android 对内存使用的设计理念

Activity 任务,返回栈 一文中我们曾讨论过 Android 多任务的设计理念。为了保持最好的用户体验,Android 被设计为可以同时执行多个任务,换句话说便是允许多个 App 同时运行并使其能够快速相互切换。

而从内存的角度来说,想要实现「丝滑」切换,则必须保证切换到该应用时其对应的进程已创建并加载至内存。理想状态下,我们希望所有应用都处于运行状态。但在软件工程中,「时间」和「空间」总是一对矛盾的存在,想要获得更短的「时间」(丝滑的使用体验),则必须付出更多「空间」(加大内存)。从另一方面讲,应用长时间保持运行状态会耗费更多的电量,导致设备续航能力变差,进而影响用户体验。

Android 内存管理就是在这种矛盾的背景下设计出来的。系统不会立即杀死使用完的进程,反而会对之前创建过的进程进行缓存。当设备内存紧张时,按照一定的策略回收内存。当设备内存低至一定阈值时,系统会按照策略杀死进程以达到释放内存的目的。

本文前半部分介绍 Android 内存管理的主要结构,内存不足时的管理策略;后半部分介绍系统是按照何种策略杀死进程的,有哪些杀进程的方法。

进程间的内存分配

内存类型

Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。

Android Detail:进程篇——进程内存分配与优先级

内存类型 – RAM、zRAM 和存储器

  • RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量
  • zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限
  • 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命

内存页

Android 的物理内存被分为多个「页」(page)。通常,每个页拥有 4KB 的内存。

Android Detail:进程篇——进程内存分配与优先级

不同类型的页

不同类型的页有着各自的作用:

  • 已用页(Used Pages

    被进程活跃使用的内存页

  • 缓存页(Cached Pages

    进程正在使用的内存页,缓存页在存储器中有相应的备份,必要时可以回收

  • 空闲页(Free Pages

    未使用的内存

其中 缓存页 又分为 私有页 和 共享页,它们各自又分 干净页 脏页:

  • 私有页:由一个进程拥有且未共享

    • 干净页:存储器中未经修改的文件备份

    • 脏页:存储器中经过修改的文件备份

  • 共享页:由多个进程使用
    • 干净页:存储器中未经修改的文件备份
    • 脏页:存储器中经过修改的文件备份

🌟 注意:干净页包含存在于存储器中的文件(或文件一部分)的精确备份。如果干净页不再包含文件的精确备份(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。

统计内存占用

如何知道应用程序占用的内存呢?

前文我们提到,设备的内存分页管理。Linux 内核会追踪设备上运行的每个进程正在使用的页。

Android Detail:进程篇——进程内存分配与优先级

统计内存占用

统计应用程序的内存占用,我们只需计算出应用正在使用的页数即可。这个过程略微复杂,因为还要考虑共享页的情况。使用相同服务或库的应用将共享内存页。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。

Android Detail:进程篇——进程内存分配与优先级

共享内存页

有以下方式来表示内存占有量:

  • 常驻内存大小 (Resident Set Size – RSS)

    应用使用的 共享页 + 非共享页的数量

  • 按比例分摊的内存大小 (Proportional Set Size – PSS)

    应用使用的 非共享页数量 + 共享页均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)

  • 独占内存大小 (Unique Set Size – USS)

    应用使用的 非共享页数量(不包括共享页)

我们最常用的是 PSS。

我们可以使用 adb shell dumpsys meminfo -s [process] 来查看进程的 PSS。其中 process 输入 pid 和 applicationId 均可。

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo -s [pid]

🌟 注意:在做内存优化时不能简单地比对 PSS 值来判断内存占用是否得到优化,因为不同设备,不同配置,同一个应用的不同功能,甚至在同一应用在同一使用场景下由于内存压力的不同,PSS 的值是不同的。

Android Detail:进程篇——进程内存分配与优先级

不同内存压力下 PSS 变化

上图中 x 轴代表内存压力,由左向右越来越大,y 轴代表 PSS 值。

蓝线代表原始 app,青色代表优化后的 app。

可以看到,在内存压力较低时,PSS 较为平稳,随着内存压力变大,kswapd 开始工作并回收一些 缓存页 ,其中可能就包括该 app 进程的页,因此 PSS 下降。当内存压力极大时触发 lmk,PSS 变为 0(关于内存不足时的管理下一小节介绍)。

我们找到两个点采样,a 和 b,a 的 PSS 小于 b,因此我们会得到原始 app 比优化后的 app 更好。而这个结论显然是错误的。

在相同的设备内存压力下比较 PSS 值才能得到相对准确的结论。由于很难控制内存压力,因此官方建议在拥有充足 RAM 的设备上进行测试,这样便保证内存压力在一个较低的水平,此时 PSS 值较为稳定,波动很小。才能更准确地判断出所做的优化是否是「负优化」

内存不足管理

Linux 中有着这样的内存管理策略:OOM Killer(Out Of Memory Killer)。这个策略主要是用于在分屏内存不足时触发,将 oom_score 最高的进程杀掉。

Android 有两种处理内存不足情况的主要机制:内核交换守护进程(kernel swap daemon)和低内存终止守护进程(Low-memory killer)。

kernel swap daemon

内核交换守护进程 (kswapd) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核维持可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd 开始回收内存。当可用内存达到上限阈值时,kswapd 停止回收内存。

kswapd 可以删除干净页来回收内存,因为这些页在存储器中有备份且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页从存储器复制到 RAM。此操作称为「请求分页」。

Android Detail:进程篇——进程内存分配与优先级

干净页被删除 存储器中有备份

kswapd 可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页关联的进程被终止,则该页将从 zRAM 中删除。

Android Detail:进程篇——进程内存分配与优先级

脏页被移至 zRAM 并进行压缩

如果可用内存量低于特定阈值,系统会开始杀死进程以回收进程占用的内存。

Low-memory killer

很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始杀死进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。

不同于 OOM Killerlmk 会每隔一段时间检查一次,当达到触发阈值时,便开始工作。

那么 lmk 根据什么来杀死进程呢?这便引出了 进程类型/进程优先级 的概念。

常用的进程类型

为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入「重要性层次结构」。这些进程类型包括(按重要性排序):

前台进程(foreground process)

前台进程 是用户执行当前操作所需的进程。如果以下任何一个条件成立,该进程被视作前台进程:

  • 该进程运行一个用户正在交互的 Activity,即 Activity 的 onResume 被调用
  • 该进程正在运行一个 BroadcastReceiver,即 BroadcastReceiver 的 onReceive 方法正在执行
  • 该进程有一个 Service 正在执行 onCreateonStartonDestroy 中的代码

此类进程是最重要的进程,在系统内数量有限。因此系统会尽可能地保持此类进程的正常运行。除非内存低至连此类进程都无法继续运行。

可见进程(visible process)

可见进程 正在运行用户当先知晓的任务,因此终止该进程会对用户体验造成明显的负面影响。如果以下任何一个条件成立,该进程被视作可见进程:

  • 该进程运行着一个对用户可见但不在前台的 Activity(onPause 被调用)

    例如应用 A 所在进程是一个前台进程,但它的前台 Activity 是一个对话框,后面显示了应用 B 的 Activity。则此时应用 B 所在的进程为 可见进程。

  • 该进程正在运行着一个通过 startForground 启动的前台服务

  • 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等

此类进程被认为非常重要,除非内存低至无法保持所有 前台进程 正常运行,否则不会终止此类进程。

服务进程(service process)

服务进程 包含一个已使用 startService 方法启动的 Service。虽然用户无法直接看到这些进程,但它们通常正在执行用户关心的任务(例如后台网络数据上传或下载),因此系统会始终使此类进程保持运行,除非没有足够的内存来保留所有前台和可见进程。

长时间运行(如 30 分钟或更长)的 Service 可能会被 降级 成下面要介绍的 缓存进程。这避免了超长时间运行的服务因内存泄漏或其它问题占用大量内存。

缓存进程(cached process)

缓存进程 是目前不需要的进程,因此如果其它地方需要内存,系统会自由地杀死该类进程。为了更高效地切换应用,系统始终保持有多个 缓存进程 可用,并根据需要定期杀死最早的进程。只有在紧急情况下系统才会达到杀死所有缓存进程的地步,此时开始杀死服务进程。

其实系统内对进程优先级的划分更为详细,使用 oom_score_adj 来描述。

进程优先级

ADJ

在 Android 的 lmk 机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj 值的取值范围,oom_adj 值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj 值越高的开始杀。进程级别以变量的形式定义在 ProcessList.java 中。

从 Android 7.0 开始,ADJ 采用 100、200、300。在这之前的版本 ADJ 采用数字 1、2、3。这样的调整可以更进一步地细化进程的优先级。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

ADJ

上图中颜色标识的便是上一小节介绍的常用的进程模式。

PERCEPTIBLE_LOW_APP_ADJ 为 Android 10 新增;

PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 为 Android 9 新增。

procstate

ADJ 是以 lmk 的角度对进程优先级的描述,相对比较底层。在 Java 世界中管理着 Android 四大组件和进程的是 AMS(Activity Manager Service)。AMS 对进程优先级的描述为 procstate(Process State),以变量的形式定义在 frameworks/base/core/java/android/app/ActivityManager.java 中。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

Process State

🌟 不同版本略有差异。

例如 Android 10 中 PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3,Android 11 删除了该属性并且值依次提前。

如何查询进程优先级

adb shell dumpsys meminfo

我们可以使用 adb shell dumpsys meminfo 命令来查看 进程 ADJ 值:

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo

前言

很高兴见到你!👋

本文是进程篇的第二篇,前文 介绍了 Android 进程的一些核心概念,而本文将沿着两条线继续介绍进程相关的内容。

第一部分介绍 Android 中内存是如何分配的以及内存不足时的管理策略;第二部分介绍内存不足时清理内存的依据——进程优先级。

了解这些内容,再去看应用的生命周期,Activity 的生命周期等内容就会有不一样的理解。

阅读本文,你将了解:

  • Android 是如何进行进程间的内存分配的
  • 如何统计应用的内存占用
  • 当内存不足时系统使用哪两种手段来释放内存
  • 常用的进程类型
  • 进程的优先级
  • ADJ 与 procstate
  • 通过操作 + 日志直观感受 ADJ 与 procstate 的变化
  • 我就想知道进程是怎么没滴(范伟老师脸😆)
  • 通过一个案例分析流氓软件的恶心行为

文章目录一览

  • 前言
  • 文章目录一览
  • 推荐阅读
  • 谈谈 Android 对内存使用的设计理念
  • 进程间的内存分配
    • 内存类型
    • 内存页
    • 统计内存占用
  • 内存不足管理
    • kernel swap daemon
    • Low-memory killer
  • 常用的进程类型
    • 前台进程(foreground process)
    • 可见进程(visible process)
    • 服务进程(service process)
    • 缓存进程(cached process)
  • 进程优先级
    • ADJ
    • procstate
    • 如何查询进程优先级
      • adb shell dumpsys meminfo

推荐阅读

以下内容与本文搭配阅读效果更佳。😉

  • 图解操作系统内存管理

👆 强烈建议阅读

  • 解读Android进程优先级ADJ算法

谈谈 Android 对内存使用的设计理念

Activity 任务,返回栈 一文中我们曾讨论过 Android 多任务的设计理念。为了保持最好的用户体验,Android 被设计为可以同时执行多个任务,换句话说便是允许多个 App 同时运行并使其能够快速相互切换。

而从内存的角度来说,想要实现「丝滑」切换,则必须保证切换到该应用时其对应的进程已创建并加载至内存。理想状态下,我们希望所有应用都处于运行状态。但在软件工程中,「时间」和「空间」总是一对矛盾的存在,想要获得更短的「时间」(丝滑的使用体验),则必须付出更多「空间」(加大内存)。从另一方面讲,应用长时间保持运行状态会耗费更多的电量,导致设备续航能力变差,进而影响用户体验。

Android 内存管理就是在这种矛盾的背景下设计出来的。系统不会立即杀死使用完的进程,反而会对之前创建过的进程进行缓存。当设备内存紧张时,按照一定的策略回收内存。当设备内存低至一定阈值时,系统会按照策略杀死进程以达到释放内存的目的。

本文前半部分介绍 Android 内存管理的主要结构,内存不足时的管理策略;后半部分介绍系统是按照何种策略杀死进程的,有哪些杀进程的方法。

进程间的内存分配

内存类型

Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。

Android Detail:进程篇——进程内存分配与优先级

内存类型 – RAM、zRAM 和存储器

  • RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量
  • zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限
  • 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命

内存页

Android 的物理内存被分为多个「页」(page)。通常,每个页拥有 4KB 的内存。

Android Detail:进程篇——进程内存分配与优先级

不同类型的页

不同类型的页有着各自的作用:

  • 已用页(Used Pages

    被进程活跃使用的内存页

  • 缓存页(Cached Pages

    进程正在使用的内存页,缓存页在存储器中有相应的备份,必要时可以回收

  • 空闲页(Free Pages

    未使用的内存

其中 缓存页 又分为 私有页 和 共享页,它们各自又分 干净页 脏页:

  • 私有页:由一个进程拥有且未共享

    • 干净页:存储器中未经修改的文件备份

    • 脏页:存储器中经过修改的文件备份

  • 共享页:由多个进程使用
    • 干净页:存储器中未经修改的文件备份
    • 脏页:存储器中经过修改的文件备份

🌟 注意:干净页包含存在于存储器中的文件(或文件一部分)的精确备份。如果干净页不再包含文件的精确备份(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。

统计内存占用

如何知道应用程序占用的内存呢?

前文我们提到,设备的内存分页管理。Linux 内核会追踪设备上运行的每个进程正在使用的页。

Android Detail:进程篇——进程内存分配与优先级

统计内存占用

统计应用程序的内存占用,我们只需计算出应用正在使用的页数即可。这个过程略微复杂,因为还要考虑共享页的情况。使用相同服务或库的应用将共享内存页。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。

Android Detail:进程篇——进程内存分配与优先级

共享内存页

有以下方式来表示内存占有量:

  • 常驻内存大小 (Resident Set Size – RSS)

    应用使用的 共享页 + 非共享页的数量

  • 按比例分摊的内存大小 (Proportional Set Size – PSS)

    应用使用的 非共享页数量 + 共享页均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)

  • 独占内存大小 (Unique Set Size – USS)

    应用使用的 非共享页数量(不包括共享页)

我们最常用的是 PSS。

我们可以使用 adb shell dumpsys meminfo -s [process] 来查看进程的 PSS。其中 process 输入 pid 和 applicationId 均可。

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo -s [pid]

🌟 注意:在做内存优化时不能简单地比对 PSS 值来判断内存占用是否得到优化,因为不同设备,不同配置,同一个应用的不同功能,甚至在同一应用在同一使用场景下由于内存压力的不同,PSS 的值是不同的。

Android Detail:进程篇——进程内存分配与优先级

不同内存压力下 PSS 变化

上图中 x 轴代表内存压力,由左向右越来越大,y 轴代表 PSS 值。

蓝线代表原始 app,青色代表优化后的 app。

可以看到,在内存压力较低时,PSS 较为平稳,随着内存压力变大,kswapd 开始工作并回收一些 缓存页 ,其中可能就包括该 app 进程的页,因此 PSS 下降。当内存压力极大时触发 lmk,PSS 变为 0(关于内存不足时的管理下一小节介绍)。

我们找到两个点采样,a 和 b,a 的 PSS 小于 b,因此我们会得到原始 app 比优化后的 app 更好。而这个结论显然是错误的。

在相同的设备内存压力下比较 PSS 值才能得到相对准确的结论。由于很难控制内存压力,因此官方建议在拥有充足 RAM 的设备上进行测试,这样便保证内存压力在一个较低的水平,此时 PSS 值较为稳定,波动很小。才能更准确地判断出所做的优化是否是「负优化」

内存不足管理

Linux 中有着这样的内存管理策略:OOM Killer(Out Of Memory Killer)。这个策略主要是用于在分屏内存不足时触发,将 oom_score 最高的进程杀掉。

Android 有两种处理内存不足情况的主要机制:内核交换守护进程(kernel swap daemon)和低内存终止守护进程(Low-memory killer)。

kernel swap daemon

内核交换守护进程 (kswapd) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核维持可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd 开始回收内存。当可用内存达到上限阈值时,kswapd 停止回收内存。

kswapd 可以删除干净页来回收内存,因为这些页在存储器中有备份且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页从存储器复制到 RAM。此操作称为「请求分页」。

Android Detail:进程篇——进程内存分配与优先级

干净页被删除 存储器中有备份

kswapd 可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页关联的进程被终止,则该页将从 zRAM 中删除。

Android Detail:进程篇——进程内存分配与优先级

脏页被移至 zRAM 并进行压缩

如果可用内存量低于特定阈值,系统会开始杀死进程以回收进程占用的内存。

Low-memory killer

很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始杀死进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。

不同于 OOM Killerlmk 会每隔一段时间检查一次,当达到触发阈值时,便开始工作。

那么 lmk 根据什么来杀死进程呢?这便引出了 进程类型/进程优先级 的概念。

常用的进程类型

为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入「重要性层次结构」。这些进程类型包括(按重要性排序):

前台进程(foreground process)

前台进程 是用户执行当前操作所需的进程。如果以下任何一个条件成立,该进程被视作前台进程:

  • 该进程运行一个用户正在交互的 Activity,即 Activity 的 onResume 被调用
  • 该进程正在运行一个 BroadcastReceiver,即 BroadcastReceiver 的 onReceive 方法正在执行
  • 该进程有一个 Service 正在执行 onCreateonStartonDestroy 中的代码

此类进程是最重要的进程,在系统内数量有限。因此系统会尽可能地保持此类进程的正常运行。除非内存低至连此类进程都无法继续运行。

可见进程(visible process)

可见进程 正在运行用户当先知晓的任务,因此终止该进程会对用户体验造成明显的负面影响。如果以下任何一个条件成立,该进程被视作可见进程:

  • 该进程运行着一个对用户可见但不在前台的 Activity(onPause 被调用)

    例如应用 A 所在进程是一个前台进程,但它的前台 Activity 是一个对话框,后面显示了应用 B 的 Activity。则此时应用 B 所在的进程为 可见进程。

  • 该进程正在运行着一个通过 startForground 启动的前台服务

  • 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等

此类进程被认为非常重要,除非内存低至无法保持所有 前台进程 正常运行,否则不会终止此类进程。

服务进程(service process)

服务进程 包含一个已使用 startService 方法启动的 Service。虽然用户无法直接看到这些进程,但它们通常正在执行用户关心的任务(例如后台网络数据上传或下载),因此系统会始终使此类进程保持运行,除非没有足够的内存来保留所有前台和可见进程。

长时间运行(如 30 分钟或更长)的 Service 可能会被 降级 成下面要介绍的 缓存进程。这避免了超长时间运行的服务因内存泄漏或其它问题占用大量内存。

缓存进程(cached process)

缓存进程 是目前不需要的进程,因此如果其它地方需要内存,系统会自由地杀死该类进程。为了更高效地切换应用,系统始终保持有多个 缓存进程 可用,并根据需要定期杀死最早的进程。只有在紧急情况下系统才会达到杀死所有缓存进程的地步,此时开始杀死服务进程。

其实系统内对进程优先级的划分更为详细,使用 oom_score_adj 来描述。

进程优先级

ADJ

在 Android 的 lmk 机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj 值的取值范围,oom_adj 值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj 值越高的开始杀。进程级别以变量的形式定义在 ProcessList.java 中。

从 Android 7.0 开始,ADJ 采用 100、200、300。在这之前的版本 ADJ 采用数字 1、2、3。这样的调整可以更进一步地细化进程的优先级。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

ADJ

上图中颜色标识的便是上一小节介绍的常用的进程模式。

PERCEPTIBLE_LOW_APP_ADJ 为 Android 10 新增;

PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 为 Android 9 新增。

procstate

ADJ 是以 lmk 的角度对进程优先级的描述,相对比较底层。在 Java 世界中管理着 Android 四大组件和进程的是 AMS(Activity Manager Service)。AMS 对进程优先级的描述为 procstate(Process State),以变量的形式定义在 frameworks/base/core/java/android/app/ActivityManager.java 中。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

Process State

🌟 不同版本略有差异。

例如 Android 10 中 PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3,Android 11 删除了该属性并且值依次提前。

如何查询进程优先级

adb shell dumpsys meminfo

我们可以使用 adb shell dumpsys meminfo 命令来查看 进程 ADJ 值:

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo

前言

很高兴见到你!👋

本文是进程篇的第二篇,前文 介绍了 Android 进程的一些核心概念,而本文将沿着两条线继续介绍进程相关的内容。

第一部分介绍 Android 中内存是如何分配的以及内存不足时的管理策略;第二部分介绍内存不足时清理内存的依据——进程优先级。

了解这些内容,再去看应用的生命周期,Activity 的生命周期等内容就会有不一样的理解。

阅读本文,你将了解:

  • Android 是如何进行进程间的内存分配的
  • 如何统计应用的内存占用
  • 当内存不足时系统使用哪两种手段来释放内存
  • 常用的进程类型
  • 进程的优先级
  • ADJ 与 procstate
  • 通过操作 + 日志直观感受 ADJ 与 procstate 的变化
  • 我就想知道进程是怎么没滴(范伟老师脸😆)
  • 通过一个案例分析流氓软件的恶心行为

文章目录一览

  • 前言
  • 文章目录一览
  • 推荐阅读
  • 谈谈 Android 对内存使用的设计理念
  • 进程间的内存分配
    • 内存类型
    • 内存页
    • 统计内存占用
  • 内存不足管理
    • kernel swap daemon
    • Low-memory killer
  • 常用的进程类型
    • 前台进程(foreground process)
    • 可见进程(visible process)
    • 服务进程(service process)
    • 缓存进程(cached process)
  • 进程优先级
    • ADJ
    • procstate
    • 如何查询进程优先级
      • adb shell dumpsys meminfo

推荐阅读

以下内容与本文搭配阅读效果更佳。😉

  • 图解操作系统内存管理

👆 强烈建议阅读

  • 解读Android进程优先级ADJ算法

谈谈 Android 对内存使用的设计理念

Activity 任务,返回栈 一文中我们曾讨论过 Android 多任务的设计理念。为了保持最好的用户体验,Android 被设计为可以同时执行多个任务,换句话说便是允许多个 App 同时运行并使其能够快速相互切换。

而从内存的角度来说,想要实现「丝滑」切换,则必须保证切换到该应用时其对应的进程已创建并加载至内存。理想状态下,我们希望所有应用都处于运行状态。但在软件工程中,「时间」和「空间」总是一对矛盾的存在,想要获得更短的「时间」(丝滑的使用体验),则必须付出更多「空间」(加大内存)。从另一方面讲,应用长时间保持运行状态会耗费更多的电量,导致设备续航能力变差,进而影响用户体验。

Android 内存管理就是在这种矛盾的背景下设计出来的。系统不会立即杀死使用完的进程,反而会对之前创建过的进程进行缓存。当设备内存紧张时,按照一定的策略回收内存。当设备内存低至一定阈值时,系统会按照策略杀死进程以达到释放内存的目的。

本文前半部分介绍 Android 内存管理的主要结构,内存不足时的管理策略;后半部分介绍系统是按照何种策略杀死进程的,有哪些杀进程的方法。

进程间的内存分配

内存类型

Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。

Android Detail:进程篇——进程内存分配与优先级

内存类型 – RAM、zRAM 和存储器

  • RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量
  • zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限
  • 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命

内存页

Android 的物理内存被分为多个「页」(page)。通常,每个页拥有 4KB 的内存。

Android Detail:进程篇——进程内存分配与优先级

不同类型的页

不同类型的页有着各自的作用:

  • 已用页(Used Pages

    被进程活跃使用的内存页

  • 缓存页(Cached Pages

    进程正在使用的内存页,缓存页在存储器中有相应的备份,必要时可以回收

  • 空闲页(Free Pages

    未使用的内存

其中 缓存页 又分为 私有页 和 共享页,它们各自又分 干净页 脏页:

  • 私有页:由一个进程拥有且未共享

    • 干净页:存储器中未经修改的文件备份

    • 脏页:存储器中经过修改的文件备份

  • 共享页:由多个进程使用
    • 干净页:存储器中未经修改的文件备份
    • 脏页:存储器中经过修改的文件备份

🌟 注意:干净页包含存在于存储器中的文件(或文件一部分)的精确备份。如果干净页不再包含文件的精确备份(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。

统计内存占用

如何知道应用程序占用的内存呢?

前文我们提到,设备的内存分页管理。Linux 内核会追踪设备上运行的每个进程正在使用的页。

Android Detail:进程篇——进程内存分配与优先级

统计内存占用

统计应用程序的内存占用,我们只需计算出应用正在使用的页数即可。这个过程略微复杂,因为还要考虑共享页的情况。使用相同服务或库的应用将共享内存页。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。

Android Detail:进程篇——进程内存分配与优先级

共享内存页

有以下方式来表示内存占有量:

  • 常驻内存大小 (Resident Set Size – RSS)

    应用使用的 共享页 + 非共享页的数量

  • 按比例分摊的内存大小 (Proportional Set Size – PSS)

    应用使用的 非共享页数量 + 共享页均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)

  • 独占内存大小 (Unique Set Size – USS)

    应用使用的 非共享页数量(不包括共享页)

我们最常用的是 PSS。

我们可以使用 adb shell dumpsys meminfo -s [process] 来查看进程的 PSS。其中 process 输入 pid 和 applicationId 均可。

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo -s [pid]

🌟 注意:在做内存优化时不能简单地比对 PSS 值来判断内存占用是否得到优化,因为不同设备,不同配置,同一个应用的不同功能,甚至在同一应用在同一使用场景下由于内存压力的不同,PSS 的值是不同的。

Android Detail:进程篇——进程内存分配与优先级

不同内存压力下 PSS 变化

上图中 x 轴代表内存压力,由左向右越来越大,y 轴代表 PSS 值。

蓝线代表原始 app,青色代表优化后的 app。

可以看到,在内存压力较低时,PSS 较为平稳,随着内存压力变大,kswapd 开始工作并回收一些 缓存页 ,其中可能就包括该 app 进程的页,因此 PSS 下降。当内存压力极大时触发 lmk,PSS 变为 0(关于内存不足时的管理下一小节介绍)。

我们找到两个点采样,a 和 b,a 的 PSS 小于 b,因此我们会得到原始 app 比优化后的 app 更好。而这个结论显然是错误的。

在相同的设备内存压力下比较 PSS 值才能得到相对准确的结论。由于很难控制内存压力,因此官方建议在拥有充足 RAM 的设备上进行测试,这样便保证内存压力在一个较低的水平,此时 PSS 值较为稳定,波动很小。才能更准确地判断出所做的优化是否是「负优化」

内存不足管理

Linux 中有着这样的内存管理策略:OOM Killer(Out Of Memory Killer)。这个策略主要是用于在分屏内存不足时触发,将 oom_score 最高的进程杀掉。

Android 有两种处理内存不足情况的主要机制:内核交换守护进程(kernel swap daemon)和低内存终止守护进程(Low-memory killer)。

kernel swap daemon

内核交换守护进程 (kswapd) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核维持可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd 开始回收内存。当可用内存达到上限阈值时,kswapd 停止回收内存。

kswapd 可以删除干净页来回收内存,因为这些页在存储器中有备份且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页从存储器复制到 RAM。此操作称为「请求分页」。

Android Detail:进程篇——进程内存分配与优先级

干净页被删除 存储器中有备份

kswapd 可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页关联的进程被终止,则该页将从 zRAM 中删除。

Android Detail:进程篇——进程内存分配与优先级

脏页被移至 zRAM 并进行压缩

如果可用内存量低于特定阈值,系统会开始杀死进程以回收进程占用的内存。

Low-memory killer

很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始杀死进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。

不同于 OOM Killerlmk 会每隔一段时间检查一次,当达到触发阈值时,便开始工作。

那么 lmk 根据什么来杀死进程呢?这便引出了 进程类型/进程优先级 的概念。

常用的进程类型

为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入「重要性层次结构」。这些进程类型包括(按重要性排序):

前台进程(foreground process)

前台进程 是用户执行当前操作所需的进程。如果以下任何一个条件成立,该进程被视作前台进程:

  • 该进程运行一个用户正在交互的 Activity,即 Activity 的 onResume 被调用
  • 该进程正在运行一个 BroadcastReceiver,即 BroadcastReceiver 的 onReceive 方法正在执行
  • 该进程有一个 Service 正在执行 onCreateonStartonDestroy 中的代码

此类进程是最重要的进程,在系统内数量有限。因此系统会尽可能地保持此类进程的正常运行。除非内存低至连此类进程都无法继续运行。

可见进程(visible process)

可见进程 正在运行用户当先知晓的任务,因此终止该进程会对用户体验造成明显的负面影响。如果以下任何一个条件成立,该进程被视作可见进程:

  • 该进程运行着一个对用户可见但不在前台的 Activity(onPause 被调用)

    例如应用 A 所在进程是一个前台进程,但它的前台 Activity 是一个对话框,后面显示了应用 B 的 Activity。则此时应用 B 所在的进程为 可见进程。

  • 该进程正在运行着一个通过 startForground 启动的前台服务

  • 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等

此类进程被认为非常重要,除非内存低至无法保持所有 前台进程 正常运行,否则不会终止此类进程。

服务进程(service process)

服务进程 包含一个已使用 startService 方法启动的 Service。虽然用户无法直接看到这些进程,但它们通常正在执行用户关心的任务(例如后台网络数据上传或下载),因此系统会始终使此类进程保持运行,除非没有足够的内存来保留所有前台和可见进程。

长时间运行(如 30 分钟或更长)的 Service 可能会被 降级 成下面要介绍的 缓存进程。这避免了超长时间运行的服务因内存泄漏或其它问题占用大量内存。

缓存进程(cached process)

缓存进程 是目前不需要的进程,因此如果其它地方需要内存,系统会自由地杀死该类进程。为了更高效地切换应用,系统始终保持有多个 缓存进程 可用,并根据需要定期杀死最早的进程。只有在紧急情况下系统才会达到杀死所有缓存进程的地步,此时开始杀死服务进程。

其实系统内对进程优先级的划分更为详细,使用 oom_score_adj 来描述。

进程优先级

ADJ

在 Android 的 lmk 机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj 值的取值范围,oom_adj 值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj 值越高的开始杀。进程级别以变量的形式定义在 ProcessList.java 中。

从 Android 7.0 开始,ADJ 采用 100、200、300。在这之前的版本 ADJ 采用数字 1、2、3。这样的调整可以更进一步地细化进程的优先级。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

ADJ

上图中颜色标识的便是上一小节介绍的常用的进程模式。

PERCEPTIBLE_LOW_APP_ADJ 为 Android 10 新增;

PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 为 Android 9 新增。

procstate

ADJ 是以 lmk 的角度对进程优先级的描述,相对比较底层。在 Java 世界中管理着 Android 四大组件和进程的是 AMS(Activity Manager Service)。AMS 对进程优先级的描述为 procstate(Process State),以变量的形式定义在 frameworks/base/core/java/android/app/ActivityManager.java 中。

下图基于 Android 11 源码。

Android Detail:进程篇——进程内存分配与优先级

Process State

🌟 不同版本略有差异。

例如 Android 10 中 PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3,Android 11 删除了该属性并且值依次提前。

如何查询进程优先级

adb shell dumpsys meminfo

我们可以使用 adb shell dumpsys meminfo 命令来查看 进程 ADJ 值:

Android Detail:进程篇——进程内存分配与优先级

adb shell dumpsys meminfo

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » Android Detail:进程篇——进程内存分配与优先级求职学习资料
分享到: 更多 (0)
D0b2wT.gif

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们