解读Android进程优先级ADJ算法

本文基于最新的Android P源码来解读进程优先级ADJ原理,基于篇幅会精炼部分代码

一、概述

1.1 进程

Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。一个APP可以拥有多个进程,多个APP也可以运行在同一个进程,通过配置Android:process属性来决定。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。

1.2 优先级

Android系统的设计理念正是希望应用进程能尽量长时间地存活,以提升用户体验。应用首次打开比较慢,这个过程有进程创建以及Application等信息的初始化,所以应用在启动之后,即便退到后台并非立刻杀死,而是存活一段时间,这样下次再使用则会非常快。对于APP同样希望自身尽可能存活更长的时间,甚至探索各种保活黑科技。物极必反,系统处于低内存的状态下,手机性能会有所下降;系统继续放任所有进程一直存活,系统内存很快就会枯竭而亡,那么需要合理地进程回收机制。

到底该回收哪个进程呢?系统根据进程的组件状态来决定每个进程的优先级值ADJ,系统根据一定策略先杀优先级最低的进程,然后逐步杀优先级更低的进程,依此类推,以回收预期的可用系统资源,从而保证系统正常运转。

谈到优先级,可能有些人会想到Linux进程本身有nice值,这个能决定CPU资源调度的优先级;而本文介绍Android系统中的ADJ,主要决定在什么场景下什么类型的进程可能会被杀,影响的是进程存活时间。ADJ与nice值两者定位不同,不过也有一定的联系,优先级很高的进程,往往也是用户不希望被杀的进程,是具有有一定正相关性。

1.3 ADJ级别

解读Android进程优先级ADJ算法_第1张图片

从Android 7.0开始,ADJ采用100、200、300,老版本采用的是个位数。为了防止剩余内存过低,Android在内核空间有lowmemorykiller(简称LMK),LMK是通过注册shrinker来触发低内存回收的,这个机制并不太优雅,可能会拖慢Shrinkers内存扫描速度,已从内核4.12中移除,后续会采用用户空间的LMKD + memory cgroups机制,这里先不展开LMK讲解。

进程刚启动时ADJ等于INVALID_ADJ,当执行完attachApplication(),该该进程的curAdj和setAdj不相等,则会触发执行setOomAdj()将该进程的节点/proc/pid/oom_score_adj写入oomadj值。阈值设置过程会根据手机屏幕尺寸、内存大小来调整,下图参数为64位机器的Android原生阈值图:

解读Android进程优先级ADJ算法_第2张图片

LMK杀进程过程:当系统剩余空闲内存低于某阈值(比如147MB),则从ADJ大于或等于相应阈值(比如900)的进程中,选择ADJ值最大的进程,如果存在多个ADJ相同的进程,则选择内存最大的进程,让向目标进程发出signal 9来杀掉该进程。

二、解读ADJ

接下来,解读每个ADJ值都对应着怎样条件的进程,包括正在运行的组件以及这些组件的状态几何。这里重点介绍上图标红的ADJ级别所对应的进程。

Android系统中计算各进程ADJ算法的核心方法:

  • updateOomAdjLocked:更新adj,当目标进程为空或者被杀则返回false;否则返回true;

  • computeOomAdjLocked:计算adj,返回计算后RawAdj值;

  • applyOomAdjLocked:应用adj,当需要杀掉目标进程则返回false;否则返回true。

当Android四大组件状态改变时会updateOomAdjLocked()来同步更新相应进程的ADJ优先级。这里需要说明一下,当同一个进程有多个决定其优先级的组件状态时,取优先级最高的ADJ作为最终的ADJ。另外,进程会通过设置maxAdj来限定ADJ的上限。

关于分析进程ADJ相关信息,常用命令如下:

  • dumpsys meminfo,

  • dumpsys activity o

  • dumpsys activity p

本文重点介绍上面ADJ图中标红的级别,对于非标红级别这里简单说明一下:

ADJ<0优先级说明:

  • NATIVE_ADJ(-1000):是由init进程fork出来的Native进程,并不受system管控;

  • SYSTEM_ADJ(-900):是指system_server进程;

  • PERSISTENT_PROC_ADJ(-800): 是指在AndroidManifest.xml中申明android:persistent=”true”的系统(即带有FLAG_SYSTEM标记)进程,persistent进程一般情况并不会被杀,即便被杀或者发生Crash系统会立即重新拉起该进程。

  • PERSISTENT_SERVICE_ADJ(-700):是由startIsolatedProcess()方式启动的进程,或者是由system_server或者persistent进程所绑定(并且带有BIND_ABOVE_CLIENT或者BIND_IMPORTANT)的服务进程

其他优先级:

  • BACKUP_APP_ADJ(300):执行bindBackupAgent()过程的进程

  • HEAVY_WEIGHT_APP_ADJ(400): realStartActivityLocked()过程,当应用的privateFlags标识PRIVATE_FLAG_CANT_SAVE_STATE的进程;

  • HOME_APP_ADJ(600):当类型为ACTIVITY_TYPE_HOME的应用,比如桌面APP

  • PREVIOUS_APP_ADJ(700):用户上一个使用的APP进程

2.1 FOREGROUND_APP_ADJ(0)

场景1:满足以下任一条件的进程都属于FOREGROUND_APP_ADJ(0)优先级:

  • 正处于resumed状态的Activity

  • 正执行一个生命周期回调的Service(比如执行onCreate,

    onStartCommand, onDestroy等)

  • 正执行onReceive()的广播接收者

  • 通过startInstrumentation()启动的进程

源码如下:

解读Android进程优先级ADJ算法_第3张图片

场景2: 当客户端进程activity里面调用bindService()方法时flags带有BIND_ADJUST_WITH_ACTIVITY参数,并且该activity处于可见状态,则当前服务进程也属于前台进程,源码如下:

解读Android进程优先级ADJ算法_第4张图片


provider客户端

场景3: 对于provider进程,还有以下两个条件能成为前台进程:

  • 当Provider的客户端进程ADJ<=FOREGROUND_APP_ADJ时,则Provider进程ADJ等于FOREGROUND_APP_ADJ

  • 当Provider有外部(非框架)进程依赖,也就是调用了getContentProviderExternal()方法,则ADJ至少等于FOREGROUND_APP_ADJ

解读Android进程优先级ADJ算法_第5张图片

2.2 VISIBLE_APP_ADJ(100)

可见进程:当ActivityRecord的visible=true,也就是Activity可见的进程。

解读Android进程优先级ADJ算法_第6张图片

从Android P开始,进一步细化ADJ级别,增加了VISIBLE_APP_LAYER_MAX(99),是指VISIBLE_APP_ADJ(100)跟PERCEPTIBLE_APP_ADJ(200)之间有99个槽,则可见级别ADJ的取值范围为[100,199]。 算法会根据其所在task的mLayerRank来调整其ADJ,100加上mLayerRank就等于目标ADJ,layer越大,则ADJ越小。 关于TaskRecord的mLayerRank的计算方式是ASS的rankTaskLayersIfNeeded()方法,当TaskRecord顶部的ActivityRecord为空或者结束或者不可见时,则设置该TaskRecord的mLayerRank等于-1; 每个ActivityDisplay的baseLayer都是从0开始,从最上面的TaskRecord开始,第一个ADJ=100,从上至下依次加1,直到199为上限。

解读Android进程优先级ADJ算法_第7张图片

service客户端

ServiceRecord的成员变量startRequested=true,是指被显式调用了startService()方法。当service被stop或kill会将其置为false。

一般情况下,即便客户端进程处于前台进程(ADJ=0)级别,服务进程只会提升到可见(ADJ=1)级别。以下flags是由调用bindService()过程所传递的flags来决定的。

解读Android进程优先级ADJ算法_第8张图片

作为工程师很多时候可能还是想看看源码,show me the code。但是关于ADJ计算这一块源码场景computeOomAdjLocked(),Google真心写得比较乱,为了更清晰地说明客户端进程如何影响服务进程,在保证不失去原意的情况下重写了这块部分逻辑:

这个过程主要根据service本身、client端情况以及activity状态分别来调整adj和schedGroup

解读Android进程优先级ADJ算法_第9张图片

上段代码说明服务端进程优先级(adj)不会低于客户端进程优先级(newAdj),而newAdj的上限受限于flags,具体服务端进程受客户端进程影响的ADJ上限如下:

  • BIND_ABOVE_CLIENT或BIND_IMPORTANT的情况下,ADJ上限为PERSISTENT_SERVICE_ADJ;

  • BIND_NOT_VISIBLE的情况下, ADJ上限为PERCEPTIBLE_APP_ADJ;

  • 否则,一般情况下,ADJ上限为VISIBLE_APP_ADJ;

由此,可见当bindService过程带有BIND_ABOVE_CLIENT或者BIND_IMPORTANT flags的同时,客户端进程ADJ小于或等于PERSISTENT_SERVICE_ADJ的情况下,该进程则为PERSISTENT_SERVICE_ADJ。另外,即便是启动过Activity的进程,当客户端进程ADJ<=200时,还是可以提升该服务进程的优先级。

2.3 PERCEPTIBLE_APP_ADJ(200)

可感知进程:当该进程存在不可见的Activity,但Activity正处于PAUSING、PAUSED、STOPPING状态,则为PERCEPTIBLE_APP_ADJ

解读Android进程优先级ADJ算法_第10张图片

满足以下任一条件的进程也属于可感知进程:

  • foregroundServices非空:前台服务进程,执行startForegroundService()方法

  • app.forcingToImportant非空:执行setProcessImportant()方法,比如Toast弹出过程。

  • hasOverlayUi非空:非activity的UI位于屏幕最顶层,比如显示类型TYPE_APPLICATION_OVERLAY的窗口。

解读Android进程优先级ADJ算法_第11张图片

2.4 SERVICE_ADJ(500)

服务进程:没有启动过Activity,并且30分钟之内活跃过的服务进程。 startRequested为true,则代表执行startService()且没有stop的进程。

解读Android进程优先级ADJ算法_第12张图片

2.5 SERVICE_B_ADJ(800)

进程由SERVICE_ADJ(500)降低到SERVICE_B_ADJ(800),有以下两种情况:

  • A类Service占比过高:当A类Service个数 > Service总数的1/3时,则加入到B类Service。换句话说,B Service的个数至少是A Service的2倍。

  • 内存紧张&&A类Service占用内存较高:当系统内存紧张级别(mLastMemoryLevel)高于ADJ_MEM_FACTOR_NORMAL,且该应用所占内存lastPss大于或等于CACHED_APP_MAX_ADJ级别所对应的内存阈值的1/3(默认值阈值约等于110MB)。

源码如下:

解读Android进程优先级ADJ算法_第13张图片


ADJ_MEM_FACTOR

这里顺便一下,内存因子ADJ_MEM_FACTOR共有4个级别,当前处于哪个内存因子级别,取决于当前进程中cached进程和空进程的个数。

解读Android进程优先级ADJ算法_第14张图片

ADJ内存因子:决定允许后台运行Jobs的最大上限,以及决定TrimMemory的级别(包括ThreadedRenderer的回收级别),再进一步来看看内存因子:

再来看看cached和empty进程:

解读Android进程优先级ADJ算法_第15张图片

用于限制empty或cached进程的上限为16个,并且empty超过8个时会清理掉30分钟没有活跃的进程。 cached和empty主要是区别是否有Activity。

2.6 CACHED_APP_MIN_ADJ(900)

缓存进程优先级从CACHED_APP_MIN_ADJ(900)到 CACHED_APP_MAX_ADJ(906)。

ADJ的转换算法:先计算出从900~906之间,cached进程和empty的numSlots=3,也就是各有3个槽; 然后分别根据cached进程和empty进程个数除以numSlots来确定单个槽的进程个数,也就是emptyFactor和cachedFactor,计算过程 每次adj加2,分布如下图:

解读Android进程优先级ADJ算法_第16张图片

则cached进程的adj分布在900, 901, 903, 905,empty进程的adj分布在900, 902, 904, 906。

cached进程是指:

  • 存在Activity且该Activity窗口不可见,并且不处于PAUSING、PAUSED、STOPPING的任一状态的情况下,则设置该进程为PROCESS_STATE_CACHED_ACTIVITY;

  • 当该进程Service的客户端进程存在Activity或者是treatLikeActivity的进程

empty进程往往是指没有activity的低优先级进程。

三、总结

Android进程优先级ADJ的每一个ADJ级别往往都有多种场景,使用adjType完美地区分相同ADJ下的不同场景; 不同ADJ进程所对应的schedGroup不同,从而分配的CPU资源也不同,schedGroup大体分为TOP(T)、前台(F)、后台(B); ADJ跟AMS中的procState有着紧密的联系。

  • adj:通过调整oom_score_adj来影响进程寿命(LMK杀进程策略);

  • schedGroup:影响进程的CPU资源调度与分配;

  • procState:从进程所包含的四大组件运行状态来评估进程状态,影响framework的内存控制策略。比如控制缓存进程和空进程个数上限依赖于procState,再比如控制APP执行handleLowMemory()的触发时机等。

为了说明整体关系,以ADJ为中心来讲解跟adjType,schedGroup,procState的对应关系,下面以一幅图来诠释整个ADJ算法的精髓,几乎涵盖了ADJ算法调整的绝大多数场景。

解读Android进程优先级ADJ算法_第17张图片

CPU调度组:

解读Android进程优先级ADJ算法_第18张图片

  1. 常说的前台进程与后台进程,其实是从CPU调度角度来划分的前台与后台;为了让用户正在使用的TOP进程能分配到更多的CPU资源,从Android 6.0开始新增了TOP进程组,CPU调度优先分配给当前正在跟用户交互的APP,提升用户体验。

  2. 上图adjType=”broadcast”的CPU调度组的选择取决于广播队列,当receiver接收到的广播来自于前台广播队列则采用前台进程组,当receiver接收到的广播来自于后台广播队列则采用后台进程组。前后台广播队列的CPU资源调度优先级不同,所以前台广播超时10秒就会ANR,而后台广播超时60秒才会ANR。更少的CPU资源分配就需要更长的时间来完成执行,这也就是为何两个广播队列定义了不同的超时阈值。

  3. 上图adjType=”exec-service”的CPU调度组的选择取决于caller, 当发起bindService或者startService的调用者caller属于后台进程组,callerFg=false,则Service的生命周期回调运行在后台进程组,非常很少的CPU资源;当caller属于前台或者TOP进程组,则Service的生命周期回调运行在前台进程组,分配较多的CPU资源。

  4. 上图adjType=”service”也有机会选择TOP组, 前提条件是在bindService的时候带有BIND_IMPORTANT的flags,用于标记该服务对于客户端进程很重要。

最后,给广大应用开发者一些友好的建议:

  1. UI进程与Service进程一定要分离,因为对于包含activity的service进程,一旦进入后台就成为”cch-started-ui-services”类型的cache进程(ADJ>=900),随时可能会被系统回收;而分离后的Service进程服务属于SERVICE_ADJ(500),被杀的可能性相对较小。尤其是系统允许自启动的服务进程必须做UI分离,避免消耗系统较大内存。

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

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

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

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

你可能感兴趣的:(解读Android进程优先级ADJ算法)