Android进程管理篇(三)-进程adj算法

一、背景介绍

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开发者的建议
  1. 只有真正需要用户可感知的应用,才调用startForegroundService()方法来启动前台服务,此时ADJ=PERCEPTIBLE_APP_ADJ(200),常驻内存,并且会在通知栏常驻通知提醒用户,比如音乐播放,地图导航。切勿为了常驻而滥用前台服务,这会严重影响用户体验。

  2. 进程中的Service工作完成后,务必主动调用stopService或stopSelf来停止服务,避免占据内存,浪费系统资源;

  3. 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;

  4. APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放。当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次。

  5. 减少在保活上花心思,更应该在优化内存上下功夫,因为在相同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总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342