[[418387]] Preface onDestroy() is called 10 seconds after Activity.finish() is called. This may cause some uncontrollable problems, such as untimely resource release in onDestroy(), abnormal assignment status, etc. I have never encountered a similar problem before. Source code is the best way to find problems. Then start analyzing from Activity.finish() to find the answer to the problem; 1. Simulation finish situation 1. Normal situation Write a simplest scenario in which FirstActivity jumps to SecondActivity, and record each life cycle and the time interval for calling finish(). - class FirstActivity : BaseLifecycleActivity() {
- private val binding by lazy { ActivityFirstBinding.inflate(layoutInflater) }
- var startTime = 0L
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- binding.goToSecond.setOnClickListener {
- start<SecondActivity>()
- finish()
- startTime = System.currentTimeMillis()
- }
- }
- override fun onPause() {
- super.onPause()
- Log.e( "finish" , "onPause() distance to finish(): ${System.currentTimeMillis() - startTime} ms" )
- }
- override fun onStop() {
- super.onStop()
- Log.e( "finish" , "onStop() distance to finish(): ${System.currentTimeMillis() - startTime} ms" )
- }
- override fun onDestroy() {
- super.onDestroy()
- Log.e( "finish" , "onDestroy() distance from finish(): ${System.currentTimeMillis() - startTime} ms" )
- }
- }
SecondActivity is a normal blank Activity without any operation. Click the button to jump to SecondActivity, and the print log is as follows: - FirstActivity: onPause, onPause() to finish(): 5 ms
- SecondActivity: onCreate
- SecondActivity: onStart
- SecondActivity: onResume
- FirstActivity: onStop, onStop() to finish(): 660 ms
- FirstActivity: onDestroy, onDestroy() distance from finish(): 663 ms
It can be seen that under normal circumstances, after FirstActivity calls back onPause, SecondActivity starts the normal life cycle process, and only when onResume is called back and visible to the user will FirstActivity call back onPause and onDestroy. The time intervals are also within the normal range. 2. Abnormal situation finish() after 10 seconds Simulate a scenario where a lot of animations are performed when SecondActivity is started, and continuously insert messages into the main thread message queue. Modify the code of SecondActivity: - class SecondActivity : BaseLifecycleActivity() {
- private val binding by lazy { ActivitySecondBinding.inflate(layoutInflater) }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- postMessage()
- }
- private fun postMessage() {
- binding.secondBt.post {
- Thread.sleep(10)
- postMessage()
- }
- }
- }
Let's look at the log again: - FirstActivity: onPause, onPause() to finish(): 6 ms
- SecondActivity: onCreate
- SecondActivity: onStart
- SecondActivity: onResume
- FirstActivity: onStop, onStop() to finish(): 10033 ms
- FirstActivity: onDestroy, onDestroy() distance from finish(): 10037 ms
FirstActivity's onPause() is not affected because during the Activity jump process, the target Activity will not start its normal life cycle until the previous Activity onPause() is called. OnStop and onDestroy() are not called back until 10 seconds have passed. Comparing the above two scenarios, we can guess that when the main thread of SecondActivity is too busy and has no chance to stop and take a breath, FirstActivity will not be able to call back onStop and onDestroy in time. Based on the above guesses, we can find the answer from the source code. 2. Detailed explanation of finish() source code 1. Analysis from Activity.finish()- > Activity.java
- public void finish() {
- finish(DONT_FINISH_TASK_WITH_ACTIVITY);
- }
The finish() method is overloaded with a parameter. The parameter is DONT_FINISH_TASK_WITH_ACTIVITY, which has a straightforward meaning and will not destroy the task stack where the Activity is located. - > Activity.java
- private void finish( int finishTask) {
- // mParent is usually null and will be used in ActivityGroup
- if (mParent == null ) {
- ......
- try {
- // Binder calls AMS.finishActivity()
- if (ActivityManager.getService()
- .finishActivity(mToken, resultCode, resultData, finishTask)) {
- mFinished = true ;
- }
- } catch (RemoteException e) {
- }
- } else {
- mParent.finishFromChild(this);
- }
- ......
- }
In most cases, mParent is null, so there is no need to consider the else branch. Some older Android programmers may know about ActivityGroup, in which case mParent may not be null. (Since I am still young, I have never used ActivityGroup, so I will not explain it in detail.) Binder calls the AMS.finishActivity() method. - > ActivityManagerService.java
- public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
- int finishTask) {
- ......
- synchronized(this) {
- // token holds a weak reference to ActivityRecord
- ActivityRecord r = ActivityRecord.isInStackLocked(token);
- if (r == null ) {
- return true ;
- }
- ......
- try {
- boolean res;
- final boolean finishWithRootActivity =
- finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY;
- // The finishTask parameter is DONT_FINISH_TASK_WITH_ACTIVITY, enter the else branch
- if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY
- || (finishWithRootActivity && r == rootR)) {
- res = mStackSupervisor.removeTaskByIdLocked(tr.taskId, false ,
- finishWithRootActivity, "finish-activity" );
- } else {
- // Call ActivityStack.requestFinishActivityLocked()
- res = tr.getStack().requestFinishActivityLocked(token, resultCode,
- resultData, "app-request" , true );
- }
- return res;
- finally
- Binder.restoreCallingIdentity(origId);
- }
- }
- }
Note the token object in the method parameter. Token is a static inner class of ActivityRecord, which holds a weak reference to the external ActivityRecord. It inherits from IApplicationToken.Stub and is a Binder object. ActivityRecord is a detailed description of the current Activity, including all the information of the Activity. The parameter passed into the finishTask() method is DONT_FINISH_TASK_WITH_ACTIVITY, so the ActivityStack.requestFinishActivityLocked() method will be called next. - > ActivityStack.java
- final boolean requestFinishActivityLocked(IBinder token, int resultCode,
- Intent resultData, String reason, boolean oomAdj) {
- ActivityRecord r = isInStackLocked(token);
- if (r == null ) {
- return false ;
- }
- finishActivityLocked(r, resultCode, resultData, reason, oomAdj);
- return true ;
- }
- final boolean finishActivityLocked(ActivityRecord r, int resultCode, Intent resultData,
- String reason, boolean oomAdj) {
- // PAUSE_IMMEDIATELY is true , defined in ActivityStackSupervisor
- return finishActivityLocked(r, resultCode, resultData, reason, oomAdj, !PAUSE_IMMEDIATELY);
- }
The last method called is an overloaded finishActivityLocked() method. - > ActivityStack.java
- // Parameter pauseImmediately is false
- final boolean finishActivityLocked(ActivityRecord r, int resultCode, Intent resultData,
- String reason, boolean oomAdj, boolean pauseImmediately) {
- if (r.finishing) { // Repeat finish
- return false ;
- }
- mWindowManager.deferSurfaceLayout();
- try {
- // mark r.finishing = true ,
- // The repeated finish detection above depends on this value
- r.makeFinishingLocked();
- final TaskRecord task = r.getTask();
- ......
- // Pause event distribution
- r.pauseKeyDispatchingLocked();
- adjustFocusedActivityStack(r, "finishActivity" );
- // Process activity result
- finishActivityResultsLocked(r, resultCode, resultData);
- // mResumedActivity is the current Activity, which will enter this branch
- if (mResumedActivity == r) {
- ......
- // Tell window manager to prepare for this one to be removed.
- r.setVisibility( false );
- if (mPausingActivity == null ) {
- // Start pausing mResumedActivity
- startPausingLocked( false , false , null , pauseImmediately);
- }
- ......
- } else if (!r.isState(PAUSING)) {
- // Will not enter this branch
- ......
- }
- return false ;
- finally
- mWindowManager.continueSurfaceLayout();
- }
- }
After calling finish, you must pause the current Activity first, no problem. Next, look at the startPausingLocked() method. - > ActivityStack.java
- final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping,
- ActivityRecord resuming, boolean pauseImmediately) {
- ......
- ActivityRecord prev = mResumedActivity;
- if (prev == null ) {
- // Activity without onResume cannot execute pause
- if (resuming == null ) {
- mStackSupervisor.resumeFocusedStackTopActivityLocked();
- }
- return false ;
- }
- ......
- mPausingActivity = prev;
- // Set the current Activity state to PAUSING
- prev.setState(PAUSING, "startPausingLocked" );
- ......
- if (prev.app != null && prev.app.thread != null ) {
- try {
- ......
- // 1\. Distribute lifecycle events through ClientLifecycleManager
- // Eventually, an EXECUTE_TRANSACTION event will be sent to H
- mService.getLifecycleManager().scheduleTransaction(prev.app.thread, prev.appToken,
- PauseActivityItem.obtain(prev.finishing, userLeaving,
- prev.configChangeFlags, pauseImmediately));
- } catch (Exception e) {
- mPausingActivity = null ;
- }
- } else {
- mPausingActivity = null ;
- }
- ......
- // mPausingActivity has been assigned a value before, which is the current Activity
- if (mPausingActivity != null ) {
- ......
- if (pauseImmediately) { // This is false , enter the else branch
- completePauseLocked( false , resuming);
- return false ;
- } else {
- // 2\. Send a message with a delay of 500ms and wait for the pause process to complete for a while
- //Eventually the activityPausedLocked() method will be called back
- schedulePauseTimeout(prev);
- return true ;
- }
- } else {
- // Will not enter this branch
- }
- }
There are two key operations here. The first step is to distribute the lifecycle process through ClientLifecycleManager as noted in Note 1. The second step is to send a message with a delay of 500ms to wait for the onPause process. However, if the process in the first step has been completed within 500ms, the message will be cancelled. So the final logic of these two steps is actually the same. Let's just look at the first step here. - mService.getLifecycleManager().scheduleTransaction(prev.app.thread, prev.appToken,
- PauseActivityItem.obtain(prev.finishing, userLeaving,
- prev.configChangeFlags, pauseImmediately));
ClientLifecycleManager sends an EXECUTE_TRANSACTION event to Handler H of the main thread, calling the execute() and postExecute() methods of XXXActivityItem. In the execute() method, Binder calls the corresponding handleXXXActivity() method in ActivityThread. Here, it is the handlePauseActivity() method, which calls back Activity.onPause() through the Instrumentation.callActivityOnPause(r.activity) method. - > Instrumentation.java
- public void callActivityOnPause(Activity activity) {
- activity.performPause();
- }
At this point, the onPause() method is executed. But the process is not over yet, and the next Activity should be displayed. As mentioned earlier, the execute() and postExecute() methods of PauseActivityItem will be called. The execute() method calls back the current Activity.onPause(), and the postExecute() method is to find the Activity to be displayed. - > PauseActivityItem.java
- public void postExecute(ClientTransactionHandler client, IBinder token,
- PendingTransactionActions pendingActions) {
- try {
- ActivityManager.getService().activityPaused(token);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
- The binder calls the AMS.activityPaused() method.
- > ActivityManagerService.java
- public final void activityPaused(IBinder token) {
- synchronized(this) {
- ActivityStack stack = ActivityRecord.getStackLocked(token);
- if (stack != null ) {
- stack.activityPausedLocked(token, false );
- }
- }
- }
The ActivityStack.activityPausedLocked() method is called. - > ActivityStack.java
- final void activityPausedLocked(IBinder token, boolean timeout) {
- final ActivityRecord r = isInStackLocked(token);
- if (r != null ) {
- // Look here
- mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
- if (mPausingActivity == r) {
- mService.mWindowManager.deferSurfaceLayout();
- try {
- // Look here
- completePauseLocked( true /* resumeNext */, null /* resumingActivity */);
- finally
- mService.mWindowManager.continueSurfaceLayout();
- }
- return ;
- } else {
- // Will not enter the else branch
- }
- }
- }
There is a line of code mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r) above, which removes the message that was delayed by 500ms. Next, look at the completePauseLocked() method. - > ActivityStack.java
- private void completePauseLocked(boolean resumeNext, ActivityRecord resuming) {
- ActivityRecord prev = mPausingActivity;
- if (prev != null ) {
- // Set the state to PAUSED
- prev.setState(PAUSED, "completePausedLocked" );
- if (prev.finishing) { // 1\. finishing is true , enter this branch
- prev = finishCurrentActivityLocked(prev, FINISH_AFTER_VISIBLE, false ,
- "completedPausedLocked" );
- } else if (prev.app != null ) {
- // Will not enter this branch
- } else {
- prev = null ;
- }
- ......
- }
- if (resumeNext) {
- // The ActivityStack that currently has focus
- final ActivityStack topStack = mStackSupervisor.getFocusedStack();
- if (!topStack.shouldSleepOrShutDownActivities()) {
- // 2\. Restore the activity to be displayed
- mStackSupervisor.resumeFocusedStackTopActivityLocked(topStack, prev, null );
- } else {
- checkReadyForSleep();
- ActivityRecord top = topStack.topRunningActivityLocked();
- if ( top == null || (prev != null && top != prev)) {
- mStackSupervisor.resumeFocusedStackTopActivityLocked();
- }
- }
- }
- ......
- }
There are two steps here. Note 1 determines the finishing state. Do you remember where finishing is assigned to true? In Activity.finish() -> AMS.finishActivity() -> ActivityStack.requestFinishActivityLocked() -> ActivityStack.finishActivityLocked() method. So the next method to be called is finishCurrentActivityLocked() method. Note 2 is to display the Activity that should be displayed, so I won't go into detail. Then follow to the finishCurrentActivityLocked() method. Looking at the name, it must be used to stop/destroy the activity. - > ActivityStack.java
- /*
- * Mark the parameters brought in from the front
- * prev, FINISH_AFTER_VISIBLE, false , "completedPausedLocked"
- */
- final ActivityRecord finishCurrentActivityLocked(ActivityRecord r, int mode, boolean oomAdj,
- String reason) {
- // Get the top Activity to be displayed
- final ActivityRecord next = mStackSupervisor.topRunningActivityLocked(
- true /* considerKeyguardState */);
- // 1\. mode is FINISH_AFTER_VISIBLE, enter this branch
- if (mode == FINISH_AFTER_VISIBLE && (r.visible || r.nowVisible)
- && next != null && ! next .nowVisible) {
- if (!mStackSupervisor.mStoppingActivities. contains (r)) {
- // Add to mStackSupervisor.mStoppingActivities
- addToStopping(r, false /* scheduleIdle */, false /* idleDelayed */);
- }
- // Set the state to STOPPING
- r.setState(STOPPING, "finishCurrentActivityLocked" );
- return r;
- }
- ......
- // destroy will be executed below, but the code cannot be executed here
- if (mode == FINISH_IMMEDIATELY
- || (prevState == PAUSED
- && (mode == FINISH_AFTER_PAUSE || inPinnedWindowingMode()))
- || finishingActivityInNonFocusedStack
- || prevState == STOPPING
- || prevState == STOPPED
- || prevState == ActivityState.INITIALIZING) {
- boolean activityRemoved = destroyActivityLocked(r, true , "finish-imm:" + reason);
- ......
- return activityRemoved ? null : r;
- }
- ......
- }
Note 1: The value of mode is FINISH_AFTER_VISIBLE, and the new Activity has not yet had onResume, so r.visible || r.nowVisible and next != null && !next.nowVisible are both valid, and the subsequent destroy process will not be entered. Although we haven't gotten the answer we want, it is at least in line with expectations. If we destroy directly here, the problem of delaying onDestroy for 10 seconds will be solved. For these activities that have not been destroyed yet, the addToStopping(r, false, false) method is executed. Let's continue to track it. - > ActivityStack.java
- void addToStopping(ActivityRecord r, boolean scheduleIdle, boolean idleDelayed) {
- if (!mStackSupervisor.mStoppingActivities. contains (r)) {
- mStackSupervisor.mStoppingActivities.add (r) ;
- ......
- }
- ......
- // In the omitted code, the storage capacity of mStoppingActivities is limited. Exceeding the limit may trigger the destruction process in advance
- }
These activities waiting to be destroyed are stored in the mStoppingActivities collection of ActivityStackSupervisor, which is an ArrayList. The entire finish process ends here. The previous Activity is saved in the ActivityStackSupervisor.mStoppingActivities collection, and the new Activity is displayed. The problem seems to be in a dilemma. When should onStop/onDestroy be called back? In fact, this is the fundamental problem. Although we can't see the essence of finish() above, it can help us form a complete process and help us form a complete closed loop of fragmented upper-level knowledge. 2. Calling onStop/onDestroy During the activity jump process, in order to ensure a smooth user experience, as long as the previous activity cannot interact with the user, that is, after onPause() is called back, the next activity will start its own life cycle process. Therefore, the calling time of onStop/onDestroy is uncertain, and even as in the example at the beginning of the article, it is called back after 10 seconds. So, who drives the execution of onStop/onDestroy? Let's take a look at the onResume process of the next activity. Look directly at the ActivityThread.handleResumeActivity() method. I believe everyone is familiar with the calling process of the life cycle. - > ActivityThread.java
- public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
- String reason) {
- ......
- // callback onResume
- final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
- ......
- final Activity a = r.activity;
- ......
- if (r.window == null && !a.mFinished && willBeVisible) {
- ......
- if (a.mVisibleFromClient) {
- if (!a.mWindowAdded) {
- a.mWindowAdded = true ;
- // Add decorView to WindowManager
- wm.addView(decor, l);
- } else {
- a.onWindowAttributesChanged(l);
- }
- }
- } else if (!willBeVisible) {
- ......
- }
- ......
- // Idler will be executed when the main thread is idle
- Looper.myQueue().addIdleHandler(new Idler());
- }
The handleResumeActivity() method is the most important part of the entire UI display process. It first calls back Activity.onResume(), then adds DecorView to the Window, which includes creating ViewRootImpl, creating Choreographer, communicating with WMS through Binder, registering vsync signals, and the famous measure/draw/layout. The source code of this part is really worth reading, but it is not the focus of this article, and will be discussed separately later. After the final interface is drawn and displayed, there is a line of code Looper.myQueue().addIdleHandler(new Idler()) . IdleHandler is not sure if you are familiar with it. It provides a mechanism that executes the callback method of IdleHandler when the main thread message queue is idle. As for what counts as "idle", we can look at the MessageQueue.next() method. - > MessageQueue.java
- Message next () {
- ......
- int pendingIdleHandlerCount = -1;
- int nextPollTimeoutMillis = 0;
- for (;;) {
- // The blocking method is mainly implemented by listening to the write events of the file descriptor through the epoll of the native layer.
- // If nextPollTimeoutMillis = -1, blocking will not time out.
- // If nextPollTimeoutMillis = 0, it will not block and return immediately.
- // If nextPollTimeoutMillis > 0, the longest blocking time is nextPollTimeoutMillis milliseconds (timeout). If a program wakes up during this period, it will return immediately.
- nativePollOnce(ptr, nextPollTimeoutMillis);
- synchronized (this) {
- Message prevMsg = null ;
- Message msg = mMessages;
- if (msg != null && msg.target == null ) {
- // msg.target == null indicates that this message is a message barrier (sent via the postSyncBarrier method)
- // If a message barrier is found, the first asynchronous message will be found in a loop (if there is an asynchronous message), and all synchronous messages will be ignored (normally sent are generally synchronous messages)
- do {
- prevMsg = msg;
- msg = msg.next ;
- } while (msg != null && !msg.isAsynchronous());
- }
- if (msg != null ) {
- if (now < msg. when ) {
- // The message trigger time has not arrived, set the timeout for the next poll
- nextPollTimeoutMillis = ( int ) Math. min (msg. when - now, Integer .MAX_VALUE);
- } else {
- // Get Message
- mBlocked = false ;
- if (prevMsg != null ) {
- prevMsg.next = msg.next ;
- } else {
- mMessages = msg.next ;
- }
- msg.next = null ;
- msg.markInUse(); // mark FLAG_IN_USE
- return msg;
- }
- } else {
- nextPollTimeoutMillis = -1;
- }
- ......
- /*
- * Two conditions:
- * 1\. pendingIdleHandlerCount = -1
- * 2\. The mMessage obtained this time is empty or needs to be processed later
- */
- if (pendingIdleHandlerCount < 0
- && (mMessages == null || now < mMessages. when )) {
- pendingIdleHandlerCount = mIdleHandlers. size ();
- }
- if (pendingIdleHandlerCount <= 0) {
- // No idle handlers to run, continue looping
- mBlocked = true ;
- continue ;
- }
- if (mPendingIdleHandlers == null ) {
- mPendingIdleHandlers = new IdleHandler[Math. max (pendingIdleHandlerCount, 4)];
- }
- mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
- }
- // The next time next is called, pendingIdleHandlerCount will be set to -1 again, which will not cause an infinite loop
- for ( int i = 0; i < pendingIdleHandlerCount; i++) {
- final IdleHandler idler = mPendingIdleHandlers[i];
- mPendingIdleHandlers[i] = null ; // release the reference to the handler
- boolean keep = false ;
- try {
- // Execute Idler
- keep = idler.queueIdle();
- } catch (Throwable t) {
- Log.wtf(TAG, "IdleHandler threw exception" , t);
- }
- if (!keep) {
- synchronized (this) {
- mIdleHandlers.remove(idler);
- }
- }
- }
- // Set pendingIdleHandlerCount to zero
- pendingIdleHandlerCount = 0;
- nextPollTimeoutMillis = 0;
- }
- }
After the normal message processing mechanism, the IdleHandler is additionally processed. When the Message obtained this time is empty or needs to be processed with a delay, the IdleHandler object in the mIdleHandlers array will be executed. There is also some additional logic about pendingIdleHandlerCount to prevent circular processing. Therefore, if nothing unexpected happens, when the new Activity completes page drawing and display, the main thread can stop and take a break to execute IdleHandler. Then come back to handleResumeActivity(), Looper.myQueue().addIdleHandler(new Idler()), where Idler is a specific implementation class of IdleHandler. - > ActivityThread.java
- private class Idler implements MessageQueue.IdleHandler {
- @Override
- public final boolean queueIdle() {
- ActivityClientRecord a = mNewActivities;
- ......
- }
- if (a != null ) {
- mNewActivities = null ;
- IActivityManager am = ActivityManager.getService();
- ActivityClientRecord prev;
- do {
- if (a.activity != null && !a.activity.mFinished) {
- try {
- // Call AMS.activityIdle()
- am.activityIdle(a.token, a.createdConfig, stopProfiling);
- a.createdConfig = null ;
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
- prev = a;
- a = a.nextIdle;
- prev.nextIdle = null ;
- } while (a != null );
- }
- ......
- return false ;
- }
- }
Binder called AMS.activityIdle(). - > ActivityManagerService.java
- public final void activityIdle(IBinder token, Configuration config, boolean stopProfiling) {
- final long origId = Binder.clearCallingIdentity();
- synchronized (this) {
- ActivityStack stack = ActivityRecord.getStackLocked(token);
- if (stack != null ) {
- ActivityRecord r =
- mStackSupervisor.activityIdleInternalLocked(token, false /* fromTimeout */,
- false /* processPausingActivities */, config);
- ......
- }
- }
- }
The ActivityStackSupervisor.activityIdleInternalLocked() method is called. - > ActivityStackSupervisor.java
- final ActivityRecord activityIdleInternalLocked(final IBinder token, boolean fromTimeout,
- boolean processPausingActivities, Configuration config) {
- ArrayList<ActivityRecord> finishes = null ;
- ArrayList<UserState> startingUsers = null ;
- int NS = 0;
- int NF = 0;
- boolean booting = false ;
- boolean activityRemoved = false ;
- ActivityRecord r = ActivityRecord.forTokenLocked(token);
- ......
- // Get the Activity to stop
- final ArrayList<ActivityRecord> stops = processStoppingActivitiesLocked(r,
- true /* remove */, processPausingActivities);
- NS = stops != null ? stops. size () : 0;
- if ((NF = mFinishingActivities. size ()) > 0) {
- finishes = new ArrayList<>(mFinishingActivities);
- mFinishingActivities.clear();
- }
- // The stop that should be stopped
- for ( int i = 0; i < NS; i++) {
- r = stops.get(i);
- final ActivityStack stack = r.getStack();
- if (stack != null ) {
- if (r.finishing) {
- stack.finishCurrentActivityLocked(r, ActivityStack.FINISH_IMMEDIATELY, false ,
- "activityIdleInternalLocked" );
- } else {
- stack.stopActivityLocked(r);
- }
- }
- }
- // The destroy method
- for ( int i = 0; i < NF; i++) {
- r = finishes.get(i);
- final ActivityStack stack = r.getStack();
- if (stack != null ) {
- activityRemoved |= stack.destroyActivityLocked(r, true , "finish-idle" );
- }
- }
- ......
- return r;
- }
stops and finishes are two ActivityRecord arrays to be stopped and destroyed respectively. The stops array is obtained through the ActivityStackSuperVisor.processStoppingActivitiesLocked() method, follow it and have a look. - > ActivityStackSuperVisor.java
- final ArrayList<ActivityRecord> processStoppingActivitiesLocked(ActivityRecord idleActivity,
- boolean remove, boolean processPausingActivities) {
- ArrayList<ActivityRecord> stops = null ;
- final boolean nowVisible = allResumedActivitiesVisible();
- // Traverse mStoppingActivities
- for ( int activityNdx = mStoppingActivities. size () - 1; activityNdx >= 0;
- ActivityRecord s = mStoppingActivities.get(activityNdx);
- ......
- }
- return stops;
- }
We will not look at the detailed processing logic in the middle, we only need to pay attention to the mStoppingActivities collection in ActivityStackSuperVisor that is traversed here. When analyzing the finish() process to the final addToStopping() method, we mentioned that these Activities waiting to be destroyed are saved in the mStoppingActivities collection of ActivityStackSupervisor, which is an ArrayList. Seeing this, the process is finally clear. Let's think back to the example at the beginning of the article. Because SecondActivity continuously sends messages to the main thread, Idler cannot be executed for a long time, and onStop/onDestroy will not be called back. 3. onStop/onDestroy is delayed by 10 seconds? No, it is obviously called back after 10 seconds. This shows that even if the main thread has no chance to execute Idle, the system still provides a fallback mechanism to prevent the unnecessary Activity from being recycled for a long time, thus causing memory leaks and other problems. From the actual phenomenon, we can guess that this fallback mechanism is to actively release the Activity 10 seconds after onResume. Let's go back to the ActivityStackSuperVisor.resumeFocusedStackTopActivityLocked() method that shows the Activity to be redirected. I won't follow you in here, but will just give you the call chain. - ASS.resumeFocusedStackTopActivityLocked() -> ActivityStack.resumeTopActivityUncheckedLocked() -> ActivityStack.resumeTopActivityInnerLocked() -> ActivityRecord.completeResumeLocked() -> ASS.scheduleIdleTimeoutLocked()
- > ActivityStackSuperVisor.java
- void scheduleIdleTimeoutLocked(ActivityRecord next ) {
- Message msg = mHandler.obtainMessage(IDLE_TIMEOUT_MSG, next );
- mHandler.sendMessageDelayed(msg, IDLE_TIMEOUT);
- }
The value of IDLE_TIMEOUT is 10, and a message is sent after a delay of 10 seconds. This message is processed in ActivityStackSupervisorHandler. - private final class ActivityStackSupervisorHandler extends Handler {
- ......
- case IDLE_TIMEOUT_MSG: {
- activityIdleInternal((ActivityRecord) msg.obj, true /* processPausingActivities */);
- } break;
- ......
- }
- void activityIdleInternal(ActivityRecord r, boolean processPausingActivities) {
- synchronized (mService) {
- activityIdleInternalLocked(r != null ? r.appToken : null , true /* fromTimeout */,
- processPausingActivities, null );
- }
- }
If you forget the activityIdleInternalLocked method, you can search upwards by pressing ctrl+F. If the main thread executes Idle within 10 seconds, this message will be removed. At this point, all the problems have been sorted out; Summarize- Activity's onStop/onDestroy relies on IdleHandler for callback, and is normally called when the main thread is idle. However, due to problems in some special scenarios, the main thread cannot be idle for a long time, and onStop/onDestroy will not be called for a long time. But this does not mean that the Activity will never be recycled. The system provides a fallback mechanism. If it is still not called after 10s of onResume callback, it will be triggered actively;
- Although there is a fallback mechanism, this is definitely not what we want to see anyway. If the onStop/onDestroy in our project is delayed by 10s, how can we troubleshoot the problem? You can use the Looper.getMainLooper().setMessageLogging() method to print out the messages in the main thread message queue;
- Due to the uncertainty of the onStop/onDestroy call timing, you must consider carefully when performing operations such as resource release to avoid the situation where resources are not released in time.
|