@flyouting
2014-06-30T00:07:23.000000Z
字数 3984
阅读 6211
android
透过ActivityManagerService来查看程序是如何被关闭的
在一般的系统进程管理和用户操作之间,还有少数一些方法能够使我们的程序终止。作为一个开发者,你的代码可能会产生一些附加效果,我们来看看一下终止应用的方法产生的影响:
在我们进一步讨论之前,我们需要先简单说下Android是如何看待前台和后台状态的。一般来说,当前对用户可见的,可交互的应用被称之为前台应用。系统中其他的应用被认为处于后台状态。低内存清理模块可能无法平等的看待后台状态,但是鉴于讨论这个问题,姑且认为是同等看待的。
服务对象可以被标记为“前台服务”,其唯一活跃的用户界面是由一个在系统窗口一直存在的notification组成。在这种特殊的情况下,应用进程被给予了很高的等级(特别是PERCEPTIBLE_APP_ADJ,值为2,值越低优先级越高),即使它没有任何可以跟用户交互的界面,虽然不能完全等同与一个可见的程序,但是这里我们认为它使得所有进程都处于前台状态。
技术上而言,我所指的是进程所在的“调度组”,这不同于出于内存压力需要而去关闭进程时用于排序的数值,系统服务使用这种区分来管理涉及到清除进程的行为。这很重要,需要记住。
行为 | 低内存清理 | 清除最近任务 | 强制关闭 |
---|---|---|---|
进程终止 | Y | Y1 | Y |
PendingIntent可触发 | Y | Y | N |
接收广播可以启动程序 | Y | Y | N |
Activity栈保存 | Y | N | N |
Y1:这个终止不能保证,详见下文
这种情况在Android系统中会阶段性发生,且用户无察觉,当系统内存不足时,它会关闭一些进程以回收资源。这么做是为了尽量少的对用户产生影响。所以,当程序进程被终止,外部事件(比如一个广播)会再次启动这个进程。程序甚至会保存在最近使用页面以方便用户再次打开。此外,进程被终止时存在的所有的Activity的栈都会被保留,当用户把应用重新切到前台的时候,他们会被重现。理论上,在第一时间终止应用进程对用户是而言是隐性的。
当用户在最近任务页面清除掉一个应用时,产生的行为有些不同。在这种情况下,系统会认为用户不希望此应用状态被保留。此程序进程分配的Activity栈也会被清理。下一次打开应用会开启一个全新的任务。跟上边一样,从其他应用和服务发来的行为可以唤醒并重启此应用进程。
此外,如果进程当时处于一个后台状态的话,那只会终止进程。这通常被认为是正确的,因为一个前台的Activity栈在最近任务页面显示且活跃时也会被暂时切入后台。然而,如果是一个活跃的前台服务的情况下,终止进程的行为会被阻止。在后边会描述为什么这样。
然而这确实意味着,后台运行的服务不能阻止这个级别的进程终止。为了帮助过渡,服务类通过onTaskRemoved()方法提供了一个回调,可以在服务被强制关闭之前记录或处理一些关键的东西。注意,这个回调特定于这种用例,低内存清理时是没有这个回调的。
最后这种情况是指用户进入系统设置页面,强制要求关闭某个应用。很多人认为这跟之前的方式是一样的。但这里有些需要特别注意,首先就是不管进程是不是后台状态,用户强制关闭时,进程一定会被终止。
除此之外,系统服务中关于此进程的记录都会被标记成"关闭状态",这意味着没有外部触发可以唤醒整个应用进程。在用户再次主动的开发它之前,这个进程是被孤立的。就跟用户刚安装了应用且一次都没打开过一样的状态。
不可避免的是,随着Android进程管理的不断进步,复杂性的提升会导致一些未知的状态。其中一个例子就是在一个bug报告的评论里,这里描述的行为是当应用正在运行一个前台服务,任务被滑动取消了,进程依然保持运行状态,但是它很容易被外部触发给搞死。
这么做的原因在于当前ActivityManagerService
是如何管理这些事件的。当收到一个请求,需要清理某一个app的任务,如果进程被认为是后台的,服务会直接终止进程。如果是在前台,那服务会延迟一会才会被终止。推测这是为了让把工作做完后才终止,或者从前台状态转变成其他。
在ActivityManagerService.computeOomAdjLocked()
方法中,进程的前后台状态的重新评定都会检查标记值。这就跟系统事件一样,不断的在发生,这个小例子的关键问题看起来像是在ActivityManagerService
中的一段代码中,它用于评估不同的事件条件。
if (app == TOP_APP) {
// The last app on the list is the foreground app.
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = Process.THREAD_GROUP_DEFAULT;
app.adjType = "top-activity";
foregroundActivities = true;
interesting = true;
procState = ActivityManager.PROCESS_STATE_TOP;
} else if (app.instrumentationClass != null) {
// Don't want to kill running instrumentation.
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = Process.THREAD_GROUP_DEFAULT;
app.adjType = "instrumentation";
interesting = true;
procState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
} else if ((queue = isReceivingBroadcast(app)) != null) {
// 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 = (queue == mFgBroadcastQueue)
? Process.THREAD_GROUP_DEFAULT : Process.THREAD_GROUP_BG_NONINTERACTIVE;
app.adjType = "broadcast";
procState = ActivityManager.PROCESS_STATE_RECEIVER;
} else if (app.executingServices.size() > 0) {
// An app that is currently executing a service callback also
// counts as being in the foreground.
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = app.execServicesFg ?
Process.THREAD_GROUP_DEFAULT : Process.THREAD_GROUP_BG_NONINTERACTIVE;
app.adjType = "exec-service";
procState = ActivityManager.PROCESS_STATE_SERVICE;
//Slog.i(TAG, "EXEC " + (app.execServicesFg ? "FG" : "BG") + ": " + app);
} else {
// As far as we know the process is empty. We may change our mind later.
schedGroup = Process.THREAD_GROUP_BG_NONINTERACTIVE;
// At this point we don't actually know the adjustment. Use the cached adj
// value that the caller wants us to.
adj = cachedAdj;
procState = ActivityManager.PROCESS_STATE_CACHED_EMPTY;
app.cached = true;
app.empty = true;
app.adjType = "cch-empty";
}
通过代码可以看到,一个接收广播的活动在检查是否有前台服务之前就被评定了。如果应用程序在接收标准广播的过程(即一个没有flag_receiver_foreground标志),该过程将被设置为后台状态,代码不会继续执行验证是否有前台服务正在运行。
总结,导致意外的情况: