Detailed explanation of the principle of app freezer in Android (2): S version

Based on version: Android S

0. Preface

In the previous two blog posts, "App Memory Recycling Optimization in Android (1)" and "App Memory Recycling Optimization in Android (2)", the process of app memory optimization in Android was analyzed in detail. The management of this mechanism is managed through the CachedAppOptimizer class. Why is it called this name instead of AppCompact? It was also mentioned in the previous two blog posts, because this class also manages an important function: freezer , an optimization for application processes that are in the Cached state for a long time.

In the previous blog post "App Freezer Principle R Version" , the process of app freeze / unfreeze was briefly analyzed. However, from the code logic point of view, the author feels that there are some logic problems in the R version. Fortunately, they have been repaired in the S version.

This article will conduct a comparative analysis of the S version based on the principles of the R version.

1. freezer trigger

Same as the R version , the S version is also triggered by calling updateAppFreezeStateLSP() in  the applyOomAdjLSP() function to confirm whether to freeze the process:

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java

   private void updateAppFreezeStateLSP(ProcessRecord app) {

        // 确认该功能是否使能,如果没有使能则返回
        if (!mCachedAppOptimizer.useFreezer()) {
            return;
        }

        // S 版本新加的,确认应用是否可以豁免
        if (app.mOptRecord.isFreezeExempt()) {
            return;
        }

        // S 版本中cached进程优化相关的属性,都放到了mOptRecord中管理
        final ProcessCachedOptimizerRecord opt = app.mOptRecord;

        // 如果进程处于frozen状态,但shouldNotFreeze变成true,需要解冻
        if (opt.isFrozen() && opt.shouldNotFreeze()) {
            mCachedAppOptimizer.unfreezeAppLSP(app);
            return;
        }

        // 确定adj,是进入freeze 还是 unfreeze 流程
        final ProcessStateRecord state = app.mState;
        if (state.getCurAdj() >= ProcessList.CACHED_APP_MIN_ADJ && !opt.isFrozen()
                && !opt.shouldNotFreeze()) {
            mCachedAppOptimizer.freezeAppAsyncLSP(app);
        } else if (state.getSetAdj() < ProcessList.CACHED_APP_MIN_ADJ) {
            mCachedAppOptimizer.unfreezeAppLSP(app);
        }
    }

The basic logic of this function is similar to the R version, with some slight differences:

  • The process has the function of freeze exemption. When the application sets the INSTALL_PACKAGES permission, the application is in the exempt state. See the ProcessList.startProcessLocked ( ) function for details;
  • Some states of the cached process in the S version are uniformly maintained by mOptRecord in ProcessRecord;
  • Finally, when unfreeze is called, it will no longer be judged whether the app is frozen, because this part of the logical judgment will be judged in the unfreeze function, which makes the code more concise and is somewhat redundant in the R version;

2. CachedAppOptimizer.init()

For the construction call of CachedAppOptimizer and the triggering process of the init() function, you can refer to Sections  1  and  2   of the article "App Memory Recycling Optimization in Android (2)" .

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java
 
    public void init() {
        ...
        DeviceConfig.addOnPropertiesChangedListener(
                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                ActivityThread.currentApplication().getMainExecutor(),
                mOnNativeBootFlagsChangedListener);
        mAm.mContext.getContentResolver().registerContentObserver(
                CACHED_APP_FREEZER_ENABLED_URI, false, mSettingsObserver);
        synchronized (mPhenotypeFlagLock) {
            ...
            updateUseFreezer();
            ...
        }
    }

Compared with the R version , there are two more functions:

  • Add a new  listener whose freeze_debounce_timeout attribute changes. When the attribute changes, updateFreezerDebounceTimeout() will be called to update;
  • A new observer whose cached_apps_freezer attribute value changes has been added  ;

In the R version , the freeze timeout is 10 minutes and a constant is used. In the S version, this value is designed to be variable, and users can modify it through DeviceConfig. When changes occur,  updateFreezerDebounceTimeout() will be called to update.

In addition, in the S version, an observer is made for the value of cached_apps_freezer to control the enabling of the freezer in a timely manner.

2.1 updateUseFreezer()

    private void updateUseFreezer() {
        // 获取 settings中属性 cached_apps_freezer的值,同R 版本
        final String configOverride = Settings.Global.getString(mAm.mContext.getContentResolver(),
                Settings.Global.CACHED_APPS_FREEZER_ENABLED);

        if ("disabled".equals(configOverride)) {
            mUseFreezer = false;
        } else if ("enabled".equals(configOverride)
                || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                    KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) {
            mUseFreezer = isFreezerSupported();
            updateFreezerDebounceTimeout();
        } else {
            mUseFreezer = false;
        }

        final boolean useFreezer = mUseFreezer;
        // enableFreezer() would need the global ActivityManagerService lock, post it.
        mAm.mHandler.post(() -> {
            if (useFreezer) {
                Slog.d(TAG_AM, "Freezer enabled");
                enableFreezer(true);

                if (!mCachedAppOptimizerThread.isAlive()) {
                    mCachedAppOptimizerThread.start();
                }

                if (mFreezeHandler == null) {
                    mFreezeHandler = new FreezeHandler();
                }

                Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
                    mCompactionPriority);

            } else {
                Slog.d(TAG_AM, "Freezer disabled");
                enableFreezer(false);
            }
        });
    }

An optimization has also been made for the R version, and  the processing of enableFreezer() is processed asynchronously, because a big adjustment has been made in the S version. See section 4 below for details .

Whether the freezer function is enabled or not can be explained more clearly with a flow chart:

It should be noted that when obtaining the freezer node, it is different from the R version :

  • The R version is to directly specify the node /sys/fs/cgroup/freezer/cgroup.freeze;
  • The S version uses the function getFreezerCheckPath() to find the node corresponding to the pid in libprocessgroup;

3. Introduction to cgroups

I won’t add anything here. For details, you can check the R version or "Detailed Explanation of the cgroup Abstraction Layer in Android"

It should be noted here that in the R version of the freezer, /sys/fs/cgroup/freezer/cgroup.freeze is used to determine whether to enable the freezer, and /sys/fs/cgroup/freezer/cgroup.procs is used to perform frozen / unfrozen operation.

In the S version, frozen/unfrozen operations pass  the /sys/fs/cgroup/uid_xxx/pid_xxx/cgroup.freeze  node.

cgroups.json is different in S version :

  "Cgroups2": {
    "Path": "/sys/fs/cgroup",
    "Mode": "0755",
    "UID": "system",
    "GID": "system",
    "Controllers": [
      {
        "Controller": "freezer",
        "Path": ".",
        "Mode": "0755",
        "UID": "system",
        "GID": "system"
      }
    ]
  }

The freezer directory no longer appears, but the uid_xxx/pid_xxx directory is created directly under the /sys/fs/cgroup directory .

See section 5.2.1 below for details .

4. enableFreezer()

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

    public synchronized boolean enableFreezer(boolean enable) {
        if (!mUseFreezer) {
            return false;
        }

        if (enable) {
            mFreezerDisableCount--;

            if (mFreezerDisableCount > 0) {
                return true;
            } else if (mFreezerDisableCount < 0) {
                Slog.e(TAG_AM, "unbalanced call to enableFreezer, ignoring");
                mFreezerDisableCount = 0;
                return false;
            }
        } else {
            mFreezerDisableCount++;

            if (mFreezerDisableCount > 1) {
                return true;
            }
        }

        // Override is applied immediately, restore is delayed
        synchronized (mAm) {
            synchronized (mProcLock) {
                mFreezerOverride = !enable;
                Slog.d(TAG_AM, "freezer override set to " + mFreezerOverride);

                mAm.mProcessList.forEachLruProcessesLOSP(true, process -> {
                    if (process == null) {
                        return;
                    }

                    final ProcessCachedOptimizerRecord opt = process.mOptRecord;
                    if (enable && opt.hasFreezerOverride()) {
                        freezeAppAsyncLSP(process);
                        opt.setFreezerOverride(false);
                    }

                    if (!enable && opt.isFrozen()) {
                        unfreezeAppLSP(process);

                        // Set freezerOverride *after* calling unfreezeAppLSP (it resets the flag)
                        opt.setFreezerOverride(true);
                    }
                });
            }
        }

        return true;
    }

The code is different from the R version  . The process in each LRU is confirmed here through  mAm.mProcessList.forEachLruProcessesLOSP() .

The mOptRecord.mFreezerOverride attribute is introduced in the process to mark whether to disable the freezer operation for a process. If this value is true, it means that the process has been disabled freezer. If you enable it again at this time, you need to make a freeze request to the process.

Of course, the CachedAppOptimizer class also has such a member variable mFreezerOverride , which is used to control freezeAppAsyncLSP() called from the outside. If the value is true, it means that the freezer is disabled. If freezeAppAsyncLSP() is called externally, there is no need to process it.

5. freezeAppAsyncLSP()

    void freezeAppAsyncLSP(ProcessRecord app) {
        final ProcessCachedOptimizerRecord opt = app.mOptRecord;
        if (opt.isPendingFreeze()) {
            // Skip redundant DO_FREEZE message
            return;
        }

        mFreezeHandler.sendMessageDelayed(
                mFreezeHandler.obtainMessage(
                    SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app),
                mFreezerDebounceTimeout);
        opt.setPendingFreeze(true);
    }

Compared with the R version , a protection is implemented here to prevent repeated freeze requests.

In addition, timeout is changed from the constant FREEZE_TIMEOUT_MS in the R version to a variable debounce timeout.

Note: The asynchronous operation during freezing is performed using ServiceThread defined in the CachedAppOptimizer class, while thawing is an eastern operation and does not pass through ServiceThread.

5.1 freeze message processing

        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SET_FROZEN_PROCESS_MSG:
                    synchronized (mAm) {
                        freezeProcess((ProcessRecord) msg.obj);
                    }
                    break;
                case REPORT_UNFREEZE_MSG:
                    int pid = msg.arg1;
                    int frozenDuration = msg.arg2;
                    String processName = (String) msg.obj;

                    reportUnfreeze(pid, frozenDuration, processName);
                    break;
                default:
                    return;
            }
        }

There are two messages for the freezer. The REPORT_UNFREEZE_MSG message is recorded after the unfreeze.

 This article focuses on the message processing of freeze. Here we see that the freezeProcess()  function is finally called . See the next section for details.

5.2 freezeProcess()

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

       private void freezeProcess(final ProcessRecord proc) {
            int pid = proc.getPid(); // Unlocked intentionally
            final String name = proc.processName;
            final long unfrozenDuration;
            final boolean frozen;
            final ProcessCachedOptimizerRecord opt = proc.mOptRecord;

            opt.setPendingFreeze(false);

            try {
                // 确认进程是否存在任意的文件锁,避免不必要的 free/unfreeze操作
                //   这是为了防止冻结进程持有文件锁,从而引起死锁
                //   冻结成功之后,还会再次确认文件锁,如果有锁,则立即解冻
                if (mProcLocksReader.hasFileLocks(pid)) {
                    if (DEBUG_FREEZER) {
                        Slog.d(TAG_AM, name + " (" + pid + ") holds file locks, not freezing");
                    }
                    return;
                }
            } catch (Exception e) {
                Slog.e(TAG_AM, "Not freezing. Unable to check file locks for " + name + "(" + pid
                        + "): " + e);
                return;
            }

            synchronized (mProcLock) {
                pid = proc.getPid();

                // 如果进程没有变成Cached或者不能freeze,则退出此次freeze操作
                if (proc.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ
                        || opt.shouldNotFreeze()) {
                    return;
                }

                // 如果freezer 处于disable状态,返回,并告知该进程
                if (mFreezerOverride) {
                    opt.setFreezerOverride(true);
                    return;
                }


                // 已经处于frozen,或者不是一个应用进程,则退出此次 freeze操作
                //   pid为0,有可能进程还没有launch完成,或者进程被kill了
                if (pid == 0 || opt.isFrozen()) {
                    // Already frozen or not a real process, either one being
                    // launched or one being killed
                    return;
                }

                Slog.d(TAG_AM, "freezing " + pid + " " + name);

                // 在S版本中,将这部分功能提前,也是正确的行为
                // 冻结 binder
                //    1.如果freezer是使能,将同步发送所有的pending 交互给指定的pid;
                //    2.该函数调用后,所有的binder 请求,都会被block,并返回error给发送请求的进程;
                try {
                    if (freezeBinder(pid, true) != 0) {
                        rescheduleFreeze(proc, "outstanding txns");
                        return;
                    }
                } catch (RuntimeException e) { //如果调用失败,则直接kill该进程
                    Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
                    mFreezeHandler.post(() -> {
                        synchronized (mAm) {
                            proc.killLocked("Unable to freeze binder interface",
                                    ApplicationExitInfo.REASON_OTHER,
                                    ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
                        }
                    });
                }

                long unfreezeTime = opt.getFreezeUnfreezeTime();

                //核心函数 setProcessFrozen(),同步冻结进程
                try {
                    Process.setProcessFrozen(pid, proc.uid, true);

                    opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
                    opt.setFrozen(true);
                } catch (Exception e) {
                    Slog.w(TAG_AM, "Unable to freeze " + pid + " " + name);
                }

                unfrozenDuration = opt.getFreezeUnfreezeTime() - unfreezeTime;
                frozen = opt.isFrozen();
            }

            if (!frozen) {
                return;
            }

            Slog.d(TAG_AM, "froze " + pid + " " + name);

            // 将此次 freeze 记录到 event log 中
            EventLog.writeEvent(EventLogTags.AM_FREEZE, pid, name);

            // See above for why we're not taking mPhenotypeFlagLock here
            if (mRandom.nextFloat() < mFreezerStatsdSampleRate) {
                FrameworkStatsLog.write(FrameworkStatsLog.APP_FREEZE_CHANGED,
                        FrameworkStatsLog.APP_FREEZE_CHANGED__ACTION__FREEZE_APP,
                        pid,
                        name,
                        unfrozenDuration);
            }

            // 确认在冻结的时候,是否收到了 TXNS_PENDING_WHILE_FROZEN的binder请求,
            //    如果有有收到请求,则重新freeze 该进程(unfreeze + freeze)
            try {
                // post-check to prevent races
                int freezeInfo = getBinderFreezeInfo(pid);

                if ((freezeInfo & TXNS_PENDING_WHILE_FROZEN) != 0) {
                    synchronized (mProcLock) {
                        rescheduleFreeze(proc, "new pending txns");
                    }
                    return;
                }
            } catch (RuntimeException e) {
                Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
                mFreezeHandler.post(() -> {
                    synchronized (mAm) {
                        proc.killLocked("Unable to freeze binder interface",
                                ApplicationExitInfo.REASON_OTHER,
                                ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
                    }
                });
            }

            try {
                // 再次check文件锁,如果该冻结进程持有文件锁,立即unfreeze
                if (mProcLocksReader.hasFileLocks(pid)) {
                    if (DEBUG_FREEZER) {
                        Slog.d(TAG_AM, name + " (" + pid + ") holds file locks, reverting freeze");
                    }
                    unfreezeAppLSP(proc);
                }
            } catch (Exception e) {
                Slog.e(TAG_AM, "Unable to check file locks for " + name + "(" + pid + "): " + e);
                unfreezeAppLSP(proc);
            }
        }

The logic is quite different from the R version :

  • Before freezing the process, call freezeBinder() to freeze binder communication;
  • After freezing the process, call getBinderFreezeInfo() to confirm whether there is a binder request for TXNS_PENDING_WHILE_FROZEN when freezing  . If there is such a request, refreeze the process (unfreeze + freeze);

Make a flow chart to understand the freezing process:

 

 5.2.1 setProcessFrozen()

frameworks/base/core/java/android/os/Process.java
 
    public static final native void setProcessFrozen(int pid, int uid, boolean frozen);
frameworks/base/core/jni/android_util_Process.cpp

void android_os_Process_setProcessFrozen(
        JNIEnv *env, jobject clazz, jint pid, jint uid, jboolean freeze)
{
    bool success = true;

    if (freeze) {
        success = SetProcessProfiles(uid, pid, {"Frozen"});
    } else {
        success = SetProcessProfiles(uid, pid, {"Unfrozen"});
    }

    if (!success) {
        signalExceptionForGroupError(env, EINVAL, pid);
    }
}

The call here has been analyzed in the  blog post "cgroup Abstraction Layer" . Through the interface SetProcessProfiles(), it is specifically a profile of SetAttributeAction  type, and finally calls ExecuteForProcess():

system/core/libprocesscgroup/task_profiles.cpp

bool SetAttributeAction::ExecuteForProcess(uid_t, pid_t pid) const {
    return ExecuteForTask(pid);
}

bool SetAttributeAction::ExecuteForTask(int tid) const {
    std::string path;

    if (!attribute_->GetPathForTask(tid, &path)) {
        LOG(ERROR) << "Failed to find cgroup for tid " << tid;
        return false;
    }

    if (!WriteStringToFile(value_, path)) {
        PLOG(ERROR) << "Failed to write '" << value_ << "' to " << path;
        return false;
    }

    return true;
}

Attribute_->GetPathForTask() needs to be determined through code:

system/core/libprocessgroup/task_profiles.cpp

bool ProfileAttribute::GetPathForTask(int tid, std::string* path) const {
    std::string subgroup;
    if (!controller()->GetTaskGroup(tid, &subgroup)) {
        return false;
    }

    if (path == nullptr) {
        return true;
    }

    if (subgroup.empty()) {
        *path = StringPrintf("%s/%s", controller()->path(), file_name_.c_str());
    } else {
        *path = StringPrintf("%s/%s/%s", controller()->path(), subgroup.c_str(),
                             file_name_.c_str());
    }
    return true;
}

There are two parts, one is to obtain the subgroup through the controller, and the other is to splice the final path.

Let's take a look at GetTaskGroup() pointed to by congtroller :

system/core/libprocessgroup/cgroup_map.cpp

bool CgroupController::GetTaskGroup(int tid, std::string* group) const {
    std::string file_name = StringPrintf("/proc/%d/cgroup", tid);
    std::string content;
    if (!android::base::ReadFileToString(file_name, &content)) {
        PLOG(ERROR) << "Failed to read " << file_name;
        return false;
    }

    // if group is null and tid exists return early because
    // user is not interested in cgroup membership
    if (group == nullptr) {
        return true;
    }

    std::string cg_tag;

    if (version() == 2) {
        cg_tag = "0::";
    } else {
        cg_tag = StringPrintf(":%s:", name());
    }
    size_t start_pos = content.find(cg_tag);
    if (start_pos == std::string::npos) {
        return false;
    }

    start_pos += cg_tag.length() + 1;  // skip '/'
    size_t end_pos = content.find('\n', start_pos);
    if (end_pos == std::string::npos) {
        *group = content.substr(start_pos, std::string::npos);
    } else {
        *group = content.substr(start_pos, end_pos - start_pos);
    }

    return true;
}

Read the information in /proc/PID/cgroup . For cgroup2, the information read is 0::. In the S version, the concatenation of uid and pid is specified, for example:

130|shift:/sys/fs/cgroup # cat /proc/2119/cgroup
4:memory:/
3:cpuset:/restricted
2:cpu:/foreground
1:blkio:/foreground
0::/uid_10083/pid_2119

The final file written is /sys/fs/cgroup/uid_xxx/pid_xxx /cgroup.freeze .

Journey diagram of a process being frozen:

6. unfreezeAppLSP()

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

   void unfreezeAppLSP(ProcessRecord app) {
        final int pid = app.getPid();
        final ProcessCachedOptimizerRecord opt = app.mOptRecord;

        // 进程已经处于peding freeze中,移除冻结消息
        if (opt.isPendingFreeze()) {
            // Remove pending DO_FREEZE message
            mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
            opt.setPendingFreeze(false);
        }

        opt.setFreezerOverride(false);

        // 如果进程还没有冻结,则无需做解冻处理
        if (!opt.isFrozen()) {
            return;
        }

        // 冻住的进程可以接收异步binder请求,但是不会处理,只是放入binder buffer, 过多的请求会导致buffer耗尽;
        // 这里需要确认下该进程在解冻之前,进程是否在冰冻期间收到同步的binder 请求,有则kill该进程
        boolean processKilled = false;

        try {
            int freezeInfo = getBinderFreezeInfo(pid);

            if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
                Slog.d(TAG_AM, "pid " + pid + " " + app.processName
                        + " received sync transactions while frozen, killing");
                app.killLocked("Sync transaction while in frozen state",
                        ApplicationExitInfo.REASON_OTHER,
                        ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION, true);
                processKilled = true;
            }

            if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0 && DEBUG_FREEZER) {
                Slog.d(TAG_AM, "pid " + pid + " " + app.processName
                        + " received async transactions while frozen");
            }
        } catch (Exception e) {
            Slog.d(TAG_AM, "Unable to query binder frozen info for pid " + pid + " "
                    + app.processName + ". Killing it. Exception: " + e);
            app.killLocked("Unable to query binder frozen stats",
                    ApplicationExitInfo.REASON_OTHER,
                    ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
            processKilled = true;
        }

        //进程被kill 了,无需 unfreeze
        if (processKilled) {
            return;
        }

        // app.freezeUnfreezeTime记录的是上次free、unfreeze的时间
        long freezeTime = opt.getFreezeUnfreezeTime();

        try {
            freezeBinder(pid, false);
        } catch (RuntimeException e) {
            Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
                    + ". Killing it");
            app.killLocked("Unable to unfreeze",
                    ApplicationExitInfo.REASON_OTHER,
                    ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
            return;
        }

        try {
            Process.setProcessFrozen(pid, app.uid, false);

            opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
            opt.setFrozen(false);
        } catch (Exception e) {
            Slog.e(TAG_AM, "Unable to unfreeze " + pid + " " + app.processName
                    + ". This might cause inconsistency or UI hangs.");
        }

        if (!opt.isFrozen()) {
            Slog.d(TAG_AM, "sync unfroze " + pid + " " + app.processName);

            mFreezeHandler.sendMessage(
                    mFreezeHandler.obtainMessage(REPORT_UNFREEZE_MSG,
                        pid,
                        (int) Math.min(opt.getFreezeUnfreezeTime() - freezeTime, Integer.MAX_VALUE),
                        app.processName));
        }
    }

The logic is relatively clear. The core processing function is setProcessFrozen(). The detailed process has been analyzed in Section 5.2.1 above.

7. Kernel processing

kernel/cgroup/cgroup.c

static struct cftype cgroup_base_files[] = {
    ...

	{
		.name = "cgroup.freeze",
		.flags = CFTYPE_NOT_ON_ROOT,
		.seq_show = cgroup_freeze_show,
		.write = cgroup_freeze_write,
	},

    ...
};

Writing to the /sys/fs/cgroup/uid_xxx/pid_xxx/cgroup.freeze file triggers the callback function cgroup_freeze_write() function, which finds the corresponding cgroup through the current kernfs and freezes all processes and child processes under this cgroup.

The detailed cgroup_freeze_write() function will be analyzed later.

 
Related blog posts:

https://justinwei.blog.csdn.net/article/details/131845360?

Detailed explanation of cgroup abstraction layer in Android_private kitchen blog-CSDN blog

App memory recycling optimization in Android (2): S version_private kitchen blog-CSDN blog

App memory recycling optimization in Android (1): R version_cachedappoptimizer_private kitchen blog-CSDN blog

Android oom_adj detailed interpretation_private kitchen blog-CSDN blog

Android oom_adj update principle (2)_private kitchen blog-CSDN blog

Android oom_adj update principle (1)_private kitchen blog-CSDN blog

 

 

Guess you like

Origin blog.csdn.net/jingerppp/article/details/131974011