android内存管理-lowmemorykiller 机制

android内存管理-lowmemorykiller 机制
概念
andorid用户层的application process ,在各种activity生命周期切换时,会触发AMS中的回收机制,比如启动新的apk,一直back 退出一个apk,在5.1上的代码来看,除了android AMS中默认的回收机制外,还会去维护一个oom adj 变量,作为linux层 lowmemorykiller的参考依据。

AMS回收机制
入口为trimApplications() 可以发现很多地方有调用,stop unregisterreceiver之类的操作时都会去触发回收:

final void trimApplications() {
    synchronized (this) {
        int i;
         // First remove any unused application processes whose package
        // has been removed.
        for (i=mRemovedProcesses.size()-1; i>=0; i--) {
            final Proce***ecord app = mRemovedProcesses.get(i);
            if (app.activities.size() == 0
                    && app.curReceiver == null && app.services.size() == 0) {
                Slog.i(
                    TAG, "Exiting empty application process "
                    + app.processName + " ("
                    + (app.thread != null ? app.thread.asBinder() : null)
                    + ")\n");
                if (app.pid > 0 && app.pid != MY_PID) {
                    app.kill("empty", false);
                } else {
                    try {
                        app.thread.scheduleExit();
                    } catch (Exception e) {
                        // Ignore exceptions.
                    }
                }
                cleanUpApplicationRecordLocked(app, false, true, -1);
                mRemovedProcesses.remove(i);

                if (app.persistent) {
                    addAppLocked(app.info, false, null /* ABI override */);
                }
            }
        }
        // Now update the oom adj for all processes.
        updateOomAdjLocked();
    }
}

mRemovedProcesses 列表中主要包含了 crash 的进程、5 秒内没有响应并被用户选在强制关闭的进程、以及应用开发这调用 killBackgroundProcess 想要杀死的进程。调用 Process.killProcess 将所有此类进程全部杀死。updateOomAdjLocked 计算更新所有process的 oomadj继续看:

final void updateOomAdjLocked() {
    // First update the OOM adjustment for each of the
    // application processes based on their current state.
    int curCachedAdj = ProcessList.CACHED_APP_MIN_ADJ;
    int nextCachedAdj = curCachedAdj+1;
    int curEmptyAdj = ProcessList.CACHED_APP_MIN_ADJ;
    int nextEmptyAdj = curEmptyAdj+2;
    for (int i=N-1; i>=0; i--) {
        Proce***ecord app = mLruProcesses.get(i);
        if (!app.killedByAm && app.thread != null) {
            app.procStateChanged = false;
            computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
 applyOomAdjLocked(app, TOP_APP, true, now);
  }
 }
}

computeOomAdjLocked 计算当前process的adj,详细规则就不贴了 https://blog.csdn.net/gaugamela/articledetails/54176460 (大家可以参考这篇文档),大体会根据 top activity .receiver.service process state 一大堆相关的因素去得出一个adj值。adj 值 (-17 ~15) 越小优先级越高,从注释能看出来,策略在kernel中的lowmemorykiller驱动中实现
这里看下定义 在\frameworks\base\services\core\java\com\android\server\am\ProcessList.java :

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 15;
static final int CACHED_APP_MIN_ADJ = 9;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 8;
// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app.  This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 7;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 6;
// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 5;
// This is a process with a heavy-weight application.  It is in the
// background, but we want to try to avoid killing it.  Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 4;
// This is a process currently hosting a backup operation.  Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 3;
// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;
// This is the process running the current foreground app.  We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;
// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -11;

// This is a system persistent process, such as telephony.  Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -12;
// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -16;
// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -17;

applyOomAdjLocked 将上面计算好的adj值经过一定的修整,设置到对应的process。只关注跟lowmemorykiller 相关的调用接口, 省略了AMS中自己根据PROCESS_STATE的 kill策略, 大体有如下: 必须是非 persistent 进程,即非系统进程; 必须是空进程,即进程中没有任何 activity 存在。如果杀死存在 Activity 的进程,有可能关闭用户正在使用的程序,或者使应用程序恢复的时延变大,从而影响用户体验; 必须无 broadcast receiver。运行 broadcast receiver 一般都在等待一个事件的发生,用户并不希望此类程序被系统强制关闭;
进程中 service 的数量必须为 0。存在 service 的进程很有可能在为一个或者多个程序提供某种服务,如 GPS 定位服务。杀死此类进程将使其他进程无法正常服务。

看下 applyOomAdjLocked中的核心:

private final boolean applyOomAdjLocked(Proce***ecord app,
      Proce***ecord TOP_APP, boolean doingAll, long now) {

    if (app.curAdj != app.setAdj) {
        ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
        if (DEBUG_SWITCH || DEBUG_OOM_ADJ) Slog.v(
            TAG, "Set " + app.pid + " " + app.processName +
            " adj " + app.curAdj + ": " + app.adjType);
        app.setAdj = app.curAdj;
    }
}

设置oomadj ,继续看 ProcessList.java

/**
 * Set the out-of-memory badness adjustment for a process.
 *
 * @param pid The process identifier to set.
 * @param uid The uid of the app
 * @param amt Adjustment value -- lmkd allows -16 to +15.
 *
 * {@hide}
 */
public static final void setOomAdj(int pid, int uid, int amt) {
    if (amt == UNKNOWN_ADJ)
        return;

    long start = SystemClock.elapsedRealtime();
    ByteBuffer buf = ByteBuffer.allocate(4 * 4);
    buf.putInt(LMK_PROCPRIO);
    buf.putInt(pid);
    buf.putInt(uid);
    buf.putInt(amt);
    writeLmkd(buf);
    long now = SystemClock.elapsedRealtime();
    if ((now-start) > 250) {
        Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid
                + " = " + amt);
    }
}

writeLmkd 写process pid uid 以及adj 的buf, 以一个定义command打头

private static void writeLmkd(ByteBuffer buf) {

    for (int i = 0; i < 3; i++) {
        if (sLmkdSocket == null) {
                if (openLmkdSocket() == false) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
        }

        try {
            sLmkdOutputStream.write(buf.array(), 0, buf.position());
            return;
        } catch (IOException ex) {
            Slog.w(ActivityManagerService.TAG,
                   "Error writing to lowmemorykiller socket");

            try {
                sLmkdSocket.close();
            } catch (IOException ex2) {
            }

            sLmkdSocket = null;
        }
    }
}
}

可以看到try了3次 ,去打开对应的socket 然后写数据,openLmkdSocket 实现如下,android的 LocalSocket 机制,通过lmkd 这个socket通信

 sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
        sLmkdSocket.connect(
            new LocalSocketAddress("lmkd",
                    LocalSocketAddress.Namespace.RESERVED));
        sLmkdOutputStream = sLmkdSocket.getOutputStream();

##lmkd service

这是作为client去请求connect ,而service端的处理在 \system\core\lmkd\lmkd.c , 可以看下这个service的启动:

service lmkd /system/bin/lmkd
class core
critical
socket lmkd seqpacket 0660 system system

标准的 C/S模式,AMS中发起的通信command 开头种类,以及buf格式 如下:

// LMK_TARGET <minfree> <minkillprio> ... (up to 6 pairs)
// LMK_PROCPRIO <pid> <prio>  
// LMK_PROCREMOVE <pid>
static final byte LMK_TARGET = 0;
static final byte LMK_PROCPRIO = 1;
static final byte LMK_PROCREMOVE = 2;

设置单一process 用的是 LMK_PROCPRIO
设置整个LMK adj minfree策略的是 LMK_TARGET
kill process用 LMK_PROCREMOVE

这个service 的代码比较短 1K不到,机制也比较简单:

int main(int argc __unused, char **argv __unused) {
struct sched_param param = {
        .sched_priority = 1,
};

mlockall(MCL_FUTURE);
sched_setscheduler(0, SCHED_FIFO, ¶m);
if (!init())
    mainloop();
ALOGI("exiting");
return 0;
}

init 那些socket相关,注册event handle func ,最后跳到mainloop去 循环poll等待

ctrl_lfd = android_get_control_socket("lmkd");
if (ctrl_lfd < 0) {
    ALOGE("get lmkd control socket failed");
    return -1;
}

细节不做关注,直接看收到刚刚 AMS那边发过来的command buf 的处理:

static void ctrl_command_handler(void) {
int ibuf[CTRL_PACKET_MAX / sizeof(int)];
int len;
int cmd = -1;
int nargs;
int targets;

len = ctrl_data_read((char *)ibuf, CTRL_PACKET_MAX);
if (len <= 0)
    return;

nargs = len / sizeof(int) - 1;
if (nargs < 0)
    goto wronglen;

cmd = ntohl(ibuf[0]);

switch(cmd) {
case LMK_TARGET:
    targets = nargs / 2;
    if (nargs & 0x1 || targets > (int)ARRAY_SIZE(lowmem_adj))
        goto wronglen;
    cmd_target(targets, &ibuf[1]);
    break;
case LMK_PROCPRIO:
    if (nargs != 3)
        goto wronglen;
    cmd_procprio(ntohl(ibuf[1]), ntohl(ibuf[2]), ntohl(ibuf[3]));
    break;
case LMK_PROCREMOVE:
    if (nargs != 1)
        goto wronglen;
    cmd_procremove(ntohl(ibuf[1]));
    break;
default:
    ALOGE("Received unknown command code %d", cmd);
    return;
}

return;

wronglen:
ALOGE("Wrong control socket read length cmd=%d len=%d", cmd, len);
}

先看上面对应的LMK_PROCPRIO 设置某个process 的adj ,核心如下:

snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
snprintf(val, sizeof(val), "%d", lowmem_oom_adj_to_oom_score_adj(oomadj));
writefilestring(path, val);

直接写process对应的文件节点,传下来的adj值 做了一个转换再写进 oom_score_adj,当oom_adj = 15, 则 oom_score_adj = 1000;
其他的, 则 oom_score_adj = oom_adj * 1000/-17:

static int lowmem_oom_adj_to_oom_score_adj(int oom_adj)
{
if (oom_adj == OOM_ADJUST_MAX) //15
    return OOM_SCORE_ADJ_MAX; //1000
else
    return (oom_adj * OOM_SCORE_ADJ_MAX) / -OOM_DISABLE;//-17
}

kernel驱动中process manager 会有对应的处理机制 ,转换后个oom_score_adj保存在进程结构体 kernel\include\linux\sched.h
task_struct->signal_struct->short oom_score_adj; / OOM kill score adjustment /

##lowmemorykiller driver
kernel中的支持:
autoconf.h中

#define CONFIG_ANDROID_LOW_MEMORY_KILLER 1

可自行到kernel中 make menuconfig 查看

驱动目录:\kernel\drivers\staging\android\lowmemorykiller.c

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}

#ifdef CONFIG_ANDROID_LOW_MEMORY_KILLER_AUTODETECT_OOM_ADJ_VALUES
__module_param_call(MODULE_PARAM_PREFIX, adj,
        &lowmem_adj_array_ops,
        .arr = &__param_arr_adj,
        S_IRUGO | S_IWUSR, -1);
__MODULE_PARM_TYPE(adj, "array of short");
#else
module_param_array_named(adj, lowmem_adj, short, &lowmem_adj_size,
         S_IRUGO | S_IWUSR);
#endif
module_param_array_named(minfree, lowmem_minfree, uint, &lowmem_minfree_size,
         S_IRUGO | S_IWUSR);

注册了一个shrinker ,这个机制之前没接触过,大体的意义就是向系统注册了这个shrinker 回调函数之后,当系统空闲内存页面不足时会调用。 CONFIG_ANDROID_LOW_MEMORY_KILLER_AUTODETECT_OOM_ADJ_VALUES 支持动态改 策略阀门 值。 另外注册了文件ops 以及节点 adj .minfree 分别与lowmem_adj . lowmen_minfree 数组成对应关系策略数组以及阀值,两个数组之间也是一一对应关系,当内存小于64M时 就去准备 kill adj >=12 的process,取最低优先级 先kill, 比如此时有3个进程分别为 12 12 14 ,那么首先kill掉的是 14 ,kill之后还是少于64M 那么两个12 adj之间,先杀占用高的,这个函数实现在下面的 lowmem_shrink 中。

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 */
};

先看下处理函数 lowmem_shrinker:

static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
    struct task_struct *tsk;
    struct task_struct *selected = NULL;
    int rem = 0;
    int tasksize;
    int i;
    short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
    int minfree = 0;
    int selected_tasksize = 0;
    short selected_oom_score_adj;
    int array_size = ARRAY_SIZE(lowmem_adj);
    int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
    int other_file = global_page_state(NR_FILE_PAGES) -
                    global_page_state(NR_SHMEM);

    if (lowmem_adj_size < array_size)
         array_size = lowmem_adj_size;
    if (lowmem_minfree_size < array_size)
         array_size = lowmem_minfree_size;
    for (i = 0; i < array_size; i++) {  
     //依次遍历策略阀值数组,从小到大,根据当前memory free情况,取触发adj值
        minfree = lowmem_minfree[i];
        if (other_free < minfree && other_file < minfree) {
            min_score_adj = lowmem_adj[i];
            break;
        }
    }
}

//这里得到的min_score_adj 就是此时内存状态下 将会kill掉的最小score_adj

for_each_process(tsk) {
    ...
    tasksize = get_mm_rss(p->mm);
    ...
    if (selected) {
        if (oom_score_adj < selected_oom_score_adj)
            continue;
        if (oom_score_adj == selected_oom_score_adj &&
            tasksize <= selected_tasksize)
            continue;
    }//可以看到 遍历一圈process 只为找到一个 oom_score_adj tasksize 最大的process
    selected = p;
    selected_tasksize = tasksize;
    selected_oom_score_adj = oom_score_adj;
}
if (selected) {
    lowmem_print(1, "Killing '%s' (%d), adj %hd,\n" \
            "   to free %ldkB on behalf of '%s' (%d) because\n" \
            "   cache %ldkB is below limit %ldkB for oom_score_adj %hd\n" \
            "   Free memory is %ldkB above reserved\n",
             selected->comm, selected->pid,
             selected_oom_score_adj,
             selected_tasksize * (long)(PAGE_SIZE / 1024),
             current->comm, current->pid,
             other_file * (long)(PAGE_SIZE / 1024),
             minfree * (long)(PAGE_SIZE / 1024),
             min_score_adj,
             other_free * (long)(PAGE_SIZE / 1024));

    trace_lowmem_kill(selected,  other_file, minfree, min_score_adj, other_free);

    lowmem_deathpending_timeout = jiffies + HZ;
    send_sig(SIGKILL, selected, 0);  //发送kill signal 去kill selected的process
    set_tsk_thread_flag(selected, TIF_MEMDIE);
    rem -= selected_tasksize;
}
}

以上就是正常的依次 触发lowmemorykill 回收策略流程,驱动比较灵活,还提供了策略阀门值动态修改的机制,通过file ops 让application层去写入修改。
上面有贴出文件节点代码 分别对应为 adj minfree
实际路径为:

同样application 那边设置下来的话 也是需要通过 lowmem_oom_adj_to_oom_score_adj 去转换之后 赋值到 策略数组 lowmem_adj 中,需要注意的是 driver中定义的数组size 为 6 。

application层的接口还是放在上面说到过的 ProcessList.java 中,贴出相关的定义和函数吧,具体流程还是得看代码:

// These are the various interesting memory levels that we will give to
// the OOM killer.  Note that the OOM killer only supports 6 slots, so we
// can't give it a different value for every possible kind of process.
private final int[] mOomAdj = new int[] {
        FOREGROUND_APP_ADJ, VISIBLE_APP_ADJ, PERCEPTIBLE_APP_ADJ,
        BACKUP_APP_ADJ, CACHED_APP_MIN_ADJ, CACHED_APP_MAX_ADJ
};
// These are the low-end OOM level limits.  This is appropriate for an
// HVGA or smaller phone with less than 512MB.  Values are in KB.
private final int[] mOomMinFreeLow = new int[] {
        12288, 18432, 24576, /*8192, 12288, 16384,*/
        36864, 43008+20000, 49152+20000 /*36864, 43008, 49152*/
};
// These are the high-end OOM level limits.  This is appropriate for a
// 1280x800 or larger screen with around 1GB RAM.  Values are in KB.
private final int[] mOomMinFreeHigh = new int[] {
        73728, 92160, 110592, /*32768, 61440, 73728,*/
        129024, 147456+20000, 184320+20000 /*129024, 147456, 184320*/
};

// The actual OOM killer memory levels we are using.
private final int[] mOomMinFree = new int[mOomAdj.length];

上面为定义的 策略adj 以及minfree 相关的数组资源

 private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
      ...
      if (write) {
        ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
        buf.putInt(LMK_TARGET);
        for (int i=0; i<mOomAdj.length; i++) {
            buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
            buf.putInt(mOomAdj[i]);
        }
        writeLmkd(buf);
        SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
    }// 同上面提到的setoomadj 一样,最终的设置由lmkd service 去实现
}

lmkd service 中对应的command LMK_TARGET 函数为:

static void cmd_target(int ntargets, int *params) {
    ...
    for (i = 0; i < lowmem_targets_size; i++) {
        char val[40];

        if (i) {
            strlcat(minfreestr, ",", sizeof(minfreestr));
            strlcat(killpriostr, ",", sizeof(killpriostr));
        }

        snprintf(val, sizeof(val), "%d", lowmem_minfree[i]);
        strlcat(minfreestr, val, sizeof(minfreestr));
        snprintf(val, sizeof(val), "%d", lowmem_adj[i]);
        strlcat(killpriostr, val, sizeof(killpriostr));
    }

    writefilestring(INKERNEL_MINFREE_PATH, minfreestr);
    writefilestring(INKERNEL_ADJ_PATH, killpriostr);
    // 获取了上面processlist 里面传下来的两个 策略数组  然后write到文件节点中
    //#define INKERNEL_MINFREE_PATH "/sys/module/lowmemorykiller/parameters/minfree"
    //#define INKERNEL_ADJ_PATH "/sys/module/lowmemorykiller/parameters/adj"
    //驱动中对应的 文件节点
}

然后这里分享每个平台对应设置的阀值及存储位置:

828-cvte:
在device/mstar/pitaya/device-common.mk
PRODUCT_PROPERTY_OVERRIDES += \
ro.mstar.lmkd.minfree="15600,18000,24000,28000,40000,60000" \
ro.mstar.lmkd.adj="0,1,2,3,9,15"

##修改某个应用的oom_adj的值
成员变量声明的地方加入:

static Map<String,Integer> appChangeOOMAdj = new HashMap<String,Integer>();
static{
    appChangeOOMAdj.put("com.juli.dangbeimarket" , 0); 
    appChangeOOMAdj.put("com.pptv.usercenter" ,-11);
    appChangeOOMAdj.put("com.juli.dangbeimarket" , 2); 
}

然后在applyOomAdjLocked函数中搜索ProcessList.setOomAdj ,然后将这个if实现改成如下:

    if (app.curAdj != app.setAdj) {
        if(appChangeOOMAdj.containsKey(app.processName) && app.curAdj > appChangeOOMAdj.get(app.processName)){
            ProcessList.setOomAdj(app.pid, app.info.uid, appChangeOOMAdj.get(app.processName));
            app.setAdj = appChangeOOMAdj.get(app.processName);
            Slog.v(TAG, app.setAdj + "haofan");
        }else{
            ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
            if (DEBUG_SWITCH || DEBUG_OOM_ADJ) Slog.v(
                    TAG, "Set " + app.pid + " " + app.processName +
                            " adj " + app.curAdj + ": " + app.adjType);
            app.setAdj = app.curAdj;
        }
    }

##OOM相关的知识:
Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。这个属性值是定义在/system/build.prop文件中的

dalvik.vm.heapstartsize=8m
它表示堆分配的初始大小,它会影响到整个系统对RAM的使用程度,和第一次使用应用时的流畅程度。
它值越小,系统ram消耗越慢,但一些较大应用一开始不够用,需要调用gc和堆调整策略,导致应用反应较慢。它值越大,这个值越大系统ram消耗越快,但是应用更流畅。

dalvik.vm.heapgrowthlimit=64m // 单个应用可用最大内存
主要对应的是这个值,它表示单个进程内存被限定在64m,即程序运行过程中实际只能使用64m内存,超出就会报OOM。(仅仅针对dalvik堆,不包括native堆)

dalvik.vm.heapsize=384m//heapsize参数表示单个进程可用的最大内存,但如果存在heapgrowthlimit参数,则以heapgrowthlimit为准.heapsize表示不受控情况下的极限堆,表示单个虚拟机或单个进程可用的最大内存。而android上的应用是带有独立虚拟机的,也就是每开一个应用就会打开一个独立的虚拟机(这样设计就会在单个程序崩溃的情况下不会导致整个系统的崩溃)。

注意:在设置了heapgrowthlimit的情况下,单个进程可用最大内存为heapgrowthlimit值。在android开发中,如果要使用大堆,需要在manifest中指定android:largeHeap为true,这样dvm heap最大可达heapsize。

不同设备,这些个值可以不一样。一般地,厂家针对设备的配置情况都会适当的修改/system/build.prop文件来调高这个值。随着设备硬件性能的不断提升,从最早的16M限制(G1手机)到后来的24m,32m,64m等,都遵循Android框架对每个应用的最小内存大小限制,参考http://source.android.com/compatibility/downloads.html 3.7节。

通过代码查看每个进程可用的最大内存,即heapgrowthlimit值:
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memClass = activityManager.getMemoryClass();//64,以m为单位

转载于:https://blog.51cto.com/6433051/2402500

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值