Android依託Java型虛擬機器,OOM是經常遇到的問題,那麼在快達到OOM的時候,系統難道不能回收部分介面來達到縮減開支的目的碼?在系統記憶體不足的情況下,可以通過AMS及LowMemoryKiller殺優先順序低的程式,來回收程式資源。但是這點對於前臺OOM問題並沒有多大幫助,因為每個Android應用有一個Java記憶體上限,比如256或者512M,而系統記憶體可能有6G或者8G,也就是說,一個APP的程式達到OOM的時候,可能系統記憶體還是很充足的,這個時候,系統如何避免OOM的呢?ios是會將不可見介面都回收,之後再恢復,Android做的並沒有那麼徹底,簡單說:對於單棧(TaskRecord)應用,在前臺的時候,所有介面都不會被回收,只有多棧情況下,系統才會回收不可見棧的Activity。注意回收的目標是不可見**棧(TaskRecord)**的Activity。
如上圖,在前臺時,左邊單棧APP跟程式生命週期繫結,多棧的,不可見棧TaskRecord1是有被幹掉風險,TaskRecord2不會。下面簡單分析下。
Android原生提供記憶體回收入口
Google應該也是想到了這種情況,原始碼自身就給APP自身回收記憶體留有入口,在每個程式啟動的時候,回同步啟動個微小的記憶體監測工具,入口是ActivityThread的attach函式,Android應用程式啟動後,都會呼叫該函式:
ActivityThread
private void attach(boolean system) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
...
final IActivityManager mgr = ActivityManagerNative.getDefault();
...
// Watch for getting close to heap limit.
<!--關鍵點1,新增監測工具-->
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();
<!--關鍵點2 :如果已經可用的記憶體不足1/4著手處理殺死Activity,並且這個時候,沒有快取程式-->
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
mgr.releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
...
}
複製程式碼
先關鍵點1,對於非系統程式,通過BinderInternal.addGcWatcher新增了一個記憶體監測工具,後面會發現,這個工具的檢測時機是每個GC節點。而對於我們上文說的回收不可見Task的時機是在關鍵點2:Java使用記憶體超過3/4的時候,呼叫AMS的releaseSomeActivities,嘗試釋放不可見Activity,當然,並非所有不可見的Activity會被回收,當APP記憶體超過3/4的時候,呼叫棧如下:
APP在GC節點的記憶體監測機制
之前說過,通過BinderInternal.addGcWatcher就新增了一個記憶體監測工具,原理是什麼?其實很簡單,就是利用了Java的finalize那一套:JVM垃圾回收器準備釋放記憶體前,會先呼叫該物件finalize(如果有的話)。
public class BinderInternal {
<!--關鍵點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);
}
<!--關鍵點2 執行之前新增的回撥-->
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
<!--關鍵點3 下一次輪迴-->
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}
public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}
複製程式碼
這裡有幾個關鍵點,關鍵點1是弱引用,GC的sGcWatcher引用的物件是要被回收的,這樣回收前就會走關鍵點2,遍歷執行之前通過BinderInternal.addGcWatcher新增的回撥,執行完畢後,重新為sGcWatcher賦值新的弱引用,這樣就會走下一個輪迴,這就是為什麼GC的時候,有機會觸發releaseSomeActivities,其實,這裡是個不錯的記憶體監測點,用來擴充套件自身的需求。
AMS的TaskRecord棧釋放機制
如果GC的時候,APP的Java記憶體使用超過了3/4,就會觸發AMS的releaseSomeActivities,嘗試回收介面,增加可用記憶體,但是並非所有場景都會真的銷燬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);
<!--如果已經有一個進行,則不再繼續-->
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;
<!--關鍵點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;
}
}
}
}
複製程式碼
這裡先看第一個關鍵點1:如果想要tasks非空,則至少需要兩個TaskRecord才行,不然,只有一個firstTask,永遠無法滿足firstTask != r.task這個條件,也無法走
tasks = new ArraySet<>();
複製程式碼
也就是說,APP當前程式中,至少兩個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被清理,如果有則只清理一個,保留兩個,如果沒有,則繼續清理次老的,保留一個前臺展示的,如果有四個,類似,如果有5個,則至少兩個清理,這裡的規則如果有興趣,可自己簡單看下。一般APP中,很少有超過兩個TaskRecord的。
demo驗證
模擬了兩個Task的模型,先啟動在一個棧裡面啟動多個Activity,然後在通過startActivity啟動一個新TaskRecord,並且在新棧中不斷分配java記憶體,當Java記憶體使用超過3/4的時候,就會看到前一個TaskRecord棧內Activity被銷燬的Log,同時如果通過studio的layoutinspect檢視,會發現APP只保留了新棧內的Activity,驗證了之前的分析。
總結
- 單棧的程式,Activity跟程式宣告週期一致
- 多棧的,只有不可見棧的Activity可能被銷燬(Java記憶體超過3/4,不可見)
- 該回收機制利用了Java虛擬機器的gc機finalize
- 至少兩個TaskRecord佔才有效,所以該機制並不激進,因為主流APP都是單棧。
作者:看書的小蝸牛
Android可見APP的不可見任務棧(TaskRecord)被銷燬分析
僅供參考,歡迎指正