1 概述
Android从诞生之初就有一个难题: 怎么最大限度的优化进程对有限的系统物理资源的使用,比如CPU、电量、内存等,同时保证良好的用户体验。
很多进程在停止和用户交互之后,会长期停留在后台,此时它们对于用户体验没有任何贡献。
Android之所以没有立刻杀掉这些进程,是出于用户恢复使用这些进程时,启动速度的考虑。但是这些进程在后台却可以持续占据使用CPU,有些会在后台持续消耗内存。
怎么在不杀掉这些进程的基础之上,最大限度的限制这些进程?
Freezer 挺身而出,通过“冰冻”的方式解决问题。
1.1 功能开启
【Android Freezer 简介】方式一 开发者选项 "Suspend execution for cached apps"。 可选项一共有三个, Device default, Enabled 和Disabled。需要注意的是,Android R上面的default 等于功能关闭;
Android S 上面是默认开启。
方式二 adb 命令: adb shell settings put global cached\_apps\_freezer
通过以上任意方式开启关闭该功能,都需要重启生效。
1.2 版本要求
Kernel 5.4, kernel 5.10或者更高版本。Pixel 4.14 和4.19的kernel 也是支持的。
Android 版本最低要求是R,但是经测试R上面功能还有些问题,建议在S上正式使用。
1.3 原理
Android按照优先级将一般的APP从高到低分为: _前台进程 --> 可感知进程--> 服务进程 --> Cached进程_。
Freezer通过冻住cached进程(如果对cached这个概念不清楚,可以参考developer.android.google.cn/guide/compo… 来迫使这些进程让出CPU,以达到优化系统资源使用的目的。
1.4 框架
文章图片
Framework 上层主要由两个类控制, OomAdjuster 负责计算APP的oom\_score\_adj,一旦某个APP的adj大于等于CACHED\_APP\_MIN\_ADJ,就会将冰冻该进程的工作委托给CachedAppOptimizer去处理,后者跑在独立的线程。
Android Q 开始,谷歌引入了cgroup抽象层,搭配使用任务配置文件,来屏蔽底层cgroup调用细节,向上提供API。cgroup抽象层编译成库libprocessgroup。抽象层通过往cgroup的文件节点写入相应的值,来触发kernel的回调。
最终kernel cgroup机制的freezer控制子系统真正实现了冰冻进程的功能。
一个进程从前台运行到被冰冻的旅程
[图片上传失败...(image-187482-1648215439544)]
2 Framework 上层
2.1 代码路径及流程
frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java
frameworks/base/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
frameworks/base/core/java/android/os/Process.java
frameworks/base/core/jni/android_util_Process.cpp
文章图片
2.2 OomAdjuster更新进程adj的场景
static final String OOM_ADJ_REASON_NONE = OOM_ADJ_REASON_METHOD + "_meh";
static final String OOM_ADJ_REASON_ACTIVITY = OOM_ADJ_REASON_METHOD + "_activityChange";
static final String OOM_ADJ_REASON_FINISH_RECEIVER = OOM_ADJ_REASON_METHOD + "_finishReceiver";
static final String OOM_ADJ_REASON_START_RECEIVER = OOM_ADJ_REASON_METHOD + "_startReceiver";
static final String OOM_ADJ_REASON_BIND_SERVICE = OOM_ADJ_REASON_METHOD + "_bindService";
static final String OOM_ADJ_REASON_UNBIND_SERVICE = OOM_ADJ_REASON_METHOD + "_unbindService";
static final String OOM_ADJ_REASON_START_SERVICE = OOM_ADJ_REASON_METHOD + "_startService";
static final String OOM_ADJ_REASON_GET_PROVIDER = OOM_ADJ_REASON_METHOD + "_getProvider";
static final String OOM_ADJ_REASON_REMOVE_PROVIDER = OOM_ADJ_REASON_METHOD + "_removeProvider";
static final String OOM_ADJ_REASON_UI_VISIBILITY = OOM_ADJ_REASON_METHOD + "_uiVisibility";
static final String OOM_ADJ_REASON_ALLOWLIST = OOM_ADJ_REASON_METHOD + "_allowlistChange";
static final String OOM_ADJ_REASON_PROCESS_BEGIN = OOM_ADJ_REASON_METHOD + "_processBegin";
static final String OOM_ADJ_REASON_PROCESS_END = OOM_ADJ_REASON_METHOD + "_processEnd";
通过更新原因可以看到,进程的状态变化,或者组件的状态变化会触发重新计算adj。
举例, 当一个APP处理广播时,就会触发重新计算这个APP的adj,此时的更新原因是OOM\_ADJ\_REASON\_START\_RECEIVER:
private final void processCurBroadcastLocked(BroadcastRecord r,
ProcessRecord app) throws RemoteException {
......
r.receiver = thread.asBinder();
r.curApp = app;
final ProcessReceiverRecord prr = app.mReceivers;
prr.addCurReceiver(r);
app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
mService.updateLruProcessLocked(app, false, null);
// Make sure the oom adj score is updated before delivering the broadcast.
// Force an update, even if there are other pending requests, overall it still saves time,
// because time(updateOomAdj(N apps)) <= N * time(updateOomAdj(1 app)).
mService.enqueueOomAdjTargetLocked(app);
mService.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER);
......
}
OomAdjuster在computeOomAdjLSP中会将该app的adj提升至FOREGROUND\_APP\_ADJ:
private boolean computeOomAdjLSP(ProcessRecord app, int cachedAdj,
ProcessRecord topApp, boolean doingAll, long now, boolean cycleReEval,
boolean computeClients) {
......
} else if (state.getCachedIsReceivingBroadcast(mTmpBroadcastQueue)) {
// An app that is currently receiving a broadcast also
// counts as being in the foreground for OOM killer purposes.
// It's placed in a sched group based on the nature of the
// broadcast as reflected by which queue it's active in.
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = (mTmpBroadcastQueue.contains(mService.mFgBroadcastQueue))
? ProcessList.SCHED_GROUP_DEFAULT : ProcessList.SCHED_GROUP_BACKGROUND;
state.setAdjType("broadcast");
procState = ActivityManager.PROCESS_STATE_RECEIVER;
if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
reportOomAdjMessageLocked(TAG_OOM_ADJ, "Making broadcast: " + app);
}
}
......
2.3 Debounce time
当某个APP进程的adj达到CACHED\_APP\_MIN\_ADJ时,CachedAppOptimizer就会调用freezeAppAsyncLSP,在debounce time之后,正式冰冻该进程, 这是为了防止进程在进入cached状态时还有任务没有完成从而间接影响用户体验。
Debounce time 在Android S 上默认是十分钟,这个值是可以在线修改的, 比如改为1秒:
adb shell device_config put activity_manager_native_boot freeze_debounce_timeout 1000
修改后的值重启会失效,变回默认值。
与异步冰冻流程相反,解冻的过程是同步进行的,一个App进程一旦被解冻成功,它就立刻恢复正常运行。
2.4 freezeProcess
文章图片
第一步,检查文件锁"/proc/locks"的状态。这是为了防止冰冻进程持有文件锁引起死锁。考虑到一些特殊场景下,进程在被冰冻的过程中拿住了文件锁,冰冻成功后还会再检查一次,发现持有锁就立刻解冻。
第二步,freeze binder。这一步禁掉该进程对同步binder请求的接收和处理,以及对异步binder请求的处理。该过程需要在100ms内成功,如果进程需要处理的请求过多导致无法完成,则再多给一轮debounce time。
第三步,setProcessFrozen,调用抽象层提供的API冰冻进程。
第四步,再次检查该进程有没有需要处理的binder请求,有则解冻进程,再多给一轮debounce time。这个检查是为一些特殊场景下,进程在被冰冻的过程中产生了binder请求。
2.5 setProcessFrozen
void android_os_Process_setProcessFrozen(
JNIEnv *env, jobject clazz, jint pid, jint uid, jboolean freeze)
{
bool success = true;
if (freeze) {
success = SetProcessProfiles(uid, pid, {"Frozen"});
} else {
success = SetProcessProfiles(uid, pid, {"Unfrozen"});
}if (!success) {
signalExceptionForGroupError(env, EINVAL, pid);
调用cgroup抽象层的SetProcessProfiles, 传入参数“Frozen”。
3 cgroup 中间抽象层
cgroup中间抽象层libprocessgroup,主要提供两个功能,其一在启动阶段,根据cgroups.json 来装载具体的cgroup; 其二根据task\_profiles.json来定义对cgroup具体的操作以及参数。 主要代码:
/system/core/libprocessgroup/
3.1 cgroups.json
示例文件:
{
"Cgroups": [
{
"Controller": "cpu",
"Path": "/dev/cpuctl",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "memory",
"Path": "/dev/memcg",
"Mode": "0700",
"Optional": true
}
],
"Cgroups2": {
"Path": "/sys/fs/cgroup",
"Mode": "0755",
"UID": "system",
"GID": "system",
"Controllers": [
{
"Controller": "freezer",
"Path": ".",
"Mode": "0755",
"UID": "system",
"GID": "system"
}
]
}
}
该文件分为两部分,"Cgroups" 描述cgroup v1 的控制子系统 和"Cgroups2" 描述cgroup v2的控制子系统。如果不了解cgroup 两个版本,可以参看以下网址:
www.kernel.org/doc/html/la… www.kernel.org/doc/html/la…
目前v1和v2是共存的, freezer所使用的是v2,对应根目录是/sys/fs/cgroup, UID和GID都是system。
启动阶段,Init在SetupCgroupAction中通过调用CgroupSetup,解析cgroups.json文件,完成对cgroup的装载。
需要注意的是,cgroups.json文件可能不止一个:
/system/core/libprocessgroup/profiles/cgroups.json//默认文件,都有
/system/core/libprocessgroup/profiles/cgroups_.json//API级别的文件,可能有
/vendor/xxx/cgroups.json//vendor自定义的文件
这三个文件加载的顺序是: 默认--> API级别 --> vendor,于是就存在一个覆盖的流程。只要后面一个文件中定义的"Controller"与前面的字符串相同,就会覆盖前者的定义。
形成的层级结构是这样的:
文章图片
Android freezer 在/sys/fs/cgroup下建立了以uid/pid命名的二级子目录,对uid节点的控制会同步应用于其下面所有的pid节点。 uid在应用安装的时候确定, pid在应用使用的时候产生。如果manifest中使用了android:process,则在当前uid目录下建立一个新的pid开头的目录; 如果进程主动调用了fork产生子进程,则不会产生新的pid目录,而是放在父进程目录下,这就意味着父进程被冰冻的时候同目录的子进程也会被冰冻。
3.2 task\_profiles.json
示例 task\_profiles.json
{
"Attributes": [
{
"Name": "UClampLatencySensitive",
"Controller": "cpu",
"File": "cpu.uclamp.latency_sensitive"
},
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
],
"Profiles": [
{
"Name": "Frozen",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "FreezerState",
"Value": "1"
}
}
]
},
{
"Name": "Unfrozen",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "FreezerState",
"Value": "0"
}
}
]
},
{
"Name": "CpuPolicySpread",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "UClampLatencySensitive",
"Value": "1"
}
}
]
}
],
"AggregateProfiles": [
{
"Name": "SCHED_SP_BACKGROUND",
"Profiles": [ "HighEnergySaving", "LowIoPriority", "TimerSlackHigh" ]
}
]
}
task\_profiles.json主要由三部分组成, Attributes, Profiles 和AggregateProfiles。
Attributes定义属性, 包含名称,控制子系统 以及接口文件。比如上面的示例文件中,定义了名称“FreezerState”的属性, 使用到的控制子系统是freezer, 接口文件是"cgroup.freeze"。
Profiles 定义操作。 一种是SetAttribute,引用前面定义的Attributes项,添加参数(Value);一种是JoinCgroup, 包含控制子系统及其路径。
比如上面的示例文件中,名称“Frozen”的项,使用SetAttribute的action,引用“FreezerState” 这个attribute,参数为1,也就是往cgroup.freeze写入1,这正是2.5小节SetProcessProfiles的具体实现。
AggregateProfiles 是Profiles项的组合使用。
task\_profiles.json也可能不止一个:
/system/core/libprocessgroup/profiles/task_profiles.json
/system/core/libprocessgroup/profiles/task_profiles_.json
/vendor/xxx/task_profiles.json
加载、覆盖的顺序和cgroups.json类似,按照"Name"来匹配,只要两个文件中定义了同名项,后者就会覆盖前者的定义。
需要注意的是,厂商需要定制自己的cgroup 或者task profile时,应该去修改vendor下面对应的文件, 而不要去改默认的或者API对应的文件:
/vendor/xxx/cgroups.json
/vendor/xxx/task_profiles.json
4 Kernel
从cgroup的抽象层到 kernel 通过文件写入:
/sys/fs/cgroup/
对该文件的写入触发写回调cgroup\_freeze\_write, 通过当前的kernfs找到对应的cgroup, 将这个cgroup下的所有进程以及子进程都freeze。详细代码流程留待下一篇再述。
{
.name = "cgroup.freeze",
.flags = CFTYPE_NOT_ON_ROOT,
.seq_show = cgroup_freeze_show,
.write = cgroup_freeze_write,
},
5 观察冰冻进程
5.1 使用ps命令查看被冻住的进程
u0_a108 177881045 1347272 82528 do_freezer_trap0 S com.android.testapp
可以看到该进程被冻住之后,处于S 睡眠态, WCHAN 是do\_freezer\_trap。
5.2 致命信号
冻住的进程可以被致命信号杀掉。这里的致命信号是指默认的信号处理函数是"terminate"或者"dump core"的信号,不可以被忽略,也不可以重新注册处理函数。
举个例子,信号SIGSEGV是杀不掉被冻住的进程的,因为它的信号处理函数已经被重新注册为debuggerd\_signal\_handler了。
5.3 binder 状态
向冻住的进程发送同步binder请求会立刻收到错误码BR\_FROZEN\_REPLY。
进程在被解冻之前系统会先检查该进程有无在冰冻期间收到同步binder请求(SYNC\_RECEIVED\_WHILE\_FROZEN), 有则会杀掉该进程。
void unfreezeAppLSP(ProcessRecord app) {
final int pid = app.getPid();
final ProcessCachedOptimizerRecord opt = app.mOptRecord;
......
try {
int freezeInfo = getBinderFreezeInfo(pid);
if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
Slog.d(TAG_AM, "pid " + pid + " " + app.processName + " "
+ " received sync transactions while frozen, killing");
app.killLocked("Sync transaction while in frozen state",
ApplicationExitInfo.REASON_OTHER,
ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION, true);
processKilled = true;
}
if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
Slog.d(TAG_AM, "pid " + pid + " " + app.processName + " "
+ " received async transactions while frozen");
}
} catch (Exception e) {
Slog.d(TAG_AM, "Unable to query binder frozen info for pid " + pid + " "
+ app.processName + ". Killing it. Exception: " + e);
app.killLocked("Unable to query binder frozen stats",
ApplicationExitInfo.REASON_OTHER,
ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
processKilled = true;
}
......
}
冻住的进程可以接收异步binder请求,但是不会处理,只是放入binder buffer, 过多的请求会导致buffer耗尽。
6 其他
问题 1. freezer有无豁免机制?
持有INSTALL\_PACKAGES的APP不会被冰冻。普通APP不应该考虑逃脱冰冻,而是应该严格遵守开发规范。
问题 2. 有无API提供给APP查询进程是否被冰冻?
目前没有。
推荐阅读
- Android小项目|Android页面的跳转
- 在 Android TV 上构建优秀的播放体验|中文字幕视频
- 程序员|来看看移动端小程序技术的前世今生!重难点整理
- Android 基础知识课程助您轻松构建应用
- Jetpack|Kotlin 之 协程(二)启动取消协程
- android|android studio离线安装插件,Android Studio手动安装Genymotion插件的方法
- 模型部署|通过MACE在Android手机上部署深度学习模型
- Android|后台启动 Activity
- Android|Android 基础控件学习--Timer