美文网首页AndroidAndroid面试二
Android进程管理篇(三)-AMS进程调度

Android进程管理篇(三)-AMS进程调度

作者: Stan_Z | 来源:发表于2019-01-16 21:22 被阅读76次
    一、背景介绍

    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进程管理篇(三)-AMS进程调度

        本文链接:https://www.haomeiwen.com/subject/faevdqtx.html