Android可见APP的不可见任务栈(TaskRecord)销毁分析
如上图,在前台时,左边单栈APP跟进程生命周期绑定,多栈的,不可见栈TaskRecord1是有被干掉风险,TaskRecord2不会。下面简单分析下。
Android原生提供内存回收入口
Google应该也是想到了这种情况,源码自身就给APP自身回收内存留有入口,在每个进程启动的时候,回同步启动个微小的内存监测工具,入口是ActivityThread的attach函数,Android应用进程启动后,都会调用该函数:
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) { ... }
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。
@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的。