Android开发程序员Android开发经验谈

Android进程保活 - 自“裁”或者耍流氓(一)

2019-06-25  本文已影响53人  Android架构

本篇文章是后台杀死系列的最后一篇,主要探讨一下进程的保活,Android本身设计的时候是非常善良的,它希望进程在不可见或者其他一些场景下APP要懂得主动释放,可是Android低估了”贪婪“,尤其是很多国产APP,只希望索取来提高自己的性能,不管其他APP或者系统的死活,导致了很严重的资源浪费,这也是Android被iOS诟病的最大原因。本文的保活手段也分两种:遵纪守法的进程保活与流氓手段换来的进程保活。

声明:坚决反对流氓手段实现进程保活 坚决反对流氓进程保活 坚决反对流氓进程保活 “请告诉产品:无法进入白名单”

针对LowmemoryKiller所做的进程保活

LowmemoryKiller会在内存不足的时候扫描所有的用户进程,找到不是太重要的进程杀死,至于LowmemoryKiller杀进程够不够狠,要看当前的内存使用情况,内存越少,下手越狠。在内核中,LowmemoryKiller.c定义了几种内存回收等级如下:(也许不同的版本会有些不同)

static short lowmem_adj[6] = {
    0,
    1,
    6,
    12,
};
static int lowmem_adj_size = 4;

static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,   /* 8MB */
    4 * 1024,   /* 16MB */
    16 * 1024,  /* 64MB */
};
static int lowmem_minfree_size = 4;

lowmem_adj中各项数值代表阈值的警戒级数,lowmem_minfree代表对应级数的剩余内存,两者一一对应,比如当系统的可用内存小于6MB时,警戒级数为0;当系统可用内存小于8M而大于6M时,警戒级数为1;当可用内存小于64M大于16MB时,警戒级数为12。LowmemoryKiller就是根据当前系统的可用内存多少来获取当前的警戒级数,如果进程的oom_adj大于警戒级数并且占内存最大,将会被优先杀死, 具有相同omm_adj的进程,则杀死占用内存较多的。omm_adj越小,代表进程越重要。一些前台的进程,oom_adj会比较小,而后台的服务,omm_adj会比较大,所以当内存不足的时候,Lowmemorykiller先杀掉的是后台服务而不是前台的进程。对于LowmemoryKiller的杀死,这里有一句话很重要,就是: 具有相同omm_adj的进程,则杀死占用内存较多的,因此,如果我们的APP进入后台,就尽量释放不必要的资源,以降低自己被杀的风险。那么如何释放呢?onTrimeMemory是个不错的时机,而onLowmemory可能是最后的稻草,下面复习一下,LowmemoryKiller如何杀进程的,简单看一下实现源码(4.3):(其他版本原理大同小异)

static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    ...     
    <!--关键点1 获取free内存状况-->
    int other_free = global_page_state(NR_FREE_PAGES);
    int other_file = global_page_state(NR_FILE_PAGES);
    <!--关键点2 找到min_adj -->
    for(i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] &&
            other_file < lowmem_minfree[i]) {
            min_adj = lowmem_adj[i];
            break;
        }
    }
  <!--关键点3 找到p->oomkilladj>min_adj并且oomkilladj最大,内存最大的进程-->
    for_each_process(p) {
        // 找到第一个大于等于min_adj的,也就是优先级比阈值低的
        if (p->oomkilladj < min_adj || !p->mm)
            continue;
        // 找到tasksize这个是什么呢
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
        // 找到优先级最低,并且内存占用大的
            if (p->oomkilladj < selected->oomkilladj)
                continue;
            if (p->oomkilladj == selected->oomkilladj &&
                tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;
        lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
                     p->pid, p->comm, p->oomkilladj, tasksize);
    }
    if(selected != NULL) {...
        force_sig(SIGKILL, selected);
    }
    return rem;
}

这里先看一下关键点1,这里是内核获取当前的free内存状况,并且根据当前空闲内存计算出当前后台杀死的等级(关键点2),之后LowmemoryKiller会遍历所有的进程,找到优先级低并且内存占用较大的进程,如果这个进程的p->oomkilladj>min_adj,就表示这个进程可以杀死,LowmemoryKiller就会送过发送SIGKILL信号杀死就进程,注意,lmkd会先找优先级低的进程,如果多个进程优先级相同,就优先杀死内存占用高的进程,这样就为我们提供了两种进程包活手段

不过大多数情况下,Android对于进程优先级的管理都是比较合理,即使某些场景需要特殊手段提高优先级,Android也是给了参考方案的,比如音频播放,UI隐藏的时候,需要将Sevice进程设置成特定的优先级防止被后台杀死,比如一些备份的进程也需要一些特殊处理,但是这些都是在Android允许的范围内的,所以绝大多数情况下,Android是不建议APP自己提高优先级的,因为这会与Android自身的的进程管理相悖,换句话说就是耍流氓。这里先讨论第二种情况,通过合理的释放内存降低被杀的风险,地主不想被杀,只能交公粮,自裁保身,不过这里也要看自裁的时机,什么时候瘦身比较划算,O(∩_∩)O哈哈~!这里就牵扯到有一个onTrimeMemory函数,该函数是一个系统回调函数,主要是Android系统经过综合评估,给APP一个内存裁剪的等级,比如当内存还算充足,APP退回后台时候,会收到TRIM_MEMORY_UI_HIDDEN等级的裁剪,就是告诉APP,释放一些UI资源,比如大量图片内存,一些引入图片浏览缓存的场景,可能就更加需要释放UI资源,下面来看下onTrimeMemory的回调时机及APP应该做出相应处理。

onTrimeMemory的回调时机及内存裁剪等级

OnTrimMemory是在Android 4.0引入的一个回调接口,其主要作用就是通知应用程序在不同的场景下进行自我瘦身,释放内存,降低被后台杀死的风险,提高用户体验,由于目前APP的适配基本是在14之上,所以不必考虑兼容问题。onTrimeMemory支持不同裁剪等级,比如,APP通过HOME建进入后台时,其优先级(oom_adj)就发生变化,从未触发onTrimeMemory回调,这个时候系统给出的裁剪等级一般是TRIM_MEMORY_UI_HIDDEN,意思是,UI已经隐藏,UI相关的、占用内存大的资源就可以释放了,比如大量的图片缓存等,当然,还会有其他很多场景对应不同的裁剪等级。因此,需要弄清楚两个问题:

先看下ComponentCallbacks2中定义的不同裁剪等级的意义:这里一共定义了4+3共7个裁剪等级,为什么说是4+3呢?因为有4个是针对后台进程的,还有3个是针对前台(RUNNING)进程的,目标对象不同,具体看下分析

裁剪等级 数值 目标进程
TRIM_MEMORY_COMPLETE 80 后台进程
TRIM_MEMORY_MODERATE 60 后台进程
TRIM_MEMORY_BACKGROUND 40 后台进程
TRIM_MEMORY_UI_HIDDEN 20 后台进程
TRIM_MEMORY_RUNNING_CRITICAL 15 前台RUNNING进程
TRIM_MEMORY_RUNNING_LOW 10 前台RUNNING进程
TRIM_MEMORY_RUNNING_MODERATE 5 前台RUNNING进程

其意义如下:

以下三个等级针对前台运行应用

以上抽象的说明了一下Android既定参数的意义,下面看一下onTrimeMemory回调的时机及原理,这里采用6.0的代码分析,因为6.0比之前4.3的代码清晰很多:当用户的操作导致APP优先级发生变化,就会调用updateOomAdjLocked去更新进程的优先级,在更新优先级的时候,会扫描一遍LRU进程列表, 重新计算进程的oom_adj,并且参考当前系统状况去通知进程裁剪内存(这里只是针对Android Java层APP),这次操作一般发生在打开新的Activity界面、退回后台、应用跳转切换等等,updateOomAdjLocked代码大概600多行,比较长,尽量精简后如下,还是比较长,这里拆分成一段段梳理:

final void updateOomAdjLocked() {
    final ActivityRecord TOP_ACT = resumedAppLocked();
    <!--关键点1 找到TOP——APP,最顶层显示的APP-->
    final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
    final long oldTime = SystemClock.uptimeMillis() - ProcessList.MAX_EMPTY_TIME;
    mAdjSeq++;
    mNewNumServiceProcs = 0;
    final int emptyProcessLimit;
    final int hiddenProcessLimit;
    <!--关键点2 找到TOP——APP,最顶层显示的APP-->
    // 初始化一些进程数量的限制:
    if (mProcessLimit <= 0) {
        emptyProcessLimit = hiddenProcessLimit = 0;
    } else if (mProcessLimit == 1) {
        emptyProcessLimit = 1;
        hiddenProcessLimit = 0;
    } else {
        // 空进程跟后台非空缓存继承的比例
        emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
        cachedProcessLimit = mProcessLimit - emptyProcessLimit;
    }

     <!--关键点3 确定下进程槽 3个槽->
    int numSlots = (ProcessList.HIDDEN_APP_MAX_ADJ - ProcessList.HIDDEN_APP_MIN_ADJ + 1) / 2;
    // 后台进程/前台进程/空进程
    int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
     int emptyFactor = numEmptyProcs/numSlots;
    if (emptyFactor < 1) emptyFactor = 1;
    int hiddenFactor = (mNumHiddenProcs > 0 ? mNumHiddenProcs : 1)/numSlots;
    if (hiddenFactor < 1) hiddenFactor = 1;
    int stepHidden = 0;
    int stepEmpty = 0;
    int numHidden = 0;
    int numEmpty = 0;
    int numTrimming = 0;
    mNumNonHiddenProcs = 0;
    mNumHiddenProcs = 0;
    int i = mLruProcesses.size();
    // 优先级
    int curHiddenAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 初始化的一些值
    int nextHiddenAdj = curHiddenAdj+1;
    // 优先级
    int curEmptyAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 有意思
    int nextEmptyAdj = curEmptyAdj+2;

这前三个关键点主要是做了一些准备工作,关键点1 是单独抽离出TOP_APP,因为它比较特殊,系统只有一个前天进程,关键点2主要是根据当前的配置获取后台缓存进程与空进程的数目限制,而关键点3是将后台进程分为三备份,无论是后台进程还是空进程,会间插的均分6个优先级,一个优先级是可以有多个进程的,而且并不一定空进程的优先级小于HIDDEN进程优先级。

    for (int i=N-1; i>=0; i--) {
            ProcessRecord app = mLruProcesses.get(i);
            if (!app.killedByAm && app.thread != null) {
                app.procStateChanged = false;
                <!--关键点4 计算进程的优先级或者缓存进程的优先级->   
                // computeOomAdjLocked计算进程优先级,但是对于后台进程和empty进程computeOomAdjLocked无效,这部分优先级是AMS自己根据LRU原则分配的
                computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
                //还未最终确认,有些进程的优先级,比如只有后台activity或者没有activity的进程,
              <!--关键点5 计算进程的优先级或者缓存进程的优先级->   
                if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
                    switch (app.curProcState) {
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                            app.curRawAdj = curCachedAdj;
                                    <!--关键点6 根据LRU为后台进程分配优先级-->
                            if (curCachedAdj != nextCachedAdj) {
                                stepCached++;
                                if (stepCached >= cachedFactor) {
                                    stepCached = 0;
                                    curCachedAdj = nextCachedAdj;
                                    nextCachedAdj += 2;
                                    if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                        default:
                                                                <!--关键点7 根据LRU为后台进程分配优先级-->
                            app.curRawAdj = curEmptyAdj;
                            app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
                            if (curEmptyAdj != nextEmptyAdj) {
                                stepEmpty++;
                                if (stepEmpty >= emptyFactor) {
                                    stepEmpty = 0;
                                    curEmptyAdj = nextEmptyAdj;
                                    nextEmptyAdj += 2;
                                    if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                    }
                }
                <!--关键点8 设置优先级-->
                applyOomAdjLocked(app, true, now, nowElapsed);

上面的这几个关键点主要是为所有进程计算出其优先级oom_adj之类的值,对于非后台进程,比如HOME进程 服务进程,备份进程等都有自己的独特的计算方式,而剩余的后台进程就根据LRU三等分配优先级。

                 <!--关键点9 根据缓存进程的数由AMS选择性杀进程,后台进程太多-->
                switch (app.curProcState) {
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                        mNumCachedHiddenProcs++;
                        numCached++;
                        if (numCached > cachedProcessLimit) {
                            app.kill("cached #" + numCached, true);
                        }
                        break;
                    case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
                        if (numEmpty > ProcessList.TRIM_EMPTY_APPS
                                && app.lastActivityTime < oldTime) {
                            app.kill("empty for "
                                    + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
                                    / 1000) + "s", true);
                        } else {
                            numEmpty++;
                            if (numEmpty > emptyProcessLimit) {
                                app.kill("empty #" + numEmpty, true);
                            }
                        }
                        break;
                    default:
                        mNumNonCachedProcs++;
                        break;
                }
                 <!--关键点10 计算需要裁剪进程的数目-->
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                        // 比home高的都需要裁剪,不包括那些等级高的进程
                    numTrimming++;
                }
            }
        }

上面的两个关键点是看当前后台进程是否过多或者过老,如果存在过多或者过老的后台进程,AMS是有权利杀死他们的。之后才是我们比较关心的存活进程的裁剪:

        final int numCachedAndEmpty = numCached + numEmpty;
        int memFactor;
         <!--关键点11 根据后台进程数目确定当前系统的内存使用状况 ,确立内存裁剪等级(内存因子)memFactor,android的理念是准许存在一定数量的后台进程,并且只有内存不够的时候,才会缩减后台进程-->
        if (numCached <= ProcessList.TRIM_CACHED_APPS
                && numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
            // 等级高低 ,杀的越厉害,越少,需要约紧急的时候才杀
            if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {//3
                memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
            } else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) { //5
                memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
            } else {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
            }
        } else {
            // 后台进程数量足够说明内存充足
            memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
        }
       <!--关键点12 根据内存裁剪等级裁剪内存 Android认为后台进程不足的时候,内存也不足-->
        if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
            if (mLowRamStartTime == 0) {
                mLowRamStartTime = now;
            }
            int step = 0;
            int fgTrimLevel;
         // 内存不足的时候,也要通知前台或可见进程进行缩减
            switch (memFactor) {
                case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
                    break;
                case ProcessStats.ADJ_MEM_FACTOR_LOW:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
                    break;
                default:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
                    break;
            }
            int factor = numTrimming/3;
            int minFactor = 2;
            if (mHomeProcess != null) minFactor++;
            if (mPreviousProcess != null) minFactor++;
            if (factor < minFactor) factor = minFactor;
            int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;

关键点11这里不太好理解:Android系统根据后台进程的数目来确定当前系统内存的状况,后台进程越多,越说明内存并不紧张,越少,说明越紧张,回收等级也就越高,如果后台进程的数目较多,内存裁剪就比较宽松是ProcessStats.ADJ_MEM_FACTOR_NORMAL,如果不足,则再根据缓存数目划分等级。以6.0源码来说:

与之对应的关键点12,是确立前台RUNNING进程(也不一定是前台显示)的裁剪等级。

之后就真正开始裁剪APP,这里先看后台进程不足的情况的裁剪,这部分相对复杂一些:

            <!--裁剪后台进程-->
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                if (allChanged || app.procStateChanged) {
                    setProcessTrackerStateLocked(app, trackerMemFactor, now);
                    app.procStateChanged = false;
                }   
                //  PROCESS_STATE_HOME = 12;  
                //PROCESS_STATE_LAST_ACTIVITY = 13; 退到后台的就会用
                // 优先级比较低,回收等级比较高ComponentCallbacks2.TRIM_MEMORY_COMPLETE
                //  当curProcState > 12且没有被am杀掉的情况;上面的update的时候,在kill的时候,是会设置app.killedByAm的
                //裁剪的话,如果 >= ActivityManager.PROCESS_STATE_HOME,老的裁剪等级较高,不重要,越新鲜的进程,裁剪等级越低

                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                     // 先清理最陈旧的 ,最陈旧的那个遭殃
                    if (app.trimMemoryLevel < curLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(curLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = curLevel;
                    step++; 
                    // 反正一共就三个槽,将来再次刷新的 时候,要看看是不是从一个槽里面移动到另一个槽,
                    // 没有移动,就不需要再次裁剪,等级没变
                    if (step >= factor) {
                        step = 0;
                        switch (curLevel) {
                            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
                                break;
                            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
                                break;
                        }
                    }
                }

上面的这部分是负责 app.curProcState >= ActivityManager.PROCESS_STATE_HOME这部分进程裁剪,主要是针对后台缓存进程,一般是oom_adj在9-11之间的进程,根据LRU确定不同的裁减等级。

                else {
                    if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                            || app.systemNoUi) && app.pendingUiClean) {
                        // 释放UI
                        final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
                        if (app.trimMemoryLevel < level && app.thread != null) {
                            try {
                                app.thread.scheduleTrimMemory(level);
                            } catch (RemoteException e) {
                            }
                        }
                        app.pendingUiClean = false;
                    }
                    // 启动的时候会回调一遍,如果有必要,启动APP的时候,app.trimMemoryLevel=0
                    if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(fgTrimLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = fgTrimLevel;
                }
            }
        } 

而这里的裁剪主要是一些优先级较高的进程,其裁剪一般是 ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ,由于这部分进程比较重要,裁剪等级较低,至于前台进程的裁剪,一般是在启动的时候,这个时候app.pendingUiClean==false,只会裁剪当前进程:

        else {
              <!--关键点13 内存充足的时候,进程的裁剪-->
             ... 
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                // 在resume的时候,都是设置成true,所以退回后台的时候app.pendingUiClean==true是满足的,
                // 因此缩减一次,但是不会再次走这里的分支缩减即使优先级变化,但是已经缩减过
                // 除非走上面的后台流程,那个时候这个进程的等级已经很低了,
                if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                        || app.systemNoUi) && app.pendingUiClean) {
                    if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
                            && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(
                                    ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
                        } catch (RemoteException e) {
                        }
                    }
                    // clean一次就弄成false
                    app.pendingUiClean = false;
                }
                // 基本算没怎么裁剪
                app.trimMemoryLevel = 0;
            }
        }
 }

最后这部分是后台进程数量充足的时候,系统只会针对app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND的进程进行裁剪,而裁剪等级也较低:ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,因此根据裁剪等级APP可以大概知道系统当前的内存状况,同时也能知道系统希望自己如何裁剪,之后APP做出相应的瘦身即可。不过,上面的进程裁剪的优先级是完全根据后台进程数量来判断的,但是,不同的ROM可能进行了改造,所以裁剪等级不一定完全准确,比如在开发者模式打开限制后台进程数量的选项,限制后台进程数目不超过2个,那么这个时候的裁剪等级就是不太合理的,因为内存可能很充足,但是由于限制了后台进程的数量,导致裁剪等级过高。因此在使用的时候,最好结合裁剪等级与当前内存数量来综合考量。

上一篇 下一篇

猜你喜欢

热点阅读