App操作影响进程优先级
本篇文章是后台杀死系列的最后一篇,主要探讨一下进程的保活,Android本身设计的时候是非常善良的,它希望进程在不可见或者其他一些场景下APP要懂得主动释放,可是Android低估了”贪婪“,尤其是很多国产APP,只希望索取来提高自己的性能,不管其他APP或者系统的死活,导致了很严重的资源浪费,这也是Android被iOS诟病的最大原因。本文的保活手段也分两种:遵纪守法的进程保活与流氓手段换来的进程保活。
声明:坚决反对流氓手段实现进程保活 坚决反对流氓进程保活 坚决反对流氓进程保活 “请告诉产品:无法进入白名单”
LowmemoryKiller会在内存不足的时候扫描所有的用户进程,找到不是太重要的进程杀死,至于LowmemoryKiller杀进程够不够狠,要看当前的内存使用情况,内存越少,下手越狠。在内核中,LowmemoryKiller.c定义了几种内存回收等级如下:(也许不同的版本会有些不同)
static short lowmem_adj[6] = {
0,
1,
6,
12,
};
static int lowmem_adj_size = 4;
static int lowmem_minfree[6] = {
3 * 512, /* 6MB */
2 * 1024, /* 8MB */
4 * 1024, /* 16MB */
16 * 1024, /* 64MB */
};
static int lowmem_minfree_size = 4;
lowmem_adj中各项数值代表阈值的警戒级数,lowmem_minfree代表对应级数的剩余内存,两者一一对应,比如当系统的可用内存小于6MB时,警戒级数为0;当系统可用内存小于8M而大于6M时,警戒级数为1;当可用内存小于64M大于16MB时,警戒级数为12。LowmemoryKiller就是根据当前系统的可用内存多少来获取当前的警戒级数,如果进程的oom_adj大于警戒级数并且占内存最大,将会被优先杀死, 具有相同omm_adj的进程,则杀死占用内存较多的。omm_adj越小,代表进程越重要。一些前台的进程,oom_adj会比较小,而后台的服务,omm_adj会比较大,所以当内存不足的时候,Lowmemorykiller先杀掉的是后台服务而不是前台的进程。对于LowmemoryKiller的杀死,这里有一句话很重要,就是: 具有相同omm_adj的进程,则杀死占用内存较多的,因此,如果我们的APP进入后台,就尽量释放不必要的资源,以降低自己被杀的风险。那么如何释放呢?onTrimeMemory是个不错的时机,而onLowmemory可能是最后的稻草,下面复习一下,LowmemoryKiller如何杀进程的,简单看一下实现源码(4.3):(其他版本原理大同小异)
static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
...
<!--关键点1 获取free内存状况-->
int other_free = global_page_state(NR_FREE_PAGES);
int other_file = global_page_state(NR_FILE_PAGES);
<!--关键点2 找到min_adj -->
for(i = 0; i < array_size; i++) {
if (other_free < lowmem_minfree[i] &&
other_file < lowmem_minfree[i]) {
min_adj = lowmem_adj[i];
break;
}
}
<!--关键点3 找到p->oomkilladj>min_adj并且oomkilladj最大,内存最大的进程-->
for_each_process(p) {
// 找到第一个大于等于min_adj的,也就是优先级比阈值低的
if (p->oomkilladj < min_adj || !p->mm)
continue;
// 找到tasksize这个是什么呢
tasksize = get_mm_rss(p->mm);
if (tasksize <= 0)
continue;
if (selected) {
// 找到优先级最低,并且内存占用大的
if (p->oomkilladj < selected->oomkilladj)
continue;
if (p->oomkilladj == selected->oomkilladj &&
tasksize <= selected_tasksize)
continue;
}
selected = p;
selected_tasksize = tasksize;
lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
p->pid, p->comm, p->oomkilladj, tasksize);
}
if(selected != NULL) {...
force_sig(SIGKILL, selected);
}
return rem;
}
这里先看一下关键点1,这里是内核获取当前的free内存状况,并且根据当前空闲内存计算出当前后台杀死的等级(关键点2),之后LowmemoryKiller会遍历所有的进程,找到优先级低并且内存占用较大的进程,如果这个进程的p->oomkilladj>min_adj,就表示这个进程可以杀死,LowmemoryKiller就会送过发送SIGKILL信号杀死就进程,注意,lmkd会先找优先级低的进程,如果多个进程优先级相同,就优先杀死内存占用高的进程,这样就为我们提供了两种进程包活手段:
不过大多数情况下,Android对于进程优先级的管理都是比较合理,即使某些场景需要特殊手段提高优先级,Android也是给了参考方案的,比如音频播放,UI隐藏的时候,需要将Sevice进程设置成特定的优先级防止被后台杀死,比如一些备份的进程也需要一些特殊处理,但是这些都是在Android允许的范围内的,所以绝大多数情况下,Android是不建议APP自己提高优先级的,因为这会与Android自身的的进程管理相悖,换句话说就是耍流氓。这里先讨论第二种情况,通过合理的释放内存降低被杀的风险,地主不想被杀,只能交公粮,自裁保身,不过这里也要看自裁的时机,什么时候瘦身比较划算,O(∩_∩)O哈哈~!这里就牵扯到有一个onTrimeMemory函数,该函数是一个系统回调函数,主要是Android系统经过综合评估,给APP一个内存裁剪的等级,比如当内存还算充足,APP退回后台时候,会收到TRIM_MEMORY_UI_HIDDEN等级的裁剪,就是告诉APP,释放一些UI资源,比如大量图片内存,一些引入图片浏览缓存的场景,可能就更加需要释放UI资源,下面来看下onTrimeMemory的回调时机及APP应该做出相应处理。
OnTrimMemory是在Android 4.0引入的一个回调接口,其主要作用就是通知应用程序在不同的场景下进行自我瘦身,释放内存,降低被后台杀死的风险,提高用户体验,由于目前APP的适配基本是在14之上,所以不必考虑兼容问题。onTrimeMemory支持不同裁剪等级,比如,APP通过HOME建进入后台时,其优先级(oom_adj)就发生变化,从未触发onTrimeMemory回调,这个时候系统给出的裁剪等级一般是TRIM_MEMORY_UI_HIDDEN,意思是,UI已经隐藏,UI相关的、占用内存大的资源就可以释放了,比如大量的图片缓存等,当然,还会有其他很多场景对应不同的裁剪等级。因此,需要弄清楚两个问题:
先看下ComponentCallbacks2中定义的不同裁剪等级的意义:这里一共定义了4+3共7个裁剪等级,为什么说是4+3呢?因为有4个是针对后台进程的,还有3个是针对前台(RUNNING)进程的,目标对象不同,具体看下分析
裁剪等级 | 数值 | 目标进程 |
---|---|---|
TRIM_MEMORY_COMPLETE | 80 | 后台进程 |
TRIM_MEMORY_MODERATE | 60 | 后台进程 |
TRIM_MEMORY_BACKGROUND | 40 | 后台进程 |
TRIM_MEMORY_UI_HIDDEN | 20 | 后台进程 |
TRIM_MEMORY_RUNNING_CRITICAL | 15 | 前台RUNNING进程 |
TRIM_MEMORY_RUNNING_LOW | 10 | 前台RUNNING进程 |
TRIM_MEMORY_RUNNING_MODERATE | 5 | 前台RUNNING进程 |
其意义如下:
以下三个等级针对前台运行应用
以上抽象的说明了一下Android既定参数的意义,下面看一下onTrimeMemory回调的时机及原理,这里采用6.0的代码分析,因为6.0比之前4.3的代码清晰很多:当用户的操作导致APP优先级发生变化,就会调用updateOomAdjLocked去更新进程的优先级,在更新优先级的时候,会扫描一遍LRU进程列表, 重新计算进程的oom_adj,并且参考当前系统状况去通知进程裁剪内存(这里只是针对Android Java层APP),这次操作一般发生在打开新的Activity界面、退回后台、应用跳转切换等等,updateOomAdjLocked代码大概600多行,比较长,尽量精简后如下,还是比较长,这里拆分成一段段梳理:
final void updateOomAdjLocked() {
final ActivityRecord TOP_ACT = resumedAppLocked();
<!--关键点1 找到TOP——APP,最顶层显示的APP-->
final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
final long oldTime = SystemClock.uptimeMillis() - ProcessList.MAX_EMPTY_TIME;
mAdjSeq++;
mNewNumServiceProcs = 0;
final int emptyProcessLimit;
final int hiddenProcessLimit;
<!--关键点2 找到TOP——APP,最顶层显示的APP-->
// 初始化一些进程数量的限制:
if (mProcessLimit <= 0) {
emptyProcessLimit = hiddenProcessLimit = 0;
} else if (mProcessLimit == 1) {
emptyProcessLimit = 1;
hiddenProcessLimit = 0;
} else {
// 空进程跟后台非空缓存继承的比例
emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
cachedProcessLimit = mProcessLimit - emptyProcessLimit;
}
<!--关键点3 确定下进程槽 3个槽->
int numSlots = (ProcessList.HIDDEN_APP_MAX_ADJ - ProcessList.HIDDEN_APP_MIN_ADJ + 1) / 2;
// 后台进程/前台进程/空进程
int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
int emptyFactor = numEmptyProcs/numSlots;
if (emptyFactor < 1) emptyFactor = 1;
int hiddenFactor = (mNumHiddenProcs > 0 ? mNumHiddenProcs : 1)/numSlots;
if (hiddenFactor < 1) hiddenFactor = 1;
int stepHidden = 0;
int stepEmpty = 0;
int numHidden = 0;
int numEmpty = 0;
int numTrimming = 0;
mNumNonHiddenProcs = 0;
mNumHiddenProcs = 0;
int i = mLruProcesses.size();
// 优先级
int curHiddenAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
// 初始化的一些值
int nextHiddenAdj = curHiddenAdj+1;
// 优先级
int curEmptyAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
// 有意思
int nextEmptyAdj = curEmptyAdj+2;
这前三个关键点主要是做了一些准备工作,关键点1 是单独抽离出TOP_APP,因为它比较特殊,系统只有一个前天进程,关键点2主要是根据当前的配置获取后台缓存进程与空进程的数目限制,而关键点3是将后台进程分为三备份,无论是后台进程还是空进程,会间插的均分6个优先级,一个优先级是可以有多个进程的,而且并不一定空进程的优先级小于HIDDEN进程优先级。
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
if (!app.killedByAm && app.thread != null) {
app.procStateChanged = false;
<!--关键点4 计算进程的优先级或者缓存进程的优先级->
// computeOomAdjLocked计算进程优先级,但是对于后台进程和empty进程computeOomAdjLocked无效,这部分优先级是AMS自己根据LRU原则分配的
computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
//还未最终确认,有些进程的优先级,比如只有后台activity或者没有activity的进程,
<!--关键点5 计算进程的优先级或者缓存进程的优先级->
if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
switch (app.curProcState) {
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
app.curRawAdj = curCachedAdj;
<!--关键点6 根据LRU为后台进程分配优先级-->
if (curCachedAdj != nextCachedAdj) {
stepCached++;
if (stepCached >= cachedFactor) {
stepCached = 0;
curCachedAdj = nextCachedAdj;
nextCachedAdj += 2;
if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
}
}
}
break;
default:
<!--关键点7 根据LRU为后台进程分配优先级-->
app.curRawAdj = curEmptyAdj;
app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
if (curEmptyAdj != nextEmptyAdj) {
stepEmpty++;
if (stepEmpty >= emptyFactor) {
stepEmpty = 0;
curEmptyAdj = nextEmptyAdj;
nextEmptyAdj += 2;
if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
}
}
}
break;
}
}
<!--关键点8 设置优先级-->
applyOomAdjLocked(app, true, now, nowElapsed);
上面的这几个关键点主要是为所有进程计算出其优先级oom_adj之类的值,对于非后台进程,比如HOME进程 服务进程,备份进程等都有自己的独特的计算方式,而剩余的后台进程就根据LRU三等分配优先级。
<!--关键点9 根据缓存进程的数由AMS选择性杀进程,后台进程太多-->
switch (app.curProcState) {
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
mNumCachedHiddenProcs++;
numCached++;
if (numCached > cachedProcessLimit) {
app.kill("cached #" + numCached, true);
}
break;
case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
if (numEmpty > ProcessList.TRIM_EMPTY_APPS
&& app.lastActivityTime < oldTime) {
app.kill("empty for "
+ ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
/ 1000) + "s", true);
} else {
numEmpty++;
if (numEmpty > emptyProcessLimit) {
app.kill("empty #" + numEmpty, true);
}
}
break;
default:
mNumNonCachedProcs++;
break;
}
<!--关键点10 计算需要裁剪进程的数目-->
if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
&& !app.killedByAm) {
// 比home高的都需要裁剪,不包括那些等级高的进程
numTrimming++;
}
}
}
上面的两个关键点是看当前后台进程是否过多或者过老,如果存在过多或者过老的后台进程,AMS是有权利杀死他们的。之后才是我们比较关心的存活进程的裁剪:
final int numCachedAndEmpty = numCached + numEmpty;
int memFactor;
<!--关键点11 根据后台进程数目确定当前系统的内存使用状况 ,确立内存裁剪等级(内存因子)memFactor,android的理念是准许存在一定数量的后台进程,并且只有内存不够的时候,才会缩减后台进程-->
if (numCached <= ProcessList.TRIM_CACHED_APPS
&& numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
// 等级高低 ,杀的越厉害,越少,需要约紧急的时候才杀
if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {//3
memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
} else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) { //5
memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
} else {
memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
}
} else {
// 后台进程数量足够说明内存充足
memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
}
<!--关键点12 根据内存裁剪等级裁剪内存 Android认为后台进程不足的时候,内存也不足-->
if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
if (mLowRamStartTime == 0) {
mLowRamStartTime = now;
}
int step = 0;
int fgTrimLevel;
// 内存不足的时候,也要通知前台或可见进程进行缩减
switch (memFactor) {
case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
break;
case ProcessStats.ADJ_MEM_FACTOR_LOW:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
break;
default:
fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
break;
}
int factor = numTrimming/3;
int minFactor = 2;
if (mHomeProcess != null) minFactor++;
if (mPreviousProcess != null) minFactor++;
if (factor < minFactor) factor = minFactor;
int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
关键点11这里不太好理解:Android系统根据后台进程的数目来确定当前系统内存的状况,后台进程越多,越说明内存并不紧张,越少,说明越紧张,回收等级也就越高,如果后台进程的数目较多,内存裁剪就比较宽松是ProcessStats.ADJ_MEM_FACTOR_NORMAL,如果不足,则再根据缓存数目划分等级。以6.0源码来说:
与之对应的关键点12,是确立前台RUNNING进程(也不一定是前台显示)的裁剪等级。
之后就真正开始裁剪APP,这里先看后台进程不足的情况的裁剪,这部分相对复杂一些:
<!--裁剪后台进程-->
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
if (allChanged || app.procStateChanged) {
setProcessTrackerStateLocked(app, trackerMemFactor, now);
app.procStateChanged = false;
}
// PROCESS_STATE_HOME = 12;
//PROCESS_STATE_LAST_ACTIVITY = 13; 退到后台的就会用
// 优先级比较低,回收等级比较高ComponentCallbacks2.TRIM_MEMORY_COMPLETE
// 当curProcState > 12且没有被am杀掉的情况;上面的update的时候,在kill的时候,是会设置app.killedByAm的
//裁剪的话,如果 >= ActivityManager.PROCESS_STATE_HOME,老的裁剪等级较高,不重要,越新鲜的进程,裁剪等级越低
if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
&& !app.killedByAm) {
// 先清理最陈旧的 ,最陈旧的那个遭殃
if (app.trimMemoryLevel < curLevel && app.thread != null) {
try {
app.thread.scheduleTrimMemory(curLevel);
} catch (RemoteException e) {
}
}
app.trimMemoryLevel = curLevel;
step++;
// 反正一共就三个槽,将来再次刷新的 时候,要看看是不是从一个槽里面移动到另一个槽,
// 没有移动,就不需要再次裁剪,等级没变
if (step >= factor) {
step = 0;
switch (curLevel) {
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
break;
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
break;
}
}
}
上面的这部分是负责 app.curProcState >= ActivityManager.PROCESS_STATE_HOME这部分进程裁剪,主要是针对后台缓存进程,一般是oom_adj在9-11之间的进程,根据LRU确定不同的裁减等级。
else {
if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
|| app.systemNoUi) && app.pendingUiClean) {
// 释放UI
final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
if (app.trimMemoryLevel < level && app.thread != null) {
try {
app.thread.scheduleTrimMemory(level);
} catch (RemoteException e) {
}
}
app.pendingUiClean = false;
}
// 启动的时候会回调一遍,如果有必要,启动APP的时候,app.trimMemoryLevel=0
if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {
try {
app.thread.scheduleTrimMemory(fgTrimLevel);
} catch (RemoteException e) {
}
}
app.trimMemoryLevel = fgTrimLevel;
}
}
}
而这里的裁剪主要是一些优先级较高的进程,其裁剪一般是 ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ,由于这部分进程比较重要,裁剪等级较低,至于前台进程的裁剪,一般是在启动的时候,这个时候app.pendingUiClean==false,只会裁剪当前进程:
else {
<!--关键点13 内存充足的时候,进程的裁剪-->
...
for (int i=N-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
// 在resume的时候,都是设置成true,所以退回后台的时候app.pendingUiClean==true是满足的,
// 因此缩减一次,但是不会再次走这里的分支缩减即使优先级变化,但是已经缩减过
// 除非走上面的后台流程,那个时候这个进程的等级已经很低了,
if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
|| app.systemNoUi) && app.pendingUiClean) {
if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
&& app.thread != null) {
try {
app.thread.scheduleTrimMemory(
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
} catch (RemoteException e) {
}
}
// clean一次就弄成false
app.pendingUiClean = false;
}
// 基本算没怎么裁剪
app.trimMemoryLevel = 0;
}
}
}
最后这部分是后台进程数量充足的时候,系统只会针对app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND的进程进行裁剪,而裁剪等级也较低:ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,因此根据裁剪等级APP可以大概知道系统当前的内存状况,同时也能知道系统希望自己如何裁剪,之后APP做出相应的瘦身即可。不过,上面的进程裁剪的优先级是完全根据后台进程数量来判断的,但是,不同的ROM可能进行了改造,所以裁剪等级不一定完全准确,比如在开发者模式打开限制后台进程数量的选项,限制后台进程数目不超过2个,那么这个时候的裁剪等级就是不太合理的,因为内存可能很充足,但是由于限制了后台进程的数量,导致裁剪等级过高。因此在使用的时候,最好结合裁剪等级与当前内存数量来综合考量。
进程优先级的计算Android是有自己的一条准则的,某些特殊场景的需要额外处理进程的oom_adj Android也是给了参考方案的。但是,那对于流氓来说,并没有任何约束效力。 "流氓"仍然能够参照oom_adj(优先级)的计算规则,利用其漏洞,提高进程的oom_adj,以降低被杀的风险。如果单单降低被杀风险还好,就怕那种即不想死,又想占用资源的APP,累积下去就会导致系统内存不足,导致整个系统卡顿。
优先级的计算逻辑比较复杂,这里只简述非缓存进程,因为一旦沦为缓存进程,其优先级就只能依靠LRU来计算,不可控。而流氓是不会让自己沦为缓存进程的,非缓存进程是以下进程中的一种,并且,优先级越高(数值越小),越不易被杀死:
ADJ优先级 | 优先级 | 进程类型 |
---|---|---|
SERVICE_ADJ | 5 | 服务进程(Service process) |
HEAVY_WEIGHT_APP_ADJ | 4 | 后台的重量级进程,system/rootdir/init.rc文件中设置 |
BACKUP_APP_ADJ | 3 | 备份进程(这个不太了解) |
PERCEPTIBLE_APP_ADJ | 2 | 可感知进程,比如后台音乐播放 ,通过startForeground设置的进程 |
VISIBLE_APP_ADJ | 1 | 可见进程(可见,但是没能获取焦点,比如新进程仅有一个悬浮Activity,其后面的进程就是Visible process) |
FOREGROUND_APP_ADJ | 0 | 前台进程(正在展示是APP,存在交互界面,Foreground process) |
我们从低到高看:如何让进程编程FOREGROUND_APP_ADJ进程,也就是前台进程,这个没有别的办法,只有TOP activity进程才能是算前台进程。正常的交互逻辑下,这个是无法实现的,锁屏的时候倒是可以启动一个Activity,但是需要屏幕点亮的时候再隐藏,容易被用户感知,得不偿失,所以基本是无解,所以之前传说的QQ通过一个像素来保活的应该不是这种方案,而通过WindowManager往主屏幕添加View的方式也并未阻止进程被杀,到底是否通过一像素实现进程包活,个人还未得到解答,希望能有人解惑。
先看一下源码中对两种优先级的定义,VISIBLE_APP_ADJ是含有可见但是非交互Activity的进程,PERCEPTIBLE_APP_ADJ是用户可感知的进程,如后台音乐播放等
// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;
// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;
这种做法是相对温和点的,Android官方曾给过类似的方案,比如音乐播放时后,通过设置前台服务的方式来保活,这里就为流氓进程提供了入口,不过显示一个常住服务会在通知栏上有个运行状态的图标,会被用户感知到。但是Android恰恰还有个漏洞可以把该图标移除,真不知道是不是Google故意的。这里可以参考微信的保活方案:双Service强制前台进程保活。
startForeground(ID, new Notification()),可以将Service变成前台服务,所在进程就算退到后台,优先级只会降到PERCEPTIBLE_APP_ADJ或者VISIBLE_APP_ADJ,一般不会被杀掉,Android的有个漏洞,如果两个Service通过同样的ID设置为前台进程,而其一通过stopForeground取消了前台显示,结果是保留一个前台服务,但不在状态栏显示通知,这样就不会被用户感知到耍流氓,这种手段是比较常用的流氓手段。优先级提高后,AMS的killBackgroundProcesses已经不能把进程杀死了,它只会杀死oom_adj大于ProcessList.SERVICE_ADJ的进程,而最近的任务列表也只会清空Activity,无法杀掉进程。 因为后台APP的优先级已经提高到了PERCEPTIBLE_APP_ADJ或ProcessList.VISIBLE_APP_ADJ,可谓流氓至极,如果再占据着内存不释放,那就是泼皮无赖了,这里有个遗留疑问:startForeground看源码只会提升到PERCEPTIBLE_APP_ADJ,但是在5.0之后的版本提升到了VISIBLE_APP_ADJ,这里看源码,没找到原因,希望有人能解惑。具体做法如下:
public class RogueBackGroundService extends Service {
private static int ROGUE_ID = 1;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
Intent intent = new Intent(this, RogueIntentService.class);
startService(intent);
startForeground(ROGUE_ID, new Notification());
}
public static class RogueIntentService extends IntentService {
//流氓相互唤醒Service
public RogueIntentService(String name) {
super(name);
}
public RogueIntentService() {
super("RogueIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
}
@Override
public void onCreate() {
super.onCreate();
startForeground(ROGUE_ID, new Notification());
}
@Override
public void onDestroy() {
stopForeground(true);
super.onDestroy();
}
}
}
不过这个漏洞在Android7.1之后失效了,因为Google加了一个校验:如果还有Service通过setForeground绑定相同id的Notification,就不能cancelNotification,也就是说还是会显示通知(在通知列表)。
private void cancelForegroudNotificationLocked(ServiceRecord r) {
if (r.foregroundId != 0) {
// First check to see if this app has any other active foreground services
// with the same notification ID. If so, we shouldn't actually cancel it,
// because that would wipe away the notification that still needs to be shown
// due the other service.
ServiceMap sm = getServiceMap(r.userId);
if (sm != null) {
<!--查看是不是与该ID 通知绑定的Service取消了了前台显示-->
for (int i = sm.mServicesByName.size()-1; i >= 0; i--) {
ServiceRecord other = sm.mServicesByName.valueAt(i);
if (other != r && other.foregroundId == r.foregroundId
&& other.packageName.equals(r.packageName)) {
// Found one! Abort the cancel.
<!--如果找到还有显示的Service,直接返回-->
return;
}
}
}
r.cancelNotification();
}
}
在7.1上,Google PlayStore渠道的微信似乎也放弃了这种保活手段,因为7.1的微信从最近的任务列表删除是可以杀死进程的,如果采用上述手段是杀不了的。
前文我们分析过Android Binder的讣告机制:如果Service Binder实体的进程挂掉,系统会向Client发送讣告,而这个讣告系统就给进程保活一个可钻的空子。可以通过两个进程中启动两个binder服务,并且互为C/S,一旦一个进程挂掉,另一个进程就会收到讣告,在收到讣告的时候,唤起被杀进程。逻辑如下下:
双服务保活.jpg
首先编写两个binder实体服务PairServiceA ,PairServiceB,并且在onCreate的时候相互绑定,并在onServiceDisconnected收到讣告的时候再次绑定。
public class PairServiceA extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new AliveBinder();
}
@Override
public void onCreate() {
super.onCreate();
bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
ToastUtil.show("bind A");
}
};
与之配对的B
public class PairServiceB extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new AliveBinder();
}
@Override
public void onCreate() {
super.onCreate();
bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
}
};
}
之后再Manifest中注册,注意要进程分离
<service android:name=".service.alive.PairServiceA"/>
<service
android:name=".service.alive.PairServiceB"
android:process=":alive"/>
之后再Application或者Activity中启动一个Service即可。
startService(new Intent(MainActivity.this, PairServiceA.class));
这个方案一般都没问题,因为Binder讣告是系统中Binder框架自带的,可能一次性全部杀死所有父子进程会不行,这个没测试过。不过这个手段也能在一定程度上提高有限级, 最近的任务列表已经不能杀死进程了。原因如下:
此时APP内至少两个进程A\B ,并且AB相互通过bindService绑定,此时就是互为客户端,在oom_adj中有这么一种计算逻辑,如果进程A的Service被B通过bind绑定,那么A的优先级可能会受到B的影响,因为在计算A的时候需要先计算B,但是B同样是A的Service,反过来有需要计算A,如果不加额外的判断,就会出现死循环,AMS是通过一个计数来标识的:mAdjSeq == app.adjSeq。于是流程就是这样
上面的流程能保证不出现死循环,并且由于A只计算了一半,所以A的很多东西没有更新,所以B拿到的A就是之前的数值,比如 curProcState、curSchedGroup:
private final int computeOomAdjLocked(ProcessRecord app, int cachedAdj, ProcessRecord TOP_APP,
boolean doingAll, long now) {
if (mAdjSeq == app.adjSeq) {
// This adjustment has already been computed.
return app.curRawAdj;
}
....
for (int is = app.services.size()-1;
is >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ
|| schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
|| procState > ActivityManager.PROCESS_STATE_TOP);
is--) {
ServiceRecord s = app.services.valueAt(is);
...
for (int conni = s.connections.size()-1;
conni >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ
|| schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
|| procState > ActivityManager.PROCESS_STATE_TOP);
conni--) {
ArrayList<ConnectionRecord> clist = s.connections.valueAt(conni);
for (int i = 0;
i < clist.size() && (adj > ProcessList.FOREGROUND_APP_ADJ
|| schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
|| procState > ActivityManager.PROCESS_STATE_TOP);
i++) {
ConnectionRecord cr = clist.get(i);
if (cr.binding.client == app) {
// Binding to ourself is not interesting.
continue;
}
if ((cr.flags&Context.BIND_WAIVE_PRIORITY) == 0) {
ProcessRecord client = cr.binding.client;
// 这里会不会出现死循环的问题呢? A需要B的计算、B需要A的计算,这个圆环也许就是为什么
// 无法左滑删除的原因 循环的,
<!--关键点1 -->
int clientAdj = computeOomAdjLocked(client, cachedAdj,
TOP_APP, doingAll, now);
int clientProcState = client.curProcState;
if (clientProcState >= ActivityManager.PROCESS_STATE_CACHED_ACTIVITY) {
clientProcState = ActivityManager.PROCESS_STATE_CACHED_EMPTY;
}
<!--关键点2-->
...
if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
if (client.curSchedGroup == Process.THREAD_GROUP_DEFAULT) {
schedGroup = Process.THREAD_GROUP_DEFAULT;
}
...
}
上面的代码中:关键点1是循环计算的入口,关键点2是无法删除的原因所在,由于A没及时更新,导致schedGroup = Process.THREAD_GROUP_DEFAULT,反过来也让A保持schedGroup = Process.THREAD_GROUP_DEFAULT。A B 都无法左滑删除。
通过startService启动的Service,如果没用呗stopService结束掉,在进程被杀掉之后,是有可能重新启动的,实现方式:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;//或者START_REDELIVER_INTENT
}
当然,前提是该进程可以被杀掉(无论被AMS还是LMDK),用户主动杀死(最近任务列表或者退出应用),都一定会通过Binder讣告机制回调:
private final void handleAppDiedLocked(ProcessRecord app,
boolean restarting, boolean allowRestart) {
int pid = app.pid;
boolean kept = cleanUpApplicationRecordLocked(app, restarting, allowRestart, -1);
...
}
进而调用cleanUpApplicationRecordLocked函数进行一系列清理及通知工作,这里先看Service相关的工作:
private final boolean cleanUpApplicationRecordLocked(ProcessRecord app,
boolean restarting, boolean allowRestart, int index) {
...
// 这里先出处理service
mServices.killServicesLocked(app, allowRestart);
...
}
这里传入的allowRestart==true,也就说:允许重新启动Service:
final void killServicesLocked(ProcessRecord app, boolean allowRestart) {
...
ServiceMap smap = getServiceMap(app.userId);
// Now do remaining service cleanup.
for (int i=app.services.size()-1; i>=0; i--) {
ServiceRecord sr = app.services.valueAt(i);
if (!app.persistent) {
app.services.removeAt(i);
}
...
if (allowRestart && sr.crashCount >= 2 && (sr.serviceInfo.applicationInfo.flags
&ApplicationInfo.FLAG_PERSISTENT) == 0) {
bringDownServiceLocked(sr);
} else if (!allowRestart
|| !mAm.mUserController.isUserRunningLocked(sr.userId, 0)) {
bringDownServiceLocked(sr);
} else {
<!--关键点1 先进行判定,如果有需要将重启的消息发送到消息队列等待执行-->
boolean canceled = scheduleServiceRestartLocked(sr, true);
// 受时间跟次数的限制 sr.stopIfKilled
<!--关键点2 二次确认,如果不应该启动Service,就将重启Service的消息移除-->
if (sr.startRequested && (sr.stopIfKilled || canceled)) {
if (sr.pendingStarts.size() == 0) {
sr.startRequested = false;
if (!sr.hasAutoCreateConnections()) {
bringDownServiceLocked(sr);
}
...
}
先看关键点1:如果允许重新启动,并且APP Crash的次数小于两次,就视图将为结束的Service重新唤起,其实就是调用scheduleServiceRestartLocked,发送消息,等待唤醒,关键点2是二次确认下,是不是需要被唤醒,如果不需要就将上面的消息移除,并进行一定的清理工作,这里的sr.stopIfKilled,其实主要跟onStartCommand返回值有关系:
void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res) {
boolean inDestroying = mDestroyingServices.contains(r);
if (r != null) {
if (type == ActivityThread.SERVICE_DONE_EXECUTING_START) {
r.callStart = true;
switch (res) {
case Service.START_STICKY_COMPATIBILITY:
case Service.START_STICKY: {
r.findDeliveredStart(startId, true);
r.stopIfKilled = false;
break;
}
case Service.START_NOT_STICKY: {
r.findDeliveredStart(startId, true);
if (r.getLastStartId() == startId) {
r.stopIfKilled = true;
}
break;
}
case Service.START_REDELIVER_INTENT: {
ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);
if (si != null) {
si.deliveryCount = 0;
si.doneExecutingCount++;
r.stopIfKilled = true;
}
break;
}
所以,如果onStartCommand返回的是Service.START_STICKY,在被杀死后是会重新启动的,有必要的话,还会重启进程:
private final boolean scheduleServiceRestartLocked(ServiceRecord r,
boolean allowCancel) {
boolean canceled = false;
...
<!--关键点1-->
mAm.mHandler.removeCallbacks(r.restarter);
mAm.mHandler.postAtTime(r.restarter, r.nextRestartTime);
r.nextRestartTime = SystemClock.uptimeMillis() + r.restartDelay;
return canceled;
}
看关键点1,其实就是发送一个重新启动Service的消息,之后就会重新启动Service。
private class ServiceRestarter implements Runnable {
private ServiceRecord mService;
void setService(ServiceRecord service) {
mService = service;
}
public void run() {
synchronized(mAm) {
performServiceRestartLocked(mService);
}
}
}
再看下几个标志的意义:
1、 START_STICKY
在运行onStartCommand后service进程被kill后,那将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建 service后将保证调用onstartCommand。如果没有传递任何开始命令给service,那将获取到null的intent
2、 START_NOT_STICKY
在运行onStartCommand后service进程被kill后,并且没有新的intent传递给它。Service将移出开始状态,并且直到新的明显的方法(startService)调用才重新创建。因为如果没有传递任何未决定的intent那么service是不会启动,也就是期间onstartCommand不会接收到任何null的intent。
3、 START_REDELIVER_INTENT
在运行onStartCommand后service进程被kill后,系统将会再次启动service,并传入最后一个intent给onstartCommand。直到调用stopSelf(int)才停止传递intent。如果在被kill后还有未处理好的intent,那被kill后服务还是会自动启动。因此onstartCommand不会接收到任何null的intent。
adj主要用来给LMKD服务,让内核曾选择性的处理后台杀死,curRawAdj是本地updateOomAdj计算出的临时值,setRawAdj是上一次计算出兵设定好的oom值,两者都是未经过二次调整的数值,curAdj与setAdj是经过调整之后的adj,这里有个小问题,为什么前台服务进程的oom_adj打印出来是1,但是在AMS登记的curAdj却是2呢?
oom: max=16 curRaw=2 setRaw=2 cur=2 set=2
curSchedGroup=-1 setSchedGroup=-1 systemNoUi=false trimMemoryLevel=0
curProcState=4 repProcState=4 pssProcState=-1 setProcState=4 lastStateTime=-37s554ms
AMS传递给LMKD服务的adj确实是2,LMKD用2计算出的oom_score_adj=117 (1000oom_adj/17) 也是准确的数值 ,那为什么proc/pid/oom_adj中的数值是1呢?应该是反向取整*导致的,高版本的内核都不在使用oom_adj,而是用oom_score_adj,oom_adj是一个向前兼容。
static void cmd_procprio(int pid, int uid, int oomadj) {
struct proc *procp;
char path80;
char val20;
if (oomadj < OOM_DISABLE || oomadj > OOM_ADJUST_MAX) {
ALOGE("Invalid PROCPRIO oomadj argument %d", oomadj);
return;
}
// 这里只记录oom_score_adj
snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
snprintf(val, sizeof(val), "%d", lowmem_oom_adj_to_oom_score_adj(oomadj));
writefilestring(path, val);
<!--use_inkernel_interface = 1-->
if (use_inkernel_interface)
return;
....
}
use_inkernel_interface标识其他几个oom_adj,oom_score跟随 oom_score_adj变化。oom_adj=(oom_score_adj*17/1000),取整的话,正好小了1;看如下解释:
The value of /proc/<pid>/oom_score_adj is added to the badness score before oom_adj;
For backwards compatibility with previous kernels, /proc/<pid>/oom_adj may also
be used to tune the badness score. Its acceptable values range from -16
(OOM_ADJUST_MIN) to +15 (OOM_ADJUST_MAX) and a special value of -17
(OOM_DISABLE) to disable oom killing entirely for that task. Its value is
scaled linearly with /proc/<pid>/oom_score_adj.
oom_adj的存在是为了和旧版本的内核兼容,并且随着oom_score_adj线性变化,如果更改其中一个,另一个会自动跟着变化,在内核中变化方式为:
所以,就会产生如下精度丢失的情况:
# echo 9 > /proc/556/oom_adj
# cat /proc/556/oom_score_adj
529
# cat /proc/556/oom_adj
8
这也是为什么Android中明明算出来的oom_adj=1(2),在proc/pid/oom_adj总记录的确实0(1)。
curSchedGroup与setSchedGroup是AMS管理进程的一个参考,定义在ProcessList.java中,从名字上看与任务调度有关系,答案也确实如此,取值有如下三种,不同版本略有不同,这里是7.0,
// Activity manager's version of Process.THREAD_GROUP_BG_NONINTERACTIVE
static final int SCHED_GROUP_BACKGROUND = 0;
// Activity manager's version of Process.THREAD_GROUP_DEFAULT
static final int SCHED_GROUP_DEFAULT = 1;
// Activity manager's version of Process.THREAD_GROUP_TOP_APP
static final int SCHED_GROUP_TOP_APP = 2;
AMS只能杀死后台进程,只有setSchedGroup==ProcessList.SCHED_GROUP_BACKGROUND的进程才被AMS看做后台进程,才可以被杀死,否则AMS无权杀死。
<!--参考代码1-->
if (app.waitingToKill != null && app.curReceivers.isEmpty()
&& app.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND) {
app.kill(app.waitingToKill, true);
success = false;
}
<!--参考代码2-->
// Kill the running processes.
for (int i = 0; i < procsToKill.size(); i++) {
ProcessRecord pr = procsToKill.get(i);
if (pr.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND
&& pr.curReceivers.isEmpty()) {
pr.kill("remove task", true);
} else {
// We delay killing processes that are not in the background or running a receiver.
pr.waitingToKill = "remove task";
}
}
以上两个场景:场景一是AMS计算oomAdj并清理进程 ,场景二的代表:从最近的任务列表删除进程。
ProcState 主要是为AMS服务,AMS依据procState判断进程当前的状态以及重要程度,对应的值位于ActivityManager.java中,主要作用是:决定进程的缓存等级以及缓存进程的生死。
<!--参考代码1-->
switch (app.curProcState) {
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
mNumCachedHiddenProcs++;
numCached++;
if (numCached > cachedProcessLimit) {
app.kill("cached #" + numCached, true);
}
break;
case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
if (numEmpty > ProcessList.TRIM_EMPTY_APPS
&& app.lastActivityTime < oldTime) {
app.kill("empty for "
+ ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
/ 1000) + "s", true);
} else {
numEmpty++;
if (numEmpty > emptyProcessLimit) {
app.kill("empty #" + numEmpty, true);
}
}
break;
default:
mNumNonCachedProcs++;
break;
}
<!--参考代码2-->
if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
|| app.systemNoUi) && app.pendingUiClean) {
final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
if (app.trimMemoryLevel < level && app.thread != null) {
try {
app.thread.scheduleTrimMemory(level);
} catch (RemoteException e) {
}
}
app.pendingUiClean = false;
}
所有流氓手段的进程保活,都是下策,建议不要使用,本文只是分析实验用。当APP退回后台,优先级变低,就应该适时释放内存,以提高系统流畅度,依赖流氓手段提高优先级,还不释放内存,保持不死的,都是作死。
作者:看书的小蜗牛
原文链接: Android进程保活-自“裁”或者耍流氓
**仅供参考,欢迎指正 **