学向勤中得,萤窗万卷书。这篇文章主要讲述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
- adb shell dumpsys activity o
- adb shell dumpsys activity p
- 来点更直观的体验???
- 我就想知道进程是怎么没的??
- Linux 杀进程方式
- Android 底层杀进程方式
- 上层 AMS 杀进程方式
- force stop
- 感受一下 force stop 的威力
- 多进程架构
- shareUserId
- 案例分析——超级清理王
- 总结
推荐阅读以下内容与本文搭配阅读效果更佳。??
- 图解操作系统内存管理
??强烈建议阅读
- 解读Android进程优先级ADJ算法
而从内存的角度来说,想要实现「丝滑」切换,则必须保证切换到该应用时其对应的进程已创建并加载至内存。理想状态下,我们希望所有应用都处于运行状态。但在软件工程中,「时间」和「空间」总是一对矛盾的存在,想要获得更短的「时间」(丝滑的使用体验),则必须付出更多「空间」(加大内存)。从另一方面讲,应用长时间保持运行状态会耗费更多的电量,导致设备续航能力变差,进而影响用户体验。
Android 内存管理就是在这种矛盾的背景下设计出来的。系统不会立即杀死使用完的进程,反而会对之前创建过的进程进行缓存。当设备内存紧张时,按照一定的策略回收内存。当设备内存低至一定阈值时,系统会按照策略杀死进程以达到释放内存的目的。
本文前半部分介绍 Android 内存管理的主要结构,内存不足时的管理策略;后半部分介绍系统是按照何种策略杀死进程的,有哪些杀进程的方法。
进程间的内存分配 内存类型Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。
文章图片
- RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量
- zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限
- 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命
文章图片
不同类型的页有着各自的作用:
- 已用页(Used Pages)
被进程活跃使用的内存页
- 缓存页(Cached Pages)
进程正在使用的内存页,缓存页在存储器中有相应的备份,必要时可以回收
- 空闲页(Free Pages)
未使用的内存
- 私有页:由一个进程拥有且未共享
- 干净页:存储器中未经修改的文件备份
- 脏页:存储器中经过修改的文件备份
- 干净页:存储器中未经修改的文件备份
- 共享页:由多个进程使用
- 干净页:存储器中未经修改的文件备份
- 脏页:存储器中经过修改的文件备份
?? 注意:干净页包含存在于存储器中的文件(或文件一部分)的精确备份。如果干净页不再包含文件的精确备份(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。统计内存占用如何知道应用程序占用的内存呢?
前文我们提到,设备的内存分页管理。Linux 内核会追踪设备上运行的每个进程正在使用的页。
文章图片
统计应用程序的内存占用,我们只需计算出应用正在使用的页数即可。这个过程略微复杂,因为还要考虑共享页的情况。使用相同服务或库的应用将共享内存页。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。
文章图片
有以下方式来表示内存占有量:
- 常驻内存大小 (Resident Set Size - RSS)
应用使用的 共享页 + 非共享页的数量
- 按比例分摊的内存大小 (Proportional Set Size - PSS)
应用使用的 非共享页数量 + 共享页均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)
- 独占内存大小 (Unique Set Size - USS)
应用使用的 非共享页数量(不包括共享页)
我们可以使用
adb shell dumpsys meminfo -s [process]
来查看进程的 PSS。其中 process 输入 pid 和 applicationId 均可。文章图片
?? 注意:在做内存优化时不能简单地比对 PSS 值来判断内存占用是否得到优化,因为不同设备,不同配置,同一个应用的不同功能,甚至在同一应用在同一使用场景下由于内存压力的不同,PSS 的值是不同的。
文章图片
上图中 x 轴代表内存压力,由左向右越来越大,y 轴代表 PSS 值。在相同的设备内存压力下比较 PSS 值才能得到相对准确的结论。由于很难控制内存压力,因此官方建议在拥有充足 RAM 的设备上进行测试,这样便保证内存压力在一个较低的水平,此时 PSS 值较为稳定,波动很小。才能更准确地判断出所做的优化是否是「负优化」
蓝线代表原始 app,青色代表优化后的 app。
可以看到,在内存压力较低时,PSS 较为平稳,随着内存压力变大,kswapd
开始工作并回收一些 缓存页 ,其中可能就包括该 app 进程的页,因此 PSS 下降。当内存压力极大时触发 lmk,PSS 变为 0(关于内存不足时的管理下一小节介绍)。
我们找到两个点采样,a 和 b,a 的 PSS 小于 b,因此我们会得到原始 app 比优化后的 app 更好。而这个结论显然是错误的。
内存不足管理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。此操作称为「请求分页」。文章图片
kswapd
可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页关联的进程被终止,则该页将从 zRAM 中删除。文章图片
如果可用内存量低于特定阈值,系统会开始杀死进程以回收进程占用的内存。
Low-memory killer很多时候,
kswapd
不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory()
通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始杀死进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。不同于
OOM Killer
,lmk
会每隔一段时间检查一次,当达到触发阈值时,便开始工作。那么
lmk
根据什么来杀死进程呢?这便引出了 进程类型/进程优先级 的概念。常用的进程类型为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入「重要性层次结构」。这些进程类型包括(按重要性排序):
前台进程(foreground process)前台进程 是用户执行当前操作所需的进程。如果以下任何一个条件成立,该进程被视作前台进程:
- 该进程运行一个用户正在交互的 Activity,即 Activity 的 onResume 被调用
- 该进程正在运行一个 BroadcastReceiver,即 BroadcastReceiver 的 onReceive 方法正在执行
- 该进程有一个 Service 正在执行 onCreate、onStart、onDestroy 中的代码
可见进程(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 源码。
文章图片
上图中颜色标识的便是上一小节介绍的常用的进程模式。procstateADJ 是以
PERCEPTIBLE_LOW_APP_ADJ 为 Android 10 新增;
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 为 Android 9 新增。
lmk
的角度对进程优先级的描述,相对比较底层。在 java 世界中管理着 Android 四大组件和进程的是 AMS(Activity Manager Service)。AMS 对进程优先级的描述为 procstate
(Process State),以变量的形式定义在 frameworks/base/core/java/android/app/ActivityManager.java 中。下图基于 Android 11 源码。
文章图片
?? 不同版本略有差异。如何查询进程优先级 adb shell dumpsys meminfo
例如 Android 10 中PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3
,Android 11 删除了该属性并且值依次提前。
我们可以使用
adb shell dumpsys meminfo
命令来查看 进程 ADJ 值:文章图片
adb shell dumpsys activity o
也可以使用
adb shell dumpsys activity o
查询 OOM 相关的信息。文章图片
adb shell dumpsys activity p
还可以使用
adb shell dumpsys activity p
查看每个进程详细的信息文章图片
来点更直观的体验???下图使用的是 Android 10 的设备,因此 procstate 值与上表略有不同,但属性名是相同的。
显示日志是因为我将
ActivityTaskManagerDebugConfig
中的 DEBUG_ALL
打开了,手头上没有显示的设备的小伙伴可以在 这篇文章的文末 下载。文章图片
- 在桌面上打开测试 app,adj = 0,此时 app 进程为前台进程,AMS 中进程状态为 TOP
- 点击 home,此时瞬间有两个变化
- activity pause,此时 adj = 200,属于用户可感知进程
- activity stop,此时 adj = 700,属于上一个应用进程(优先级比缓存进程高),AMS 中进程状态为 LAST
- activity pause,此时 adj = 200,属于用户可感知进程
- 打开图库(其它任意应用均可),此时 测试 app adj 没有变化,仍为上一个应用进程。(用户可从最近任务列表,或手势操作切回测试 app)
- 再次点击 home,此时上一个应用进程为 图库 所在进程。测试 app 所在进程 adj = 900,属于缓存进程,AMS 中进程状态为 CAC(缓存进程,包含 activity)
我就想知道进程是怎么没的??
文章图片
Linux 杀进程方式我们都知道,进程间通信有一个方式叫作「信号」。Linux 提供了几十种信号,分别代表不同的意义。信号之间依靠它们的值来区分。信号可以在任何时候发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就可以了。这就像一个系统应急手册,当遇到什么情况,做什么事情,都事先准备好,出了事情照着做就可以了。
一旦有信号产生,用户进程对信号有以下的处理方式:
- 执行默认操作
Linux 对每种信号都规定了默认操作。
- 捕捉信号
我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
- 忽略信号
有两个信号是应用进程无法捕捉和忽略的,即SIGKILL
和SEGSTOP
,它们用于在任何时候中断或结束某一进程。
SIGKILL
信号,它的值是 9。Android 底层杀进程方式Android 杀进程底层也是使用的信号的方式。
frameworks/base/core/java/android/os/Process.java 使用了三种信号:
SIGNAL_QUIT
= 3SIGNAL_KILL
= 9SIGNAL_USR1
= 10
文章图片
文章图片
?? 其中killProcessQuiet
与killProcessGroup
被标记为 @hide,app 层的开发者只能调用 killProcess(int pid)。而 killProcess 与 killProcessQuiet 的唯一区别是 前者打印日志,后者不打印。
上层(AMS)杀进程均是对这三个方法的调用。
killProcess
虽然是一个静态方法,开发人员可以调用,但 app 层只能调用该方法来实现「自杀」。如果能随意杀死其他进程,那么可就「天下大乱」了。Process.killProcess(Process.myPid())
文章图片
上一小节我们提到,信号值为 9 的信号既不能被忽略也不能被捕捉,因此直接由内核处理,无法有其他操作。这有点像「君让臣死,臣不得不死」,并且没有一丝丝犹豫,没带走一片云彩。
对信号 3 和 10,则是交由目标进程(art 虚拟机)的 SignalCatcher 线程来捕获完成相应操作的。
这部分源码详情可参见 Gityuan 的 理解杀进程的实现原理。
上层 AMS 杀进程方式在 Java 世界,AMS 中封装着杀死进程的方法,不过本质上都是上面 Process 的三个 kill 方法的调用。
文章图片
其中 SYSTEM_UID 指配置了与 system sharedUserId,其他权限指在 Manifest 声明相关的权限上表中功能最强,效果最好的方法是 forceStopPackage。
force stopforce stop 是 Android 中杀进程的一把利器,使用它可以 杀死指定包名的进程,清理相关的四大组件,清除已注册的 alarm 和 notification。
我们以
adb shell am force-stop
命令为例,梳理一下 force stop 的工作流程。adb shell am 命令会调用 frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java 的
onCommand
方法。该方法会根据传入的 cmd 字符串进入到不同的分支,而 force-stop 命令会执行
runForceStop
方法,该方法内部最终调用到 AMS 的 forceStopPackage
方法。其主要代码如下:文章图片
接下来我们简单梳理一下 AMS 的 forceStopPackage 方法:
文章图片
该方法主要有两个操作:手机设置-应用详情的强行停止,就是 force stop
详细内容可参考 Gityuan 的Android进程绝杀技--forceStop。虽然该文是根据 6.0 的源码解析的,但笔者对比了 Android 11 的源码,其核心逻辑没有太大变化,如果想深入了解这部分源码,这篇文章很仍有很大的参考价值。
- 清除进程,四大组件
- 移除 Alarm Notification
文章图片
只看代码比较抽象,我们来更直观的感受一下 force stop 的威力!
感受一下 force stop 的威力 多进程架构
我们在开发过程中经常将 UI 与 Service 分离,使用不同的进程。这样做的目的是为了提高进程优先级,包含 Activity 的 Service 进程与不包含的 Service 进程 ADJ 不同。但 force stop 会将该应用的所有相关进程都 kill 掉。因此不要认为进程分离后便可逃过 force stop 「毒手」。
文章图片
shareUserId
shareUserId
我们在 前文 已有介绍,关于 shareUserId
有两个重要的内容:- 只有具有相同签名(以及请求了相同 sharedUserId)的两个应用才能够获得相同的用户 id
- 具有相同 shareUserId 的应用不代表一定运行在同一进程中
文章图片
但笔者在 Android 10 设备上并没有看到上述现象,欢迎了解这方面知识的小伙伴在评论区留言。
案例分析——超级清理王近日在邓老师的群里看到这样一个流氓软件,即使用户手动点击强行停止,该软件也能重启。
文章图片
文章图片
这个案例 MIUI 的大佬已经解释了,贴图在本节末尾。而这一节我们主要介绍根据前文的内容来分析该软件的流氓行为。
我们通过
adb shell ps
命令并过滤该应用 uid 得到以下结果:文章图片
从图中可以看出,该应用有 6 个进程:接着我们使用
其中前 4 个进程的 PPID(父进程 id)为 782(经查询其父进程为 Zygote 而并非 Zygote64,这意味着它是个 32 位应用)。
最后 2 个进程(main
,daemon
)的 PPID 为 1(init 进程),它们是 native 进程。
adb shell am force-stop com.dn.cpyr.qlds
命令或手动点击「强行停止」按钮杀掉该应用进程,随后再次使用 ps 命令打印进程信息。文章图片
我们发现该应用的进程还在,但 pid 不同了,这意味着 之前的进程已被杀掉,但随后重启。我们可以通过系统日志来验证上述结论是否正确。
文章图片
文章图片
从上方日志可以看出,原有进程的确被杀,而后其创建的 native 进程发送 crash,紧接着新的进程便创建起来,完成重生。对此,我在群中找到了这样的解释:
文章图片
上图来自邓老师微信群其实,该应用为了实现「杀不死」,主要从在两个方向上进行了处理:
- 被杀后重启,即上面提到的
- 通过各种手段提高进程优先级
- UI 进程与 Service 进程分离
文章图片
- 使用 MediaPlayer 播放无声音乐
文章图片
- 使用 AccountManager 备份数据
文章图片
- 注册无障碍服务(辅助功能)
文章图片
- 注册设备管理器
文章图片
【Android Detail(进程篇-进程内存分配与优先级)】
文章图片
总结
- Android 的内存被分为一个个「页」,每个「页」大约 4 KB
- 查看应用占用内存最常见的方式是查看
PSS
,可以使用adb shell dumpsys meminfo -s [process]
查看 - 当内存到达
kswapd
工作的阈值范围时,kswapd
通过删除干净页和压缩脏页来回收内存 - 当内存达到
lmk
工作的阈值范围时,lmk
通过杀死进程来回收内存 - 进程根据重要性不同有着重要的优先级,优先级低的优先被
lmk
杀死 - 在系统上层,
AMS
管理着进程,对应的进程优先级描述为 procstate - 在底层,对应的进程优先级描述为 ADJ
- Linux 中使用信号的方式杀死进程,Android 也采用相同的方式,源码在 Process.java 中
- force stop 具有很强大的力量,所以不要使用所谓的「黑科技」占用设备内存
推荐阅读
- ObjectMapper采坑记及源码分析
- 鎶ラ敊Failed to install the following SDK components:platforms;android-29 Android SDK Platform 29(
- Android studio .a静态库的生成与调用
- HTTPDNS开源 Android SDK,赋能更多开发者参与共建
- 铻嶄簯鍗虫椂閫氳SDK闆嗘垚 -- 鍥藉唴鍘傚晢鎺ㄩ€侀泦鎴愯俯鍧戠瘒(Android骞冲彴)
- 铻嶄簯鍗虫椂閫氳SDK闆嗘垚 -- FCM鎺ㄩ€侀泦鎴愭寚鍗?Android骞冲彴)
- Smobiler APP开发----TabPageView组件
- uni-app生命周期
- appium 移动端自动化测试