一、背景介绍
Android在设计上是有真后台的,理论上是希望应用程序能尽可能长地存活,这样用户体验会更好,毕竟热启动肯定比冷启动要快。但是系统内存是有限的,不可能让所有应用一视同仁地存活着,所以需要系统制定一套规则来统一管理,决定在什么场景下哪些应用要保证它的使用体验,哪些应用需要被杀掉腾出内存空间来。在Android framework层中,ActivityManagerService简称AMS,就主要负责进程的调度和管理。
二、规则介绍
那具体怎么管理?最简单的想法就是通过优先级来控制。
那么按优先级来划分进程,粗分为5类:
前台进程(Foreground process)
:用户当前操作所必需的进程。
- 拥有用户正在交互的 Activity(已调用onResume())
- 拥有某个 Service,后者绑定到用户正在交互的 Activity
- 拥有正在“前台”运行的 Service(服务已调用 startForeground())
- 拥有正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
- 拥有正执行其 onReceive() 方法的 BroadcastReceiver
可见进程(Visible process)
: 没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。
- 拥有不在前台、但仍对用户可见的 Activity(已调用onPause())。
- 拥有绑定到可见(或前台)Activity 的 Service
服务进程(Service process)
: 尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。
- 正在运行startService()方法启动的服务,且不属于上述两个更高类别进程的进程。
后台进程(Background process)
后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。
- 对用户不可见的Activity的进程(已调用Activity的onStop()方法)
空进程(Empty process)
只有一个进程壳子,存在的意义仅仅是缩短启动时间。
- 不含任何活动应用组件的进程
当然系统划分不会这么糙,分别通过adj和procState来给进程贴优先级标签,其中adj定义在ProcessList.java中,procState定义在ActivityManager.java中,如下所示:
ADJ:
ADJ级别 | 取值 | 含义 |
---|---|---|
NATIVE_ADJ | -1000 | native进程 |
SYSTEM_ADJ | -900 | 仅指system_server进程 |
PERSISTENT_PROC_ADJ | -800 | 系统persistent进程 |
PERSISTENT_SERVICE_ADJ | -700 | 关联着系统或persistent进程 |
FOREGROUND_APP_ADJ | 0 | 前台进程 |
VISIBLE_APP_ADJ | 100 | 可见进程 |
PERCEPTIBLE_APP_ADJ | 200 | 可感知进程,比如后台音乐播放 |
BACKUP_APP_ADJ | 300 | 备份进程 |
HEAVY_WEIGHT_APP_ADJ | 400 | 重量级进程 |
SERVICE_ADJ | 500 | 服务进程 |
HOME_APP_ADJ | 600 | Home进程 |
PREVIOUS_APP_ADJ | 700 | 上一个进程 |
SERVICE_B_ADJ | 800 | B List中的Service |
CACHED_APP_MIN_ADJ | 900 | 不可见进程的adj最小值 |
CACHED_APP_MAX_ADJ | 906 | 不可见进程的adj最大值 |
ProcState:
state级别 | 取值 | 解释 |
---|---|---|
PROCESS_STATE_CACHED_EMPTY | 16 | 进程处于cached状态,且为空进程 |
PROCESS_STATE_CACHED_ACTIVITY_CLIENT | 15 | 进程处于cached状态,且为另一个cached进程(内含Activity)的client进程 |
PROCESS_STATE_CACHED_ACTIVITY | 14 | 进程处于cached状态,且内含Activity |
PROCESS_STATE_LAST_ACTIVITY | 13 | 后台进程,且拥有上一次显示的Activity |
PROCESS_STATE_HOME | 12 | 后台进程,且拥有home Activity |
PROCESS_STATE_RECEIVER | 11 | 后台进程,且正在运行receiver |
PROCESS_STATE_SERVICE | 10 | 后台进程,且正在运行service |
PROCESS_STATE_HEAVY_WEIGHT | 9 | 后台进程,但无法执行restore,因此尽量避免kill该进程 |
PROCESS_STATE_BACKUP | 8 | 后台进程,正在运行backup/restore操作 |
PROCESS_STATE_IMPORTANT_BACKGROUND | 7 | 对用户很重要的进程,用户不可感知其存在 |
PROCESS_STATE_IMPORTANT_FOREGROUND | 6 | 对用户很重要的进程,用户可感知其存在 |
PROCESS_STATE_TOP_SLEEPING | 5 | 与PROCESS_STATE_TOP一样,但此时设备正处于休眠状态 |
PROCESS_STATE_FOREGROUND_SERVICE | 4 | 拥有一个前台Service |
PROCESS_STATE_BOUND_FOREGROUND_SERVICE | 3 | 拥有一个前台Service,且由系统绑定 |
PROCESS_STATE_TOP | 2 | 拥有当前用户可见的top Activity |
PROCESS_STATE_PERSISTENT_UI | 1 | persistent系统进程,并正在执行UI操作 |
PROCESS_STATE_PERSISTENT | 0 | persistent系统进程 |
PROCESS_STATE_NONEXISTENT | -1 | 不存在的进程 |
三、调度策略分析
标签定义好了,那么如何去为进程设置adj和procState值,以及如何决定在哪些场景下杀掉哪些进程呢?那么来看看AMS的调度策略
AMS中与adj相关的有三个方法:
-
updateOomAdjLocked
:在进程组件生命周期变化时更新adj,然后分别执行以下两个方法 -
computeOomAdjLocked
:计算adj -
applyOomAdjLocked
:应用adj
updateOomAdjLocked只是一个调用的入口而已,实际干活的是computeOomAdjLocked和applyOomAdjLocked,那么来分别看看,代码不贴了,总结下主要的功能点。
updateOomAdjLocked有无参、一参、五参三个重载方法,主要看看无参方法:
final void updateOomAdjLocked() {
LruProcesses
以Lru的方式保存活着的进程
emptyProcessLimit 与 cachedProcessLimit
设置好empty和cache的数量,可以设置cache和empty的总数mProcessLimit(默认是32,2G内存可能调整为16),一般来说他俩各占一半。再往下是初始化一些变量的操作,这里要重点注意numSlots所表达的意思。ProcessList.CACHED_APP_MAX_ADJ和Process.CACHED_APP_MIN_ADJ常量系统默认的值分别为906和900,表示的是后台进程和empty进程分配的值是在900至906之间,共有7个值。numSloas计算过程中除以2是因为每个槽配置到进程包含cache进程和empty进程两种,两种进程需用不同adj值表示,所以每个槽包含两个adj值的分配空间,所以需要除以二,计算出来的numSlots值为3。emptyFactor表示每个槽中需要放置几个empty进程,是根据当前empty进程总数决定的,cachedFactor即是表示需要放置几个后台进程到每个槽中。为便于理解,结合后面代码逻辑举例来讲,比如后台此时有15个cahcedProcess(后台进程)和12个emptyProcess,则会将15/3=5个cachedProcess设置到在第一个槽(可分配900,901这两个oom_adj值)中,oom_adj设置为900;将12/3=4个emptyProcess设置在第一槽,oom_adj值设置为901;然后再设置5个cachedProcess和4个emptyProcess的oom_adj值分别为902和903,即第二个槽。
从LruProcesses最新加入的元素开始逐个取,更新所有进程状态
先走computeOomAdjLocked
:
根据进程状态来计算出对应的adj和procState
前台:
- 拥有当前用户可见的activity ,则adj=0,procState=2
- 拥有一个前台service ,则adj=0,procState=4
- 后台进程,且正在运行receiver ,则adj=0,procState=11
- 后台进程,且正在运行service ,则adj=0,procState=10
- 以上条件都不符合,则adj=cachedAdj,procState=16
非前台:
- 当activity可见, 则adj=1,procState=2;
- 当activity正在暂停或者已经暂停, 则adj=2,procState=2;
- 当activity正在停止, 则adj=2,procState=13(且activity尚未finish);
- 以上都不满足,否则procState=14
adj > 200的情况(可感知的后台进程)
- 当存在前台service时,则adj=2, procState=4;
- 当强制前台时,则adj=2, procState=6;
当进程为HeavyWeightProcess,则adj=4, procState=9;
当进程为HomeProcess情况,则adj=6, procState=12;
当进程为PreviousProcess情况,则adj=7, procState=13;
备份进程,则adj=3, procState=7或8
Service情况:
当adj>0 或 schedGroup为后台线程组 或procState>2时,双重循环遍历:
- 当service已启动,则procState<=10;
- 当service在30分钟内活动过,则adj=5,cached=false;
- 获取service所绑定的connections
- 当client与当前app同一个进程,则continue;
- 当client进程的ProcState >=cache,则设置为空进程
- 当进程存在显示的ui,则将当前进程的adj和ProcState值赋予给client进程
- 当不存在显示的ui,且service上次活动时间距离现在超过30分钟,则只将当前进程的adj值赋予给client进程
- 当前进程adj > client进程adj的情况
- 当service进程比较重要时,则设置adj >= -11
- 当client进程adj<2,且当前进程adj>2时,则设置adj=2;
- 当client进程adj>1时,则设置adj = clientAdj
- 否则,设置adj <= 1;
- 若client进程不是cache进程,则当前进程也设置为非cache进程
- 当绑定的是前台进程的情况
- 当client进程状态为前台时,则设置mayBeTop=true,并设置client进程procState=16
- 当client进程状态 < 2的前提下:若绑定前台service,则clientProcState=3;否则clientProcState=6
- 当connections并没有绑定前台service时,则clientProcState >= 7
- 保证当前进程procState不会必client进程的procState大
- 当进程adj >0,且activity可见 或者resumed 或 正在暂停,则设置adj = 0
ContentProvider情况
当adj>0 或 schedGroup为后台线程组 或procState>2时,双重循环遍历:
- 当client与当前app同一个进程,则continue;
- 当client进程procState >=14,则设置成procState =16
- 没有ui展示,则保证adj >=0
- 当client进程状态为前台时,则设置mayBeTop=true,并设置client进程procState=16设置为空进程
- 当client进程状态 < 2时,则clientProcState=3;
- procState 比client进程值更大时,则取client端的状态值。
- 当contentprovider存在外部进程依赖(非framework)时,则设置adj =0, procState=6
之后再根据一些逻辑调整下adj:d
比如:当A类Service个数 > service/3时,则加入到B类Service
经过computeOomAdjLocked之后,还存在部分进程仍然未分配adj,但是procState是有的。一般剩下的进程只会被分配成cache和empty。
那么接下来再讲讲updateOomAdjLocked
之后的进程管理策略:
- 当cached进程超过上限(cachedProcessLimit),则杀掉该进程
- 当空进程超过上限(emptyProcessLimit),则杀掉该进程
- 当空进程超过上限(TRIM_EMPTY_APPS为cachedProcessLimit的一半),且空闲时间超过30分钟,则杀掉该进程
applyOomAdjLocked(app, true, now, nowElapsed);
//应用当前进程设置的adj
这部分其实就是把adj把adj值 通过socket通信发送给lmkd守护进程,并把对应值写入:/proc/<pid>/oom_score_adj
最后就是lowmemorykiller的查杀进程逻辑了。具体可以参考我之前的文章:lowmemorykiller总结
}
四、进程管理的思考
站在系统的角度:
尽量权衡好内存和用户体验两者关系
- 对于内存较大的手机,可以尽量多保证后台进程的数量,这样可以保证APP启动速度
- 对于内存较小手机,减少cache/empty 进程数量,在一定情况下BServices进程可以降级为cache/empty,利于杀死更多进程。启动某些大应用的时候,可以把cache/empty清理掉,比如启动相机、启动某款大型游戏等等。
- 对于一些热门的APP做白名单来给特权,保障使用体验等等。
站在应用的角度:
肯定是希望自己能存活越久越好,这就牵扯到一些进程保活的方式(这个后续会总结),其中一类就是想办法提高进程的adj,但是凡是不是以真正业务需要来强行保活的行为都是耍流氓。
五、对APP开发者的建议
只有真正需要用户可感知的应用,才调用startForegroundService()方法来启动前台服务,此时ADJ=PERCEPTIBLE_APP_ADJ(200),常驻内存,并且会在通知栏常驻通知提醒用户,比如音乐播放,地图导航。切勿为了常驻而滥用前台服务,这会严重影响用户体验。
进程中的Service工作完成后,务必主动调用stopService或stopSelf来停止服务,避免占据内存,浪费系统资源;
不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放。当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次。
减少在保活上花心思,更应该在优化内存上下功夫,因为在相同ADJ级别的情况下,系统会选择优先杀内存占用大的进程。
六、一个小Tips
UI进程与Service进程一定要分离,因为对于包含activity的service进程,一旦进入后台就有机会成为cache进程(ADJ>=900),随时可能会被系统回收;而分离后的Service进程服务属于SERVICE_ADJ(500),被杀的可能性相对较小。尤其是系统允许自启动的服务进程必须做UI分离,避免消耗系统较大内存。
if (app.hasShownUi && app != mHomeProcess) {
// If this process has shown some UI, let it immediately
// go to the LRU list because it may be pretty heavy with
// UI stuff. We'll tag it with a label just to help
// debug and understand what is going on.
if (adj > ProcessList.SERVICE_ADJ) {
app.adjType = "cch-started-ui-services";
}
修改方案:
1当前进程不运行Activity, 只运行Service
2 采用fg-service
参考:
http://gityuan.com/2018/05/19/android-process-adj/
系列文章:
Android进程管理篇(一)-应用进程启动过程
Android进程管理篇(二)-进程查杀方式总结
Android进程管理篇(三)-AMS进程调度
lowmemorykiller总结