Android依托Java型虛擬機,OOM是經(jīng)常遇到的問題,那么在快達到OOM的時候,系統(tǒng)難道不能回收部分界面來達到縮減開支的目的碼?在系統(tǒng)內(nèi)存不足的情況下,可以通過AMS及LowMemoryKiller殺優(yōu)先級低的進程,來回收進程資源。但是這點對于前臺OOM問題并沒有多大幫助,因為每個Android應(yīng)用有一個Java內(nèi)存上限,比如256或者512M,而系統(tǒng)內(nèi)存可能有6G或者8G,也就是說,一個APP的進程達到OOM的時候,可能系統(tǒng)內(nèi)存還是很充足的,這個時候,系統(tǒng)如何避免OOM的呢?ios是會將不可見界面都回收,之后再恢復(fù),Android做的并沒有那么徹底,簡單說:對于單棧(TaskRecord)應(yīng)用,在前臺的時候,所有界面都不會被回收,只有多棧情況下,系統(tǒng)才會回收不可見棧的Activity。注意回收的目標(biāo)是不可見棧(TaskRecord)的Activity。

如上圖,在前臺時,左邊單棧APP跟進程生命周期綁定,多棧的,不可見棧TaskRecord1是有被干掉風(fēng)險,TaskRecord2不會。下面簡單分析下。
Android原生提供內(nèi)存回收入口
Google應(yīng)該也是想到了這種情況,源碼自身就給APP自身回收內(nèi)存留有入口,在每個進程啟動的時候,回同步啟動個微小的內(nèi)存監(jiān)測工具,入口是ActivityThread的attach函數(shù),Android應(yīng)用進程啟動后,都會調(diào)用該函數(shù):
ActivityThread
private void attach(boolean system) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
...
final IActivityManager mgr = ActivityManagerNative.getDefault();
...
// Watch for getting close to heap limit.
<!--關(guān)鍵點1,添加監(jiān)測工具-->
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
<!--關(guān)鍵點2 :如果已經(jīng)可用的內(nèi)存不足1/4著手處理殺死Activity,并且這個時候,沒有緩存進程-->
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
mgr.releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
...
}
先關(guān)鍵點1,對于非系統(tǒng)進程,通過BinderInternal.addGcWatcher添加了一個內(nèi)存監(jiān)測工具,后面會發(fā)現(xiàn),這個工具的檢測時機是每個GC節(jié)點。而對于我們上文說的回收不可見Task的時機是在關(guān)鍵點2:Java使用內(nèi)存超過3/4的時候,調(diào)用AMS的releaseSomeActivities,嘗試釋放不可見Activity,當(dāng)然,并非所有不可見的Activity會被回收,當(dāng)APP內(nèi)存超過3/4的時候,調(diào)用棧如下:

APP在GC節(jié)點的內(nèi)存監(jiān)測機制
之前說過,通過BinderInternal.addGcWatcher就添加了一個內(nèi)存監(jiān)測工具,原理是什么?其實很簡單,就是利用了Java的finalize那一套:JVM垃圾回收器準(zhǔn)備釋放內(nèi)存前,會先調(diào)用該對象finalize(如果有的話)。
public class BinderInternal {
<!--關(guān)鍵點1 弱引用-->
static WeakReference<GcWatcher> sGcWatcher
= new WeakReference<GcWatcher>(new GcWatcher());
static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
static Runnable[] sTmpWatchers = new Runnable[1];
static long sLastGcTime;
static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
sLastGcTime = SystemClock.uptimeMillis();
synchronized (sGcWatchers) {
sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
}
<!--關(guān)鍵點2 執(zhí)行之前添加的回調(diào)-->
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
<!--關(guān)鍵點3 下一次輪回-->
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}
public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}
這里有幾個關(guān)鍵點,關(guān)鍵點1是弱引用,GC的sGcWatcher引用的對象是要被回收的,這樣回收前就會走關(guān)鍵點2,遍歷執(zhí)行之前通過BinderInternal.addGcWatcher添加的回調(diào),執(zhí)行完畢后,重新為sGcWatcher賦值新的弱引用,這樣就會走下一個輪回,這就是為什么GC的時候,有機會觸發(fā)releaseSomeActivities,其實,這里是個不錯的內(nèi)存監(jiān)測點,用來擴展自身的需求。
AMS的TaskRecord棧釋放機制
如果GC的時候,APP的Java內(nèi)存使用超過了3/4,就會觸發(fā)AMS的releaseSomeActivities,嘗試回收界面,增加可用內(nèi)存,但是并非所有場景都會真的銷毀Activity,比如單棧的APP就不會銷毀,多棧的也要分場景,可能選擇性銷毀不可見Activity。
ActivityManagerService
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized(this) {
final long origId = Binder.clearCallingIdentity();
try {
ProcessRecord app = getRecordForAppLocked(appInt);
mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord firstTask = null;
ArraySet<TaskRecord> tasks = null;
for (int i = 0; i < app.activities.size(); i++) {
ActivityRecord r = app.activities.get(i);
<!--如果已經(jīng)有一個進行,則不再繼續(xù)-->
if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
return;
}
<!--過濾-->
if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
|| r.state == PAUSED || r.state == STOPPING) {
continue;
}
if (r.task != null) {
if (firstTask == null) {
firstTask = r.task;
<!--關(guān)鍵點1 只要要多余一個TaskRecord才有機會走這一步,-->
} else if (firstTask != r.task) {
if (tasks == null) {
tasks = new ArraySet<>();
tasks.add(firstTask);
}
tasks.add(r.task);
}
}
}
<!--注釋很明顯,-->
if (tasks == null) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
return;
}
// If we have activities in multiple tasks that are in a position to be destroyed,
// let's iterate through the tasks and release the oldest one.
final int numDisplays = mActivityDisplays.size();
for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
// Step through all stacks starting from behind, to hit the oldest things first.
for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
final ActivityStack stack = stacks.get(stackNdx);
// Try to release activities in this stack; if we manage to, we are done.
if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
return;
}
}
}
}
這里先看第一個關(guān)鍵點1:如果想要tasks非空,則至少需要兩個TaskRecord才行,不然,只有一個firstTask,永遠(yuǎn)無法滿足firstTask != r.task這個條件,也無法走
tasks = new ArraySet<>();
也就是說,APP當(dāng)前進程中,至少兩個TaskRecord才有必要走Activity的銷毀邏輯,注釋說明很清楚:Didn't find two or more tasks to release,如果能找到超過兩個會怎么樣呢?
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks,
String reason) {
<!--maxTasks 保證最多清理- tasks.size() / 4有效個,最少清理一個 同時最少保留一個前臺TaskRecord->
int maxTasks = tasks.size() / 4;
if (maxTasks < 1) {
<!--至少清理一個-->
maxTasks = 1;
}
int numReleased = 0;
for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
final TaskRecord task = mTaskHistory.get(taskNdx);
if (!tasks.contains(task)) {
continue;
}
int curNum = 0;
final ArrayList<ActivityRecord> activities = task.mActivities;
for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
final ActivityRecord activity = activities.get(actNdx);
if (activity.app == app && activity.isDestroyable()) {
destroyActivityLocked(activity, true, reason);
if (activities.get(actNdx) != activity) {
actNdx--;
}
curNum++;
}
}
if (curNum > 0) {
numReleased += curNum;
maxTasks--;
if (mTaskHistory.get(taskNdx) != task) {
// The entire task got removed, back up so we don't miss the next one.
taskNdx--;
}
}
}
return numReleased;
}
ActivityStack利用maxTasks 保證,最多清理tasks.size() / 4,最少清理1個TaskRecord,同時,至少要保證保留一個前臺可見TaskRecord,比如如果有兩個TaskRecord,則清理先前的一個,保留前臺顯示的這個,如果三個,則還要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有則只清理一個,保留兩個,如果沒有,則繼續(xù)清理次老的,保留一個前臺展示的,如果有四個,類似,如果有5個,則至少兩個清理,這里的規(guī)則如果有興趣,可自己簡單看下。一般APP中,很少有超過兩個TaskRecord的。
demo驗證
模擬了兩個Task的模型,先啟動在一個棧里面啟動多個Activity,然后在通過startActivity啟動一個新TaskRecord,并且在新棧中不斷分配java內(nèi)存,當(dāng)Java內(nèi)存使用超過3/4的時候,就會看到前一個TaskRecord棧內(nèi)Activity被銷毀的Log,同時如果通過studio的layoutinspect查看,會發(fā)現(xiàn)APP只保留了新棧內(nèi)的Activity,驗證了之前的分析。

總結(jié)
- 單棧的進程,Activity跟進程聲明周期一致
- 多棧的,只有不可見棧的Activity可能被銷毀(Java內(nèi)存超過3/4,不可見)
- 該回收機制利用了Java虛擬機的gc機finalize
- 至少兩個TaskRecord占才有效,所以該機制并不激進,因為主流APP都是單棧。
作者:看書的小蝸牛
Android可見APP的不可見任務(wù)棧(TaskRecord)被銷毀分析
僅供參考,歡迎指正