提示:实现App 保活-进程保活机制
文章目录
- 前言-需求-场景
- 一、参考资料
- 二、lmk杀进程相关知识
- 二、涉及到文件修改
- 三、实现方案
-
- 1、配置文件-cmhi_permissions.xml
- 2、资源拷贝-先拷贝到system分区-机器第一次开机再拷贝到data分区
-
- [拷贝资源到system分区- 配置属性key](#拷贝资源到system分区- 配置属性key)
- 机器第一次开机-从system分区拷贝资源到data分区
- 3、在AMS中读取配置文件-解析-写数据到属性中
-
- [解析xml 工具类-PermissionConfigParser](#解析xml 工具类-PermissionConfigParser)
- AMS中解析xml资源-设置白名单数据到属性中去
- 4、在守护进程中根据白名单过滤-防止白名单应用被杀掉
- 四、低内存下-监控守护进程实验
- 总结
前言-需求-场景
需求
Android体系下,类似的保活需求、应用进程保活、应用进程不被杀死等需求,其实就是保活相关需求。更细一点的就是App中的服务不被杀死等,早年前特别是国产RK、全志等soc 平台在商显领域需求特别多,因为内存紧张、机器性能本身不高。为了稳健性,保活机制还是需要的。
比如如下 客户需求:系统在监测到/data/system/cmhi_permissions.xml 中 allow-app-alive-inlmkd 的应用后,直接将 app 添加到 LowMemoryKiller 白名单中,保证 app 不会在低内存时被 LowMemoryKiller 杀死进程

思路
这里思路一般有3种,都是实践中的解决方案:
- 最low 的方案:系统服务中开一个定时检测机制,轮训检测App
在这里插入代码片进程是否还活着,如果死掉了就重启进程或者通知进程。 - AMS 处理方案:让system_server进程针对对应包名把adj值强制调低,但是这样可能只能说大大提升自己不被杀概率。实际上是AMS来记录这个adj 值,低内存下会通知底层来处理kill 方案。
- 直接在底层模块中来实现判断包名或者进程名,如果是白名单不去杀死不就行了吗。
分析:如上,第一种方案还是会杀死进程的,需要重启且不具备时效性;第二种方案知识大概率降低被杀死概率;第三种方案才是最优解,绝不杀死。
涉及到知识点
这里面涉及到知识点其实蛮多的,下面简要说明:
- 分区概念,如上需求中涉及到
/data/system目录下的文件,这个是data 分区,实现方案中一定是从已有分区拷贝文件到 data分区的 - 属性概念:白名单数据一般都是以包名为主体,那么这些白名单放在哪里呢? 不会 直接读取文件数据吧,而且最终是需要守护进程c 代码中获取数据的,所以我们必须用属性来实现数据同步。
- 在守护进程里面处理低内存拦截机制,那么必须具体
lmk-lowmemorykiller知识点,即lmk守护进程和lowmemorykiller机制
一、参考资料
Android 系统属性添加篇
RK-Android11-系统增加一个属性值
Framework 层属性机制Settings.System, Settings.Secure和Settings.Global存储及应用
系统拷贝文件到data分区-/data/system目录-实战拷贝资源到vendor分区
如何避免重要app进程被系统lmk-lowmemorykiller杀掉-实战源码
二、lmk杀进程相关知识
这块知识整体比较复杂,不过总结一下主要就分为以下几个部分:
-
1、systemserver进程的AMS,它的职责是:策略收集并更新各个app进程的adj值,并且把各个进程adj值通过socket传递给远端守护进程lmkd
-
2、远端lmkd守护进程,这块其实分为两个新老版本阶段
在老版本安卓kernel是有lmk模块,所以lmkd其实不负责任何的杀进程动作,只负责写入相关的进程adj值到字节节点
新版本安卓kernel统一没有了lmk模块,这个时候lmkd就需要负责相关的检查内存,遍历进程,杀进程等业务

二、涉及到文件修改
修改的文件
java
\device\mediatek\system\common\device.mk 预置资源到system 分区、属性定义
\system\memory\lmkd\lmkd.cpp 守护进程-实现白名单过滤匹配,实现白名单机制
\system\core\rootdir\init.rc 系统第一次启动时候,拷贝system 分区中预拷贝的资源文件到data分区
\frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java AMS 读取配置资源,设置属性文件值
新增的文件
java
\device\mediatek\system\common\fise\cmhi_permissions.xml 待 拷贝资源文件,存储源默认数据
\frameworks\base\services\core\java\com\android\server\utils\PermissionConfigParser.java 解析资源文件工具类,就是一个XML 工具
三、实现方案
1、配置文件-cmhi_permissions.xml
路径:\device\mediatek\system\common\fise\cmhi_permissions.xml ,其中 allow-app-alive-inlmkd 节点为守护 ap白名单
java
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<permissions>
<allow-app-alive-inlmkd package="com.ss.android.ugc.live" />
<allow-app-alive-inlmkd package="com.android.permissioncontroller" />
<allow-app-alive-inlmkd package="com.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedP" />
<allow-app-alive-inlmkd package="com.qiyi.video.pad" />
<allow-app-alive-inlmkd package="com.abupdate.fota_demo_iot" />
<allow-app-alive-inlmkd package="com.sohu.inputmethod.sogou" />
<allow-system-alert-window package="com.deling.launcher"/>
<allow-system-alert-window package="aaaa.bbb.cccc"/>
<allow-system-alert-window package="eee.fff.ddd"/>
<allow-app-self-update package="com.mqqqqq.jj"/>
<allow-app-self-update package="com.bo.test"/>
<allow-app-self-update package="com.jijiji.qqqq"/>
</permissions>
2、资源拷贝-先拷贝到system分区-机器第一次开机再拷贝到data分区
强烈建议参考之前笔记 系统拷贝文件到data分区-/data/system目录-实战拷贝资源到vendor分区
拷贝资源到system分区- 配置属性key
路径:\device\mediatek\system\common\device.mk
配置拷贝资源脚本,如下:
java
# modify by fangchen start whitelist
PRODUCT_COPY_FILES += $(LOCAL_PATH)/fise/cmhi_permissions.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/cmhi_permissions.xml:mtk
# modify by fangchen end whitelist
persist.sys.allow_app_alive= \


机器第一次开机-从system分区拷贝资源到data分区
路径:\system\core\rootdir\init.rc ,脚本如下:
java
on post-fs-data
mark_post_data
.......
mkdir /data/system 0771 system system
chmod 0771 /data/system
chown system system /data/system
copy /system/etc/cmhi_permissions.xml /data/system/cmhi_permissions.xml
chmod 0644 /data/system/cmhi_permissions.xml
chown system system /data/system/cmhi_permissions.xml
3、在AMS中读取配置文件-解析-写数据到属性中
解析xml 工具类-PermissionConfigParser
路径:\frameworks\base\services\core\java\com\android\server\utils\PermissionConfigParser.java
java
package com.android.server.utils;
// PermissionConfigParser.java
//package com.android.server.pm;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import android.util.Log;
import android.util.Slog;
public class PermissionConfigParser {
private static final String TAG = "PermissionConfigParser";
private static final String CONFIG_FILE = "/data/system/cmhi_permissions.xml";
public static ArrayList<String> parsePermissionPackages(String permissionTag) {
ArrayList<String> packageList = new ArrayList<>();
File configFile = new File(CONFIG_FILE);
if (!configFile.exists()) {
Slog.w(TAG, "Config file not exist: " + CONFIG_FILE);
return packageList;
}
FileReader fileReader = null;
try {
fileReader = new FileReader(configFile);
XmlPullParser parser = Xml.newPullParser();
parser.setInput(fileReader);
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
String tagName = parser.getName();
if (permissionTag.equals(tagName)) {
String packageName = parser.getAttributeValue(null, "package");
if (packageName != null && !packageName.isEmpty()) {
packageList.add(packageName);
}
}
}
eventType = parser.next();
}
} catch (Exception e) {
Slog.e(TAG, "Failed to parse config file", e);
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (Exception e) {
// ignore
}
}
}
return packageList;
}
public static ArrayList<String> getAllowAppAlivePackages() {
return parsePermissionPackages("allow-app-alive-inlmkd");
}
public static ArrayList<String> getAllowAppAlertPackages() {
return parsePermissionPackages("allow-system-alert-window");
}
public static ArrayList<String> getAllowAppUpdatePackages() {
return parsePermissionPackages("allow-app-self-update");
}
public static String toPropertyString(ArrayList<String> packages) {
if (packages == null || packages.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < packages.size(); i++) {
sb.append(packages.get(i));
if (i < packages.size() - 1) {
sb.append(",");
}
}
return sb.toString();
}
}
AMS中解析xml资源-设置白名单数据到属性中去
路径:\frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
java
public ActivityManagerService(Context systemContext, ActivityTaskManagerService atm) {
LockGuard.installLock(this, LockGuard.INDEX_ACTIVITY);
mInjector = new Injector(systemContext);
mContext = systemContext;
.............
initHjqPermissionsReceiver(mContext); //新增解析方法
}
private void parseWhiteListPermission() {
Log.d(TAG,"========parseWhiteListPermission==========");
new Thread(() -> {
try {
Log.d(TAG,"======deal Thread =====================");
ArrayList<String> alivePackages = PermissionConfigParser.getAllowAppAlivePackages(); //解析xml 获取到数据
String propAliveValue = PermissionConfigParser.toPropertyString(alivePackages); //集合数据转String数据
SystemProperties.set("persist.sys.allow_app_alive", propAliveValue); //设置白名单到属性中存储
ArrayList<String> alertPackages = PermissionConfigParser.getAllowAppAlertPackages();
String propAlertValue = PermissionConfigParser.toPropertyString(alertPackages);
SystemProperties.set("persist.sys.allow_app_alert", propAlertValue);
for (String packageName : alertPackages) {
Thread.sleep(100);
boolean isSuccess = AppOpsReflection.setSystemAlertWindowMode(
mContext,
packageName,
true
);
Log.d(TAG, packageName+" alert window permissions is set result:"+isSuccess);
}
ArrayList<String> updatePackages = PermissionConfigParser.getAllowAppUpdatePackages();
String propUpdateValue = PermissionConfigParser.toPropertyString(updatePackages);
SystemProperties.set("persist.sys.allow_app_update", propUpdateValue);
Log.d(TAG, "Set allow_app_alive property: " + propAliveValue);
Log.d(TAG, "persist.sys.allow_app_alert: " + propAlertValue);
Log.d(TAG, "persist.sys.allow_app_update: " + propUpdateValue);
} catch (Exception e) {
Thread.currentThread().interrupt();
Log.e(TAG, "exception=============");
}
}).start();
}
4、在守护进程中根据白名单过滤-防止白名单应用被杀掉
路径:\system\memory\lmkd\lmkd.cpp
新增白名单方法
读取属性中去,然后判断当前 task_name 也就是进程名是不是白名单内部的值,决定是否拦截不被杀死。
java
bool is_in_white_list(char* task_name) {
char buf[500] = {};
int len = property_get("persist.allow_app_alive_inlmkd.pkg", buf, "");
if (len > 0) {
if (strstr(buf,task_name)) {
ALOGE("is_in_white_list buf %s contain of task_name %s", buf,task_name);
return true;
}
}
return false;
}
拦截机制
在 kill_one_process 方法中进行是否是白名单判断
java
/* Kill one process specified by procp. Returns the size (in pages) of the process killed */
static int kill_one_process(struct proc* procp, int min_oom_score, struct kill_info *ki,
union meminfo *mi, struct wakeup_info *wi, struct timespec *tm) {
int pid = procp->pid;
int pidfd = procp->pidfd;
uid_t uid = procp->uid;
char *taskname;
.............................
taskname = proc_get_name(pid, buf, sizeof(buf));
// taskname will point inside buf, do not reuse buf onwards.
if (!taskname) {
goto out;
}
ALOGE(" will kill_one_process taskname==wfc= %s",taskname);
// modify by fang chen start
if (is_in_white_list(taskname)) {
ALOGE(" not kill_one_process is_in_white_list taskname %s",taskname);
goto out;
}
...................
}
四、低内存下-监控守护进程实验
就是复现低内存情况下看日志打印,是否可以拦截。可参考内存压测关联知识点 lmk内存压力测试工具mem-pressure源码剖析
压测方法:
- 自己在源码中编译压测 工具类,push 到机器,然后进行压测
- 下载安装大量消耗资源的应用,比如视频类、K歌类、游戏类 并安装各种apk,并逐步打开各个apk软件,达到消耗内存的目的。
会看到如下打印日志: 这些日志不就是我们在守护进程代码里面打印的日志吗? 说明实验是没有问题的。
java
lowmemorykiller: will kill_one_process taskname==wfc= com.sohu.inputmethod.sogou:home
lowmemorykiller: will kill_one_process taskname==wfc= com.abupdate.fota_demo_iot
lowmemorykiller: will kill_one_process taskname==wfc= com.qiyi.video.pad:downloader
lowmemorykiller: will kill_one_process taskname==wfc= com.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedProcessService0:0
lowmemorykiller: will kill_one_process taskname==wfc= com.android.permissioncontroller
lowmemorykiller: will kill_one_process taskname==wfc= com.ss.android.ugc.live

总结
- app保活,其实是一个常规需求,但是很多情况下我们并没有在系统中真正实现保活,基本上都是死掉后重启机制实现,有一定的时效性和弊端
- 这里实现保活其实涉及到很多知识点,这里只是通过需求把各个知识点串联起来了
- 涉及到属性定义是基本需求、文件拷贝到对应分区的具体实现机制根据不同情况来具体实现,这里面主要是分区的知识点。
- 守护进程才是真正实现需求,我们直接在守护进程里面完成白名单过滤拦截的。