Android Advanced: Detailed analysis of the problem of onDestroy being called 10 seconds after calling Activity.finish() from the source code

Android Advanced: Detailed analysis of the problem of onDestroy being called 10 seconds after calling Activity.finish() from the source code

[[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().

  1. class FirstActivity : BaseLifecycleActivity() {
  2. private val binding by lazy { ActivityFirstBinding.inflate(layoutInflater) }
  3. var startTime = 0L
  4. override fun onCreate(savedInstanceState: Bundle?) {
  5. super.onCreate(savedInstanceState)
  6. setContentView(binding.root)
  7. binding.goToSecond.setOnClickListener {
  8. start<SecondActivity>()
  9. finish()
  10. startTime = System.currentTimeMillis()
  11. }
  12. }
  13. override fun onPause() {
  14. super.onPause()
  15. Log.e( "finish" , "onPause() distance to finish(): ${System.currentTimeMillis() - startTime} ms" )
  16. }
  17. override fun onStop() {
  18. super.onStop()
  19. Log.e( "finish" , "onStop() distance to finish(): ${System.currentTimeMillis() - startTime} ms" )
  20. }
  21. override fun onDestroy() {
  22. super.onDestroy()
  23. Log.e( "finish" , "onDestroy() distance from finish(): ${System.currentTimeMillis() - startTime} ms" )
  24. }
  25. }

SecondActivity is a normal blank Activity without any operation. Click the button to jump to SecondActivity, and the print log is as follows:

  1. FirstActivity: onPause, onPause() to finish(): 5 ms
  2. SecondActivity: onCreate
  3. SecondActivity: onStart
  4. SecondActivity: onResume
  5. FirstActivity: onStop, onStop() to finish(): 660 ms
  6. 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:

  1. class SecondActivity : BaseLifecycleActivity() {
  2. private val binding by lazy { ActivitySecondBinding.inflate(layoutInflater) }
  3. override fun onCreate(savedInstanceState: Bundle?) {
  4. super.onCreate(savedInstanceState)
  5. setContentView(binding.root)
  6. postMessage()
  7. }
  8. private fun postMessage() {
  9. binding.secondBt.post {
  10. Thread.sleep(10)
  11. postMessage()
  12. }
  13. }
  14. }

Let's look at the log again:

  1. FirstActivity: onPause, onPause() to finish(): 6 ms
  2. SecondActivity: onCreate
  3. SecondActivity: onStart
  4. SecondActivity: onResume
  5. FirstActivity: onStop, onStop() to finish(): 10033 ms
  6. 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()

  1. > Activity.java
  2. public void finish() {
  3. finish(DONT_FINISH_TASK_WITH_ACTIVITY);
  4. }

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.

  1. > Activity.java
  2. private void finish( int finishTask) {
  3. // mParent is usually null and will be used in ActivityGroup
  4. if (mParent == null ) {
  5. ......
  6. try {
  7. // Binder calls AMS.finishActivity()
  8. if (ActivityManager.getService()
  9. .finishActivity(mToken, resultCode, resultData, finishTask)) {
  10. mFinished = true ;
  11. }
  12. } catch (RemoteException e) {
  13. }
  14. } else {
  15. mParent.finishFromChild(this);
  16. }
  17. ......
  18. }

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.

  1. > ActivityManagerService.java
  2. public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
  3. int finishTask) {
  4. ......
  5. synchronized(this) {
  6. // token holds a weak reference to ActivityRecord
  7. ActivityRecord r = ActivityRecord.isInStackLocked(token);
  8. if (r == null ) {
  9. return   true ;
  10. }
  11. ......
  12. try {
  13. boolean res;
  14. final boolean finishWithRootActivity =
  15. finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY;
  16. // The finishTask parameter is DONT_FINISH_TASK_WITH_ACTIVITY, enter the else branch
  17. if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY
  18. || (finishWithRootActivity && r == rootR)) {
  19. res = mStackSupervisor.removeTaskByIdLocked(tr.taskId, false ,
  20. finishWithRootActivity, "finish-activity" );
  21. } else {
  22. // Call ActivityStack.requestFinishActivityLocked()
  23. res = tr.getStack().requestFinishActivityLocked(token, resultCode,
  24. resultData, "app-request" , true );
  25. }
  26. return res;
  27. finally
  28. Binder.restoreCallingIdentity(origId);
  29. }
  30. }
  31. }

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.

  1. > ActivityStack.java
  2. final boolean requestFinishActivityLocked(IBinder token, int resultCode,
  3. Intent resultData, String reason, boolean oomAdj) {
  4. ActivityRecord r = isInStackLocked(token);
  5. if (r == null ) {
  6. return   false ;
  7. }
  8. finishActivityLocked(r, resultCode, resultData, reason, oomAdj);
  9. return   true ;
  10. }
  11. final boolean finishActivityLocked(ActivityRecord r, int resultCode, Intent resultData,
  12. String reason, boolean oomAdj) {
  13. // PAUSE_IMMEDIATELY is true , defined in ActivityStackSupervisor
  14. return finishActivityLocked(r, resultCode, resultData, reason, oomAdj, !PAUSE_IMMEDIATELY);
  15. }

The last method called is an overloaded finishActivityLocked() method.

  1. > ActivityStack.java
  2. // Parameter pauseImmediately is false  
  3. final boolean finishActivityLocked(ActivityRecord r, int resultCode, Intent resultData,
  4. String reason, boolean oomAdj, boolean pauseImmediately) {
  5. if (r.finishing) { // Repeat finish
  6. return   false ;
  7. }
  8. mWindowManager.deferSurfaceLayout();
  9. try {
  10. // mark r.finishing = true ,
  11. // The repeated finish detection above depends on this value
  12. r.makeFinishingLocked();
  13. final TaskRecord task = r.getTask();
  14. ......
  15. // Pause event distribution
  16. r.pauseKeyDispatchingLocked();
  17. adjustFocusedActivityStack(r, "finishActivity" );
  18. // Process activity result
  19. finishActivityResultsLocked(r, resultCode, resultData);
  20. // mResumedActivity is the current Activity, which will enter this branch
  21. if (mResumedActivity == r) {
  22. ......
  23. // Tell window manager to   prepare   for this one to be removed.
  24. r.setVisibility( false );
  25. if (mPausingActivity == null ) {
  26. // Start pausing mResumedActivity
  27. startPausingLocked( false , false , null , pauseImmediately);
  28. }
  29. ......
  30. } else if (!r.isState(PAUSING)) {
  31. // Will not enter this branch
  32. ......
  33. }
  34. return   false ;
  35. finally
  36. mWindowManager.continueSurfaceLayout();
  37. }
  38. }

After calling finish, you must pause the current Activity first, no problem. Next, look at the startPausingLocked() method.

  1. > ActivityStack.java
  2. final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping,
  3. ActivityRecord resuming, boolean pauseImmediately) {
  4. ......
  5. ActivityRecord prev = mResumedActivity;
  6. if (prev == null ) {
  7. // Activity without onResume cannot execute pause
  8. if (resuming == null ) {
  9. mStackSupervisor.resumeFocusedStackTopActivityLocked();
  10. }
  11. return   false ;
  12. }
  13. ......
  14. mPausingActivity = prev;
  15. // Set the current Activity state to PAUSING
  16. prev.setState(PAUSING, "startPausingLocked" );
  17. ......
  18. if (prev.app != null && prev.app.thread != null ) {
  19. try {
  20. ......
  21. // 1\. Distribute lifecycle events through ClientLifecycleManager
  22. // Eventually, an EXECUTE_TRANSACTION event will be sent to H
  23. mService.getLifecycleManager().scheduleTransaction(prev.app.thread, prev.appToken,
  24. PauseActivityItem.obtain(prev.finishing, userLeaving,
  25. prev.configChangeFlags, pauseImmediately));
  26. } catch (Exception e) {
  27. mPausingActivity = null ;
  28. }
  29. } else {
  30. mPausingActivity = null ;
  31. }
  32. ......
  33. // mPausingActivity has been assigned a value before, which is the current Activity
  34. if (mPausingActivity != null ) {
  35. ......
  36. if (pauseImmediately) { // This is false , enter the else branch
  37. completePauseLocked( false , resuming);
  38. return   false ;
  39. } else {
  40. // 2\. Send a message with a delay of 500ms and wait for the pause process to complete for a while
  41. //Eventually the activityPausedLocked() method will be called back
  42. schedulePauseTimeout(prev);
  43. return   true ;
  44. }
  45. } else {
  46. // Will not enter this branch
  47. }
  48. }

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.

  1. mService.getLifecycleManager().scheduleTransaction(prev.app.thread, prev.appToken,
  2. PauseActivityItem.obtain(prev.finishing, userLeaving,
  3. 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.

  1. > Instrumentation.java
  2. public void callActivityOnPause(Activity activity) {
  3. activity.performPause();
  4. }

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.

  1. > PauseActivityItem.java
  2. public void postExecute(ClientTransactionHandler client, IBinder token,
  3. PendingTransactionActions pendingActions) {
  4. try {
  5. ActivityManager.getService().activityPaused(token);
  6. } catch (RemoteException ex) {
  7. throw ex.rethrowFromSystemServer();
  8. }
  9. }
  10. The binder calls the AMS.activityPaused() method.
  11. > ActivityManagerService.java
  12. public final void activityPaused(IBinder token) {
  13. synchronized(this) {
  14. ActivityStack stack = ActivityRecord.getStackLocked(token);
  15. if (stack != null ) {
  16. stack.activityPausedLocked(token, false );
  17. }
  18. }
  19. }

The ActivityStack.activityPausedLocked() method is called.

  1. > ActivityStack.java
  2. final void activityPausedLocked(IBinder token, boolean timeout) {
  3. final ActivityRecord r = isInStackLocked(token);
  4. if (r != null ) {
  5. // Look here
  6. mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
  7. if (mPausingActivity == r) {
  8. mService.mWindowManager.deferSurfaceLayout();
  9. try {
  10. // Look here
  11. completePauseLocked( true /* resumeNext */, null /* resumingActivity */);
  12. finally
  13. mService.mWindowManager.continueSurfaceLayout();
  14. }
  15. return ;
  16. } else {
  17. // Will not enter the else branch
  18. }
  19. }
  20. }

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.

  1. > ActivityStack.java
  2. private void completePauseLocked(boolean resumeNext, ActivityRecord resuming) {
  3. ActivityRecord prev = mPausingActivity;
  4. if (prev != null ) {
  5. // Set the state to PAUSED
  6. prev.setState(PAUSED, "completePausedLocked" );
  7. if (prev.finishing) { // 1\. finishing is true , enter this branch
  8. prev = finishCurrentActivityLocked(prev, FINISH_AFTER_VISIBLE, false ,
  9. "completedPausedLocked" );
  10. } else if (prev.app != null ) {
  11. // Will not enter this branch
  12. } else {
  13. prev = null ;
  14. }
  15. ......
  16. }
  17. if (resumeNext) {
  18. // The ActivityStack that currently has focus
  19. final ActivityStack topStack = mStackSupervisor.getFocusedStack();
  20. if (!topStack.shouldSleepOrShutDownActivities()) {
  21. // 2\. Restore the activity to be displayed
  22. mStackSupervisor.resumeFocusedStackTopActivityLocked(topStack, prev, null );
  23. } else {
  24. checkReadyForSleep();
  25. ActivityRecord top = topStack.topRunningActivityLocked();
  26. if ( top == null || (prev != null && top != prev)) {
  27. mStackSupervisor.resumeFocusedStackTopActivityLocked();
  28. }
  29. }
  30. }
  31. ......
  32. }

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.

  1. > ActivityStack.java
  2. /*
  3. * Mark the parameters brought in from the front
  4. * prev, FINISH_AFTER_VISIBLE, false , "completedPausedLocked"  
  5. */
  6. final ActivityRecord finishCurrentActivityLocked(ActivityRecord r, int mode, boolean oomAdj,
  7. String reason) {
  8. // Get the top Activity to be displayed
  9. final ActivityRecord next = mStackSupervisor.topRunningActivityLocked(
  10. true /* considerKeyguardState */);
  11. // 1\. mode is FINISH_AFTER_VISIBLE, enter this branch
  12. if (mode == FINISH_AFTER_VISIBLE && (r.visible || r.nowVisible)
  13. && next != null && ! next .nowVisible) {
  14. if (!mStackSupervisor.mStoppingActivities. contains (r)) {
  15. // Add to mStackSupervisor.mStoppingActivities
  16. addToStopping(r, false /* scheduleIdle */, false /* idleDelayed */);
  17. }
  18. // Set the state to STOPPING
  19. r.setState(STOPPING, "finishCurrentActivityLocked" );
  20. return r;
  21. }
  22. ......
  23. // destroy will be executed below, but the code cannot be executed here
  24. if (mode == FINISH_IMMEDIATELY
  25. || (prevState == PAUSED
  26. && (mode == FINISH_AFTER_PAUSE || inPinnedWindowingMode()))
  27. || finishingActivityInNonFocusedStack
  28. || prevState == STOPPING
  29. || prevState == STOPPED
  30. || prevState == ActivityState.INITIALIZING) {
  31. boolean activityRemoved = destroyActivityLocked(r, true , "finish-imm:" + reason);
  32. ......
  33. return activityRemoved ? null : r;
  34. }
  35. ......
  36. }

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.

  1. > ActivityStack.java
  2. void addToStopping(ActivityRecord r, boolean scheduleIdle, boolean idleDelayed) {
  3. if (!mStackSupervisor.mStoppingActivities. contains (r)) {
  4. mStackSupervisor.mStoppingActivities.add (r) ;
  5. ......
  6. }
  7. ......
  8. // In the omitted code, the storage capacity of mStoppingActivities is limited. Exceeding the limit may trigger the destruction process in advance
  9. }

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.

  1. > ActivityThread.java
  2. public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
  3. String reason) {
  4. ......
  5. // callback onResume
  6. final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
  7. ......
  8. final Activity a = r.activity;
  9. ......
  10. if (r.window == null && !a.mFinished && willBeVisible) {
  11. ......
  12. if (a.mVisibleFromClient) {
  13. if (!a.mWindowAdded) {
  14. a.mWindowAdded = true ;
  15. // Add decorView to WindowManager
  16. wm.addView(decor, l);
  17. } else {
  18. a.onWindowAttributesChanged(l);
  19. }
  20. }
  21. } else if (!willBeVisible) {
  22. ......
  23. }
  24. ......
  25. // Idler will be executed when the main thread is idle
  26. Looper.myQueue().addIdleHandler(new Idler());
  27. }

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.

  1. > MessageQueue.java
  2. Message next () {
  3. ......
  4. int pendingIdleHandlerCount = -1;
  5. int nextPollTimeoutMillis = 0;
  6. for (;;) {
  7. // The blocking method is mainly implemented by listening to the write events of the file descriptor through the epoll of the native layer.
  8. // If nextPollTimeoutMillis = -1, blocking will not time out.
  9. // If nextPollTimeoutMillis = 0, it will not block and return immediately.
  10. // If nextPollTimeoutMillis > 0, the longest blocking time is nextPollTimeoutMillis milliseconds (timeout). If a program wakes up during this period, it will return immediately.
  11. nativePollOnce(ptr, nextPollTimeoutMillis);
  12. synchronized (this) {
  13. Message prevMsg = null ;
  14. Message msg = mMessages;
  15. if (msg != null && msg.target == null ) {
  16. // msg.target == null indicates that this message is a message barrier (sent via the postSyncBarrier method)
  17. // 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)
  18. do {
  19. prevMsg = msg;
  20. msg = msg.next ;
  21. } while (msg != null && !msg.isAsynchronous());
  22. }
  23. if (msg != null ) {
  24. if (now < msg. when ) {
  25. // The message trigger time has not arrived, set the timeout for the next poll
  26. nextPollTimeoutMillis = ( int ) Math. min (msg. when - now, Integer .MAX_VALUE);
  27. } else {
  28. // Get Message
  29. mBlocked = false ;
  30. if (prevMsg != null ) {
  31. prevMsg.next = msg.next ;
  32. } else {
  33. mMessages = msg.next ;
  34. }
  35. msg.next = null ;
  36. msg.markInUse(); // mark FLAG_IN_USE
  37. return msg;
  38. }
  39. } else {
  40. nextPollTimeoutMillis = -1;
  41. }
  42. ......
  43. /*
  44. * Two conditions:
  45. * 1\. pendingIdleHandlerCount = -1
  46. * 2\. The mMessage obtained this time is empty or needs to be processed later
  47. */
  48. if (pendingIdleHandlerCount < 0
  49. && (mMessages == null || now < mMessages. when )) {
  50. pendingIdleHandlerCount = mIdleHandlers. size ();
  51. }
  52. if (pendingIdleHandlerCount <= 0) {
  53. // No idle handlers to run, continue looping
  54. mBlocked = true ;
  55. continue ;
  56. }
  57. if (mPendingIdleHandlers == null ) {
  58. mPendingIdleHandlers = new IdleHandler[Math. max (pendingIdleHandlerCount, 4)];
  59. }
  60. mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
  61. }
  62. // The next time next is called, pendingIdleHandlerCount will be set to -1 again, which will not cause an infinite loop
  63. for ( int i = 0; i < pendingIdleHandlerCount; i++) {
  64. final IdleHandler idler = mPendingIdleHandlers[i];
  65. mPendingIdleHandlers[i] = null ; // release the reference to the handler
  66. boolean keep = false ;
  67. try {
  68. // Execute Idler
  69. keep = idler.queueIdle();
  70. } catch (Throwable t) {
  71. Log.wtf(TAG, "IdleHandler threw exception" , t);
  72. }
  73. if (!keep) {
  74. synchronized (this) {
  75. mIdleHandlers.remove(idler);
  76. }
  77. }
  78. }
  79. // Set pendingIdleHandlerCount to zero
  80. pendingIdleHandlerCount = 0;
  81. nextPollTimeoutMillis = 0;
  82. }
  83. }

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.

  1. > ActivityThread.java
  2. private class Idler implements MessageQueue.IdleHandler {
  3. @Override
  4. public final boolean queueIdle() {
  5. ActivityClientRecord a = mNewActivities;
  6. ......
  7. }
  8. if (a != null ) {
  9. mNewActivities = null ;
  10. IActivityManager am = ActivityManager.getService();
  11. ActivityClientRecord prev;
  12. do {
  13. if (a.activity != null && !a.activity.mFinished) {
  14. try {
  15. // Call AMS.activityIdle()
  16. am.activityIdle(a.token, a.createdConfig, stopProfiling);
  17. a.createdConfig = null ;
  18. } catch (RemoteException ex) {
  19. throw ex.rethrowFromSystemServer();
  20. }
  21. }
  22. prev = a;
  23. a = a.nextIdle;
  24. prev.nextIdle = null ;
  25. } while (a != null );
  26. }
  27. ......
  28. return   false ;
  29. }
  30. }

Binder called AMS.activityIdle().

  1. > ActivityManagerService.java
  2. public final void activityIdle(IBinder token, Configuration config, boolean stopProfiling) {
  3. final long origId = Binder.clearCallingIdentity();
  4. synchronized (this) {
  5. ActivityStack stack = ActivityRecord.getStackLocked(token);
  6. if (stack != null ) {
  7. ActivityRecord r =
  8. mStackSupervisor.activityIdleInternalLocked(token, false /* fromTimeout */,
  9. false /* processPausingActivities */, config);
  10. ......
  11. }
  12. }
  13. }

The ActivityStackSupervisor.activityIdleInternalLocked() method is called.

  1. > ActivityStackSupervisor.java
  2. final ActivityRecord activityIdleInternalLocked(final IBinder token, boolean fromTimeout,
  3. boolean processPausingActivities, Configuration config) {
  4. ArrayList<ActivityRecord> finishes = null ;
  5. ArrayList<UserState> startingUsers = null ;
  6. int NS = 0;
  7. int NF = 0;
  8. boolean booting = false ;
  9. boolean activityRemoved = false ;
  10. ActivityRecord r = ActivityRecord.forTokenLocked(token);
  11. ......
  12. // Get the Activity to stop
  13. final ArrayList<ActivityRecord> stops = processStoppingActivitiesLocked(r,
  14. true /* remove */, processPausingActivities);
  15. NS = stops != null ? stops. size () : 0;
  16. if ((NF = mFinishingActivities. size ()) > 0) {
  17. finishes = new ArrayList<>(mFinishingActivities);
  18. mFinishingActivities.clear();
  19. }
  20. // The stop that should be stopped
  21. for ( int i = 0; i < NS; i++) {
  22. r = stops.get(i);
  23. final ActivityStack stack = r.getStack();
  24. if (stack != null ) {
  25. if (r.finishing) {
  26. stack.finishCurrentActivityLocked(r, ActivityStack.FINISH_IMMEDIATELY, false ,
  27. "activityIdleInternalLocked" );
  28. } else {
  29. stack.stopActivityLocked(r);
  30. }
  31. }
  32. }
  33. // The destroy method
  34. for ( int i = 0; i < NF; i++) {
  35. r = finishes.get(i);
  36. final ActivityStack stack = r.getStack();
  37. if (stack != null ) {
  38. activityRemoved |= stack.destroyActivityLocked(r, true , "finish-idle" );
  39. }
  40. }
  41. ......
  42. return r;
  43. }

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.

  1. > ActivityStackSuperVisor.java
  2. final ArrayList<ActivityRecord> processStoppingActivitiesLocked(ActivityRecord idleActivity,
  3. boolean remove, boolean processPausingActivities) {
  4. ArrayList<ActivityRecord> stops = null ;
  5. final boolean nowVisible = allResumedActivitiesVisible();
  6. // Traverse mStoppingActivities
  7. for ( int activityNdx = mStoppingActivities. size () - 1; activityNdx >= 0; --activityNdx) {  
  8. ActivityRecord s = mStoppingActivities.get(activityNdx);
  9. ......
  10. }
  11. return stops;
  12. }

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.

  1. ASS.resumeFocusedStackTopActivityLocked() -> ActivityStack.resumeTopActivityUncheckedLocked() -> ActivityStack.resumeTopActivityInnerLocked() -> ActivityRecord.completeResumeLocked() -> ASS.scheduleIdleTimeoutLocked()
  2. > ActivityStackSuperVisor.java
  3. void scheduleIdleTimeoutLocked(ActivityRecord next ) {
  4. Message msg = mHandler.obtainMessage(IDLE_TIMEOUT_MSG, next );
  5. mHandler.sendMessageDelayed(msg, IDLE_TIMEOUT);
  6. }

The value of IDLE_TIMEOUT is 10, and a message is sent after a delay of 10 seconds. This message is processed in ActivityStackSupervisorHandler.

  1. private final class ActivityStackSupervisorHandler extends Handler {
  2. ......
  3. case IDLE_TIMEOUT_MSG: {
  4. activityIdleInternal((ActivityRecord) msg.obj, true /* processPausingActivities */);
  5. } break;
  6. ......
  7. }
  8. void activityIdleInternal(ActivityRecord r, boolean processPausingActivities) {
  9. synchronized (mService) {
  10. activityIdleInternalLocked(r != null ? r.appToken : null , true /* fromTimeout */,
  11. processPausingActivities, null );
  12. }
  13. }

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.

<<:  WeChat Android version 8.0.11 beta developer update content: optimize the Android 11 experience of mini-programs and mini-games

>>:  B-end designers come to see! Let us take you to understand the design concept of "B-end C-ization"

Recommend

Review: A complete event operation planning plan is like this

The end of the year is approaching and various ac...

Most persimmons do not grow on persimmon trees.

In a certain edition of the high school biology t...

How to give full play to the SEO function of the website?

1. SEO parameter settings Website title keywords:...

How to make users believe in your product?

Before users buy a product, their biggest concern...

After years of hard study, don’t go to a fake university!

Mixed Knowledge Specially designed to cure confus...

The latest practical skills to make money through Tik Tok live streaming!

In fact, it is very simple to judge whether a per...

HTML5 Mobile Design Basics

Desktop website design is mostly fixed layout or ...