Android进程保活-自“裁”或者耍流氓(下篇)

猪小花1号2018-08-30 09:24

作者:李尚


通过START_STICKY与START_REDELIVER_INTENT实现被杀唤醒

通过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。

ProcessRecord中一些参数的意义的意义

  • int maxAdj; // Maximum OOM adjustment for this process
  • int curRawAdj; // Current OOM unlimited adjustment for this process
  • int setRawAdj; // Last set OOM unlimited adjustment for this process
  • int curAdj; // Current OOM adjustment for this process
  • int setAdj; // Last set OOM adjustment for this process

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 path[80];
    char val[20];

    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线性变化,如果更改其中一个,另一个会自动跟着变化,在内核中变化方式为:

  • 写oom_score_adj时,内核里都记录在变量 task->signal->oom_score_adj 中;
  • 读oom_score_adj时,从内核的变量 task->signal->oom_score_adj 中读取;
  • 写oom_adj时,也是记录到变量 task->signal->oom_score_adj 中,会根据oom_adj值按比例换算成oom_score_adj。
  • 读oom_adj时,也是从内核变量 task->signal->oom_score_adj 中读取,只不过显示时又按比例换成oom_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)。

  • int curSchedGroup; // Currently desired scheduling class
  • int setSchedGroup; // Last set to background scheduling class

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并清理进程 ,场景二的代表:从最近的任务列表删除进程。 

  • int curProcState = PROCESS_STATE_NONEXISTENT; // Currently computed process state
  • int repProcState = PROCESS_STATE_NONEXISTENT; // Last reported process state
  • int setProcState = PROCESS_STATE_NONEXISTENT; // Last set process state in process tracker
  • int pssProcState = PROCESS_STATE_NONEXISTENT; // Currently requesting pss for

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退回后台,优先级变低,就应该适时释放内存,以提高系统流畅度,依赖流氓手段提高优先级,还不释放内存,保持不死的,都是作死。

参考文档

谷歌文档Application
Android四大组件与进程启动的关系
Android 7.0 ActivityManagerService(8) 进程管理相关流程分析(2) updateOomAdjLocked
Android 7.0 ActivityManagerService(9) 进程管理相关流程分析(3) computeOomAdjLocked 精
Android代码内存优化建议-OnTrimMemory优化 精
微信Android客户端后台保活经验分享
按"Home"键回到桌面的过程
Android low memory killer 机制
应用内存优化之OnLowMemory&OnTrimMemory



相关阅读:Android进程保活-自“裁”或者耍流氓(上篇)

Android进程保活-自“裁”或者耍流氓(中篇)

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者李尚授权发布。