标签:超级 显示 注意 存储器 注册 相互 创建 微信 相同
很高兴见到你!??
本文是进程篇的第二篇,前文 介绍了 Android 进程的一些核心概念,而本文将沿着两条线继续介绍进程相关的内容。
第一部分介绍 Android 中内存是如何分配的以及内存不足时的管理策略;第二部分介绍内存不足时清理内存的依据——进程优先级。
了解这些内容,再去看应用的生命周期,Activity 的生命周期等内容就会有不一样的理解。
阅读本文,你将了解:
以下内容与本文搭配阅读效果更佳。??
?? 强烈建议阅读
Activity 任务,返回栈 一文中我们曾讨论过 Android 多任务的设计理念。为了保持最好的用户体验,Android 被设计为可以同时执行多个任务,换句话说便是允许多个 App 同时运行并使其能够快速相互切换。
而从内存的角度来说,想要实现「丝滑」切换,则必须保证切换到该应用时其对应的进程已创建并加载至内存。理想状态下,我们希望所有应用都处于运行状态。但在软件工程中,「时间」和「空间」总是一对矛盾的存在,想要获得更短的「时间」(丝滑的使用体验),则必须付出更多「空间」(加大内存)。从另一方面讲,应用长时间保持运行状态会耗费更多的电量,导致设备续航能力变差,进而影响用户体验。
Android 内存管理就是在这种矛盾的背景下设计出来的。系统不会立即杀死使用完的进程,反而会对之前创建过的进程进行缓存。当设备内存紧张时,按照一定的策略回收内存。当设备内存低至一定阈值时,系统会按照策略杀死进程以达到释放内存的目的。
本文前半部分介绍 Android 内存管理的主要结构,内存不足时的管理策略;后半部分介绍系统是按照何种策略杀死进程的,有哪些杀进程的方法。
Android 设备包含三种不同类型的内存:RAM、zRAM 和存储器。
Android 的物理内存被分为多个「页」(page)。通常,每个页拥有 4KB 的内存。
不同类型的页有着各自的作用:
已用页(Used Pages)
被进程活跃使用的内存页
缓存页(Cached Pages)
进程正在使用的内存页,缓存页在存储器中有相应的备份,必要时可以回收
空闲页(Free Pages)
未使用的内存
其中 缓存页 又分为 私有页 和 共享页,它们各自又分 干净页 脏页:
干净页:存储器中未经修改的文件备份
脏页:存储器中经过修改的文件备份
?? 注意:干净页包含存在于存储器中的文件(或文件一部分)的精确备份。如果干净页不再包含文件的精确备份(例如,因应用操作所致),则会变成脏页。干净页可以删除,因为始终可以使用存储器中的数据重新生成它们;脏页则不能删除,否则数据将会丢失。
如何知道应用程序占用的内存呢?
前文我们提到,设备的内存分页管理。Linux 内核会追踪设备上运行的每个进程正在使用的页。
统计应用程序的内存占用,我们只需计算出应用正在使用的页数即可。这个过程略微复杂,因为还要考虑共享页的情况。使用相同服务或库的应用将共享内存页。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。
有以下方式来表示内存占有量:
常驻内存大小 (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 均可。
?? 注意:在做内存优化时不能简单地比对 PSS 值来判断内存占用是否得到优化,因为不同设备,不同配置,同一个应用的不同功能,甚至在同一应用在同一使用场景下由于内存压力的不同,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)。
内核交换守护进程 (kswapd
) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核维持可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd
开始回收内存。当可用内存达到上限阈值时,kswapd
停止回收内存。
kswapd
可以删除干净页来回收内存,因为这些页在存储器中有备份且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页从存储器复制到 RAM。此操作称为「请求分页」。
kswapd
可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页关联的进程被终止,则该页将从 zRAM 中删除。
如果可用内存量低于特定阈值,系统会开始杀死进程以回收进程占用的内存。
很多时候,kswapd
不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory()
通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始杀死进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。
不同于 OOM Killer
,lmk
会每隔一段时间检查一次,当达到触发阈值时,便开始工作。
那么 lmk
根据什么来杀死进程呢?这便引出了 进程类型/进程优先级 的概念。
为了确定在内存不足时应该终止哪些进程,Android 会根据每个进程中运行的组件以及这些组件的状态,将它们放入「重要性层次结构」。这些进程类型包括(按重要性排序):
前台进程 是用户执行当前操作所需的进程。如果以下任何一个条件成立,该进程被视作前台进程:
此类进程是最重要的进程,在系统内数量有限。因此系统会尽可能地保持此类进程的正常运行。除非内存低至连此类进程都无法继续运行。
可见进程 正在运行用户当先知晓的任务,因此终止该进程会对用户体验造成明显的负面影响。如果以下任何一个条件成立,该进程被视作可见进程:
该进程运行着一个对用户可见但不在前台的 Activity(onPause 被调用)
例如应用 A 所在进程是一个前台进程,但它的前台 Activity 是一个对话框,后面显示了应用 B 的 Activity。则此时应用 B 所在的进程为 可见进程。
该进程正在运行着一个通过 startForground 启动的前台服务
系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等
此类进程被认为非常重要,除非内存低至无法保持所有 前台进程 正常运行,否则不会终止此类进程。
服务进程 包含一个已使用 startService 方法启动的 Service。虽然用户无法直接看到这些进程,但它们通常正在执行用户关心的任务(例如后台网络数据上传或下载),因此系统会始终使此类进程保持运行,除非没有足够的内存来保留所有前台和可见进程。
长时间运行(如 30 分钟或更长)的 Service 可能会被 降级 成下面要介绍的 缓存进程。这避免了超长时间运行的服务因内存泄漏或其它问题占用大量内存。
缓存进程 是目前不需要的进程,因此如果其它地方需要内存,系统会自由地杀死该类进程。为了更高效地切换应用,系统始终保持有多个 缓存进程 可用,并根据需要定期杀死最早的进程。只有在紧急情况下系统才会达到杀死所有缓存进程的地步,此时开始杀死服务进程。
其实系统内对进程优先级的划分更为详细,使用 oom_score_adj 来描述。
在 Android 的 lmk
机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj
值的取值范围,oom_adj 值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj
值越高的开始杀。进程级别以变量的形式定义在 ProcessList.java 中。
从 Android 7.0 开始,ADJ 采用 100、200、300。在这之前的版本 ADJ 采用数字 1、2、3。这样的调整可以更进一步地细化进程的优先级。
下图基于 Android 11 源码。
上图中颜色标识的便是上一小节介绍的常用的进程模式。
PERCEPTIBLE_LOW_APP_ADJ 为 Android 10 新增;
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 为 Android 9 新增。
ADJ 是以 lmk
的角度对进程优先级的描述,相对比较底层。在 Java 世界中管理着 Android 四大组件和进程的是 AMS(Activity Manager Service)。AMS 对进程优先级的描述为 procstate
(Process State),以变量的形式定义在 frameworks/base/core/java/android/app/ActivityManager.java 中。
下图基于 Android 11 源码。
?? 不同版本略有差异。
例如 Android 10 中
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3
,Android 11 删除了该属性并且值依次提前。
我们可以使用 adb shell dumpsys meminfo
命令来查看 进程 ADJ 值:
也可以使用 adb shell dumpsys activity o
查询 OOM 相关的信息。
还可以使用 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
打开图库(其它任意应用均可),此时 测试 app adj 没有变化,仍为上一个应用进程。(用户可从最近任务列表,或手势操作切回测试 app)
再次点击 home,此时上一个应用进程为 图库 所在进程。测试 app 所在进程 adj = 900,属于缓存进程,AMS 中进程状态为 CAC(缓存进程,包含 activity)
相信你已经对这部分内容有一个直观的认识了,你还可以自己尝试更多的场景。??
我们都知道,进程间通信有一个方式叫作「信号」。Linux 提供了几十种信号,分别代表不同的意义。信号之间依靠它们的值来区分。信号可以在任何时候发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就可以了。这就像一个系统应急手册,当遇到什么情况,做什么事情,都事先准备好,出了事情照着做就可以了。
一旦有信号产生,用户进程对信号有以下的处理方式:
执行默认操作
Linux 对每种信号都规定了默认操作。
捕捉信号
我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
忽略信号
有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL
和 SEGSTOP
,它们用于在任何时候中断或结束某一进程。
Linux 杀死进程的方式便是依托 SIGKILL
信号,它的值是 9。
Android 杀进程底层也是使用的信号的方式。
frameworks/base/core/java/android/os/Process.java 使用了三种信号:
SIGNAL_QUIT
= 3SIGNAL_KILL
= 9SIGNAL_USR1
= 10该类还封装了三个杀死进程的静态方法,它们最终会调用相应的 native 方法。底层通过系统调用进入内核。
?? 其中
killProcessQuiet
与killProcessGroup
被标记为 @hide,app 层的开发者只能调用 killProcess(int pid)。而 killProcess 与 killProcessQuiet 的唯一区别是 前者打印日志,后者不打印。上层(AMS)杀进程均是对这三个方法的调用。
killProcess
虽然是一个静态方法,开发人员可以调用,但 app 层只能调用该方法来实现「自杀」。如果能随意杀死其他进程,那么可就「天下大乱」了。
Process.killProcess(Process.myPid())
上一小节我们提到,信号值为 9 的信号既不能被忽略也不能被捕捉,因此直接由内核处理,无法有其他操作。这有点像「君让臣死,臣不得不死」,并且没有一丝丝犹豫,没带走一片云彩。
对信号 3 和 10,则是交由目标进程(art 虚拟机)的 SignalCatcher 线程来捕获完成相应操作的。
这部分源码详情可参见 Gityuan 的 理解杀进程的实现原理。
在 Java 世界,AMS 中封装着杀死进程的方法,不过本质上都是上面 Process 的三个 kill 方法的调用。
其中 SYSTEM_UID 指配置了与 system sharedUserId,其他权限指在 Manifest 声明相关的权限
上表中功能最强,效果最好的方法是 forceStopPackage。
force 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 方法:
该方法主要有两个操作:
- 清除进程,四大组件
- 移除 Alarm Notification
详细内容可参考 Gityuan 的 Android进程绝杀技--forceStop。虽然该文是根据 6.0 的源码解析的,但笔者对比了 Android 11 的源码,其核心逻辑没有太大变化,如果想深入了解这部分源码,这篇文章很仍有很大的参考价值。
手机设置-应用详情的强行停止,就是 force stop
只看代码比较抽象,我们来更直观的感受一下 force stop 的威力!
我们在开发过程中经常将 UI 与 Service 分离,使用不同的进程。这样做的目的是为了提高进程优先级,包含 Activity 的 Service 进程与不包含的 Service 进程 ADJ 不同。但 force stop 会将该应用的所有相关进程都 kill 掉。因此不要认为进程分离后便可逃过 force stop 「毒手」。
shareUserId
我们在 前文 已有介绍,关于 shareUserId
有两个重要的内容:
在 Gityuan Android进程绝杀技--forceStop 一文中提到使用相同的 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 备份数据
注册无障碍服务(辅助功能)
注册设备管理器
这种流氓软件应该直接送它一个操作,再也不见!??
PSS
,可以使用 adb shell dumpsys meminfo -s [process]
查看kswapd
工作的阈值范围时,kswapd
通过删除干净页和压缩脏页来回收内存lmk
工作的阈值范围时, lmk
通过杀死进程来回收内存lmk
杀死AMS
管理着进程,对应的进程优先级描述为 procstate标签:超级 显示 注意 存储器 注册 相互 创建 微信 相同
原文地址:https://www.cnblogs.com/Flywith24/p/13967503.html