Android background killing series 2: ActivityManagerService and App on-site recovery mechanism

Android background killing series 2: ActivityManagerService and App on-site recovery mechanism

This is the second in the Android background killing series, mainly explaining how ActivityMangerService restores processes killed in the background (based on 4.3). In the opening FragmentActivity and PhoneWindow background killing processing mechanism, some common problems caused by background killing are briefly described, as well as some compatibility of Android system controls with background killing, and the role of onSaveInstance and onRestoreInstance in execution timing. Finally, it talks about how to deal with background killing, but there is no explanation on how to restore processes killed in the background. This article does not involve background killing, such as the LowmemoryKiller mechanism, but only describes how to restore killed processes. Suppose an application is killed in the background, and when the App is called up again from the recent task list, how does the system handle it? There are several problems that may need to be solved:

  • How does the Android framework layer (AMS) know that the App is killed?
  • How is the scene before the App is killed saved?
  • How to restore the killed App by the system (AMS)
  • What is the difference between the startup process of an App killed by the background and the normal startup process?
  • Why is the order of Activity restoration reversed?

How does the system (AMS) know that the App is killed?

First, let's look at the first question. How does the system know that the Application has been killed? Android uses the Linux oomKiller mechanism, but simply makes a variant of it, using a hierarchical LowmemoryKiller. But this is actually at the kernel level. After LowmemoryKiller kills the process, it will not send a notification to the user space, which means that the ActivityMangerService at the framework layer cannot know whether the App has been killed. However, only by knowing whether the App or Activity has been killed can AMS (ActivityMangerService) correctly follow the wake-up process. So when does AMS know that the App or Activity has been killed in the background? Let's first look at what happens when we wake up from the recent task list.

The process of calling up the App again from the recent task list or Icon

In the system source code systemUi package, there is a RecentActivity, which is actually the entrance to the recent task list, and its presentation interface is displayed through RecentsPanelView. Click the recent App and its execution code is as follows:

  1. public void handleOnClick( View   view ) {
  2. ViewHolder holder = (ViewHolder) view .getTag();
  3. TaskDescription ad = holder.taskDescription;
  4. final Context context = view .getContext();
  5. final ActivityManager am = (ActivityManager)
  6. context.getSystemService(Context.ACTIVITY_SERVICE);
  7. Bitmap bm = holder.thumbnailViewImageBitmap;
  8. ...
  9. // Key point 1 If TaskDescription is not closed actively, it is closed normally, ad.taskId is >= 0
  10. if (ad.taskId >= 0) {
  11. // This is an active task; it should just go to the foreground.
  12. am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME,
  13. opts);
  14. } else {
  15. Intent intent = ad.intent;
  16. intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
  17. | Intent.FLAG_ACTIVITY_TASK_ON_HOME
  18. | Intent.FLAG_ACTIVITY_NEW_TASK);
  19. try {
  20. context.startActivityAsUser(intent, opts,
  21. new UserHandle(UserHandle.USER_CURRENT));
  22. }...
  23. }

In the above code, there is a judgment ad.taskId >= 0. If this condition is met, the APP is called up through moveTaskToFront. So how is ad.taskId obtained? There are various RecentTasksLoader in the recent package. This class is a Loader used to load the recent task list. Take a look at its source code, mainly at loading:

  1. @Override
  2. protected Void doInBackground(Void... params) {
  3. // We load   in two stages: first , we update progress with just the first screenful
  4. // of items. Then , we update   with the rest of the items
  5. final int origPri = Process.getThreadPriority(Process.myTid());
  6. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  7. final PackageManager pm = mContext.getPackageManager();
  8. final ActivityManager am = (ActivityManager)
  9. mContext.getSystemService(Context.ACTIVITY_SERVICE);
  10.  
  11. final List<ActivityManager.RecentTaskInfo> recentTasks =
  12. am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
  13.              
  14. ....
  15. TaskDescription item = createTaskDescription(recentInfo.id,
  16. recentInfo.persistentId, recentInfo.baseIntent,
  17. recentInfo.origActivity, recentInfo.description);
  18. ....
  19. }

As you can see, it actually requests the recent task information from AMS through ActivityManger's getRecentTasks, and then creates TaskDescription through createTaskDescription. The recentInfo.id passed here is actually the taskId of TaskDescription. Let's take a look at its meaning:

  1. public List<ActivityManager.RecentTaskInfo> getRecentTasks( int maxNum,
  2. int flags, int userId) {
  3. ...
  4. IPackageManager pm = AppGlobals.getPackageManager();
  5.  
  6. final int N = mRecentTasks. size ();
  7. ...
  8. for ( int i=0; i<N && maxNum > 0; i++) {
  9. TaskRecord tr = mRecentTasks.get(i);
  10. if (i == 0
  11. || ((flags&ActivityManager.RECENT_WITH_EXCLUDED) != 0)
  12. || (tr.intent == null )
  13. || ((tr.intent.getFlags()
  14. &Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0)) {
  15. ActivityManager.RecentTaskInfo rti
  16. = new ActivityManager.RecentTaskInfo();
  17. rti.id = tr.numActivities > 0 ? tr.taskId : -1;
  18. rti.persistentId = tr.taskId;
  19. rti.baseIntent = new Intent(
  20. tr.intent != null ? tr.intent : tr.affinityIntent);
  21. if (!detailed) {
  22. rti.baseIntent.replaceExtras((Bundle) null );
  23. }

It can be seen that the id of RecentTaskInfo is determined by TaskRecord. If numActivities > 0 in TaskRecord, go to the Id of TaskRecord, otherwise take -1. The numActivities here is actually the number of ActivityRecords recorded in TaskRecode. For more specific details, you can check ActivityManagerService and ActivityStack by yourself. Then it is easy to explain here. As long as it is a surviving APP or an APP killed by LowmemoryKiller, its AMS ActivityRecord is completely saved. This is the basis for recovery. The data obtained by RecentActivity is actually a replica of the one in AMS. RecentActivity does not know whether the APP to be awakened is alive. As long as TaskRecord tells RecentActivity that it is in stock, it will directly go through the awakening process, that is, awaken the App through ActivityManager's moveTaskToFront. As for the subsequent work, it will be completely handled by AMS. Now take a look at the flowchart here:

When calling up the App, AMS detects whether the App or Activity is killed abnormally.

Next, look at moveTaskToFrontLocked. This function is in ActivityStack. ActivityStack is mainly used to manage ActivityRecord stack. All started Activities retain an ActivityRecord in ActivityStack, which is also a basis for AMS to manage Activity. ActivityStack finally movesTaskToFrontLocked and calls resumeTopActivityLocked to wake up Activity. AMS obtains the information of the Activity to be resumed mainly through ActivityRecord. It does not know whether the Activity itself is alive. After obtaining it, AMS knows the link of waking up the Activity before knowing that the App or Activity is killed. Take a look at the resumeTopActivityLocked source code:

  1. final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
  2.    
  3. // This activity is now becoming visible.
  4. mService.mWindowManager.setAppVisibility( next .appToken, true );
  5.                 
  6. .... Restore Logic
  7. if ( next .app != null && next .app.thread != null ) {
  8. // Normal recovery
  9. try {
  10. // Deliver all pending results.
  11. ArrayList a = next .results;
  12. if (a != null ) {
  13. final int N = a. size ();
  14. if (! next .finishing && N > 0) {
  15. next .app.thread.scheduleSendResult( next .appToken, a);
  16. }
  17. }
  18. ...
  19. next .app.thread.scheduleResumeActivity( next .appToken,
  20. mService.isNextTransitionForward());
  21. ...
  22. } catch (Exception e) {
  23. // Whoops, need to restart this activity!
  24. // A restart is needed here. Is it killed by the background? Is it an abnormal branch? ? ? ? Killed by abnormality
  25. if (DEBUG_STATES) Slog.v(TAG, "Resume failed; resetting state to "  
  26. + lastState + ": " + next );
  27. next .state = lastState;
  28. mResumedActivity = lastResumedActivity;
  29. <! --Indeed, this is because the process has hung up-->  
  30. Slog.i(TAG, "Restarting because process died: " + next );
  31. . . .
  32. startSpecificActivityLocked( next , true , false );
  33. return   true ;
  34. }
  35. ...
  36. }

Since finish is not actively called, AMS will not clean up ActivityRecord and TaskRecord. Therefore, when resuming, the above branch is taken. Next.app.thread.scheduleSendResult or next.app.thread.scheduleResumeActivity may be called here to call up the previous Activity. However, if the APP or Activity is killed by an exception, the call-up operation must fail and an exception will be thrown. First, assuming that the entire APP is killed, the Binder thread that communicates with AMS on the APP side no longer exists. At this time, RemoteException will be thrown when communicating through Binder. In this way, the catch part below will be taken, and the APP will be rebuilt again through startSpecificActivityLocked, and the last Activity will be rebuilt. In fact, you can use AIDL to write a C/S communication locally, close one end, and then access it with the other end, which will throw a RemoteException, as shown below:

There is another possibility, the APP is not killed, but the Activity is killed. What will happen at this time? First of all, the management of Activity must be through AMS, and the killing of Activity must be handled by AMS and recorded. Strictly speaking, this situation does not belong to background killing, because it belongs to the normal management of AMS and is within the controllable range. For example, if the "Do not retain activities" in the developer mode is turned on, at this time, although the Activity will be killed, the ActivitRecord is still retained, so there is still a trace to follow when waking up or rolling back. Take a look at the Destroy callback code of ActivityStack.

  1. final boolean destroyActivityLocked(ActivityRecord r,
  2. boolean removeFromApp, boolean oomAdj, String reason) {
  3. ...
  4. if (hadApp) {
  5. ...
  6. boolean skipDestroy = false ;
  7. try {
  8. Key code 1
  9. r.app.thread.scheduleDestroyActivity(r.appToken, r.finishing,
  10. r.configChangeFlags);
  11. ...
  12. if (r.finishing && !skipDestroy) {
  13. if (DEBUG_STATES) Slog.v(TAG, "Moving to DESTROYING: " + r
  14. + " (destroy requested)" );
  15. r.state = ActivityState.DESTROYING;
  16. Message msg = mHandler.obtainMessage(DESTROY_TIMEOUT_MSG);
  17. msg.obj = r;
  18. mHandler.sendMessageDelayed(msg, DESTROY_TIMEOUT);
  19. } else {
  20. Key code 2
  21. r.state = ActivityState.DESTROYED;
  22. if (DEBUG_APP) Slog.v(TAG, "Clearing app during destroy for activity " + r);
  23. r.app = null ;
  24. }
  25. }
  26. return removedFromHistory;
  27. }

There are two key points here: 1. Tell the client's AcvitityThread to clear the Activity; 2. If the AMS closes the Activity abnormally, set the state of the ActivityRecord to ActivityState.DESTROYED and clear its ProcessRecord reference: r.app = null. This is an important sign when waking up. Through this, AMS can know that the Activity was closed abnormally by itself. Setting ActivityState.DESTROYED is to avoid the subsequent clearing logic.

  1. final void activityDestroyed(IBinder token) {
  2. synchronized (mService) {
  3. final long origId = Binder.clearCallingIdentity();
  4. try {
  5. ActivityRecord r = ActivityRecord.forToken(token);
  6. if (r != null ) {
  7. mHandler.removeMessages(DESTROY_TIMEOUT_MSG, r);
  8. }
  9. int   index = indexOfActivityLocked(r);
  10. if ( index >= 0) {
  11. 1 <! --Whether to remove ActivityRecord from the history list here -->  
  12. if (r.state == ActivityState.DESTROYING) {
  13. cleanUpActivityLocked(r, true , false );
  14. removeActivityFromHistoryLocked(r);
  15. }
  16. }
  17. resumeTopActivityLocked( null );
  18. finally
  19. Binder.restoreCallingIdentity(origId);
  20. }
  21. }
  22. }

Look at the key point 1 of the code. Only when r.state == ActivityState.DESTROYING will ActivityRecord be removed. However, for an Activity that is not finished abnormally, its state will not be set to ActivityState.DESTROYING. It will directly skip ActivityState.DESTROYING and be set to ActivityState.DESTROYED. Therefore, it will not removeActivityFromHistoryLocked, which means that the ActivityRecord scene is retained. It seems that it also relies on exceptions to distinguish whether the Activity is terminated normally. How to start the Activity in this case? Through the above two analysis, we know two key points.

  1. ActivityRecord is not removed from HistoryRecord list
  2. The ProcessRecord field of ActivityRecord is set to null, r.app = null

This ensures that when resumeTopActivityLocked, the startSpecificActivityLocked branch is taken

  1. final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
  2. ...
  3.               
  4. if ( next .app != null && next .app.thread != null ) {
  5. ...
  6.          
  7. } else {
  8. // Whoops, need to restart this activity!
  9. ...
  10. startSpecificActivityLocked( next , true , true );
  11. }
  12.  
  13. return   true ;
  14. }

At this point, AMS knows whether the app or activity has been killed abnormally, and thus decides whether to go through the resume process or the restore process.

How to save the scene before the App is killed: New Activity startup and old Activity preservation

The process of saving the App scene is relatively simple. The entry point is basically when starting Activity. As long as the interface jumps, it basically involves the switching of Activity and the saving of the current Activity scene: Let's draw a simple diagram first. When talking about FragmentActivity in the opening part, I briefly talked about the execution timing of onSaveInstance. Here we will take a closer look at how AMS manages these jumps and scene saving. Simulate the scenario: When Activity A starts Activity B, A is not visible at this time and may be destroyed. It is necessary to save the scene of A. What is this process like: It is briefly described as follows

  • ActivityA startActivity ActivityB
  • ActivityA pause
  • ActivityB create
  • ActivityB start
  • Activity B resume
  • ActivityA onSaveInstance
  • ActivityA stop

The process is probably as follows:

Now let's follow the source code step by step to see what AMS does when the new Activity is started and the old Activity is saved: skip the simple startActivity and go directly to AMS to see

ActivityManagerService

  1. public final int startActivityAsUser(IApplicationThread caller, String callingPackage,
  2. Intent intent, String resolvedType, IBinder resultTo,
  3. String resultWho, int requestCode, int startFlags,
  4. String profileFile, ParcelFileDescriptor profileFd, Bundle options, int userId) {
  5. enforceNotIsolatedCaller( "startActivity" );
  6. ...
  7. return mMainStack.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType,
  8. resultTo, resultWho, requestCode, startFlags, profileFile, profileFd,
  9. null , null , options, userId);
  10. }

ActivityStack

  1. final int startActivityMayWait(IApplicationThread caller, int callingUid,
  2.                    
  3. int res = startActivityLocked(caller, intent, resolvedType,
  4. aInfo, resultTo, resultWho, requestCode, callingPid, callingUid,
  5. callingPackage, startFlags, options, componentSpecified, null );
  6.          
  7. . . .
  8. }

Here, startActivityMayWait is used to start a new APP or a new Activity. Here we only look at the simple ones. As for the process of starting the App from the desktop, you can refer to more detailed articles, such as Lao Luo's startActivity process, which is probably to create a new ActivityRecord, ProcessRecord, etc., and add the corresponding stack in AMS, etc., resumeTopActivityLocked is the unified entrance for interface switching. When you come in for the first time, since ActivityA is not paused yet, you need to pause ActivityA first. After these are completed,

ActivityStack

  1. final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
  2. ...
  3. <! --You must set the current Resume Activity to pause and then stop it before you can continue -->  
  4. // We need to start pausing the current activity so the top one
  5. // can be resumed...
  6. if (mResumedActivity != null ) {
  7. if ( next .app != null && next .app.thread != null ) {
  8.            
  9. mService.updateLruProcessLocked( next .app, false );
  10. }
  11. startPausingLocked(userLeaving, false );
  12. return   true ;
  13. }
  14. ....

In fact, here is to pause ActivityA. AMS tells ActivityThread that ActivityA needs to be paused through Binder. After ActivityThread is completed, it notifies AMS through Binder, and AMS will start to resume ActivityB.

  1. private final void startPausingLocked(boolean userLeaving, boolean uiSleeping) {
  2.  
  3. if (prev.app != null && prev.app.thread != null ) {
  4. ...
  5. try {
  6. prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,
  7. userLeaving, prev.configChangeFlags);

ActivityThread

  1. private void handlePauseActivity(IBinder token, boolean finished,
  2. boolean userLeaving, int configChanges) {
  3. ActivityClientRecord r = mActivities.get(token);
  4. if (r != null ) {
  5. ...
  6. performPauseActivity(token, finished, r.isPreHoneycomb());
  7. ...
  8. // Tell the activity manager we have paused.
  9. try {
  10. ActivityManagerNative.getDefault().activityPaused(token);
  11. } catch (RemoteException ex) {
  12. }
  13. }
  14. }

After receiving the pause message from ActivityA, AMS will wake up ActivityB, and the entry is still resumeTopActivityLocked, wake up B, and then stop A further. At this time, the scene preservation is involved.

ActivityStack

  1. private final void completePauseLocked() {
  2.    
  3. if (!mService.isSleeping()) {
  4. resumeTopActivityLocked(prev);
  5. } else {
  6.     
  7. ...
  8.     

This article does not care how ActivityB is started. We only care about how ActivityA saves the scene. After ActivityB starts, it stops ActivityA through stopActivityLocked of ActivityStack.

  1. private final void stopActivityLocked(ActivityRecord r) {
  2. ...
  3. if (mMainStack) {
  4.               
  5. r.app.thread.scheduleStopActivity(r.appToken, r.visible, r.configChangeFlags);
  6. ...
  7. }

Looking back at the APP side, let's look at the call in ActivityThread: First, callActivityOnSaveInstanceState to save the scene to the Bundle.

  1. private void performStopActivityInner(ActivityClientRecord r,
  2. StopInfo info, boolean keepShown, boolean saveState) {
  3. ...
  4. // Next have the activity save its current state and managed dialogs...
  5. if (!r.activity.mFinished && saveState) {
  6. if (r.state == null ) {
  7. state = new Bundle();
  8. state.setAllowFds( false );
  9. mInstrumentation.callActivityOnSaveInstanceState(r.activity, state);
  10. r.state = state;
  11. . . .
  12. }

Afterwards, ActivityManagerNative.getDefault().activityStopped is used to notify AMS that the Stop action is complete. When notifying, the saved on-site data will also be brought over.

  1. private static class StopInfo implements Runnable {
  2. ActivityClientRecord activity;
  3. Bundle state;
  4. Bitmap thumbnail;
  5. CharSequence description;
  6.  
  7. @Override public void run() {
  8. // Tell activity manager we have been stopped.
  9. try {
  10.  
  11. ActivityManagerNative.getDefault().activityStopped(
  12. activity.token, state, thumbnail, description);
  13. } catch (RemoteException ex) {
  14. }
  15. }
  16. }

Through the above process, AMS not only starts a new Activity, but also saves the scene of the previous Activity. Even if the previous Activity is killed for various reasons, AMS can still know how to wake up the Activity and restore it during the rollback or re-awakening process.

Now we need to solve two problems: 1. How to save the scene, 2. How does AMS determine whether the APP or Activity is killed abnormally? Then the last question is how does AMS restore the APP or Activity that was killed abnormally.

Recovery logic when the entire Application is killed in the background

In fact, when explaining how AMS determines whether the APP or Activity is killed by an exception, the logic of recovery has been involved. We also know that once AMS knows that the APP is killed by the background, it is not a normal resume process, but a relaucher. Let's first look at how the entire APP is handled when it is killed. Look at the resumeTopActivityLocked part. From the above analysis, it is known that in this scenario, the Binder communication will throw an exception and take the exception branch, as follows:

  1. final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
  2. ....
  3. if ( next .app != null && next .app.thread != null ) {
  4. if (DEBUG_SWITCH) Slog.v(TAG, "Resume running: " + next );
  5. ...
  6. try {
  7. ...
  8. } catch (Exception e) {
  9. // Whoops, need to restart this activity!
  10. Here is to know that the entire app is killed
  11. Slog.i(TAG, "Restarting because process died: " + next );
  12. next .state = lastState;
  13. mResumedActivity = lastResumedActivity;
  14. Slog.i(TAG, "Restarting because process died: " + next );
  15.                
  16. startSpecificActivityLocked( next , true , false );
  17. return   true ;
  18. }

From the above code, we can know that it is actually startSpecificActivityLocked, which is not much different from the first time you call up the APP from the desktop. There is only one thing to note, that is, the Activity started at this time has the last scene data passed, because the last time when it retreated to the background, all the scenes of the Activity interface were saved and passed to AMS, then this time the recovery startup will return this data to ActivityThread, let's take a closer look at the special processing code for recovery in performLaunchActivity:

  1. private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
  2. ActivityInfo aInfo = r.activityInfo;
  3. Activity activity = null ;
  4. try {
  5. java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
  6. activity = mInstrumentation.newActivity(
  7. cl, component.getClassName(), r.intent);
  8. StrictMode.incrementExpectedActivityCount(activity.getClass());
  9. r.intent.setExtrasClassLoader(cl);
  10. if (r.state != null ) {
  11. r.state.setClassLoader(cl);
  12. }
  13. } catch (Exception e) {
  14. ...
  15. }
  16. try {
  17. Application app = r.packageInfo.makeApplication( false , mInstrumentation);
  18. ...
  19. Key Point 1
  20. mInstrumentation.callActivityOnCreate(activity, r.state);
  21. ...
  22. r.activity = activity;
  23. r.stopped = true ;
  24. if (!r.activity.mFinished) {
  25. activity.performStart();
  26. r.stopped = false ;
  27. }
  28. Key Point 1
  29. if (!r.activity.mFinished) {
  30. if (r.state != null ) {
  31. mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
  32. }
  33. }
  34. if (!r.activity.mFinished) {
  35. activity.mCalled = false ;
  36. mInstrumentation.callActivityOnPostCreate(activity, r.state);
  37. ...
  38.  
  39. }

Let's look at key points 1 and 2. First, let's look at key point 1. mInstrumentation.callActivityOnCreate will call back onCreate of Activity. This function actually mainly performs some Fragment recovery work for FragmentActivity. r.state in ActivityClientRecord is what AMS transmits to APP to restore the scene. When it starts normally, these are null. Let's look at key point 2. When r.state != null is not empty, mInstrumentation.callActivityOnRestoreInstanceState is executed. This function is mainly used to restore Window by default, such as restoring the previous display position of ViewPager, etc. It can also be used to restore user saved data.

The application is not killed by the background, and the activity is killed and restored

Turning on the developer mode "Do not retain activities" is such a scenario. In the above analysis, we know that when AMS actively kills the Activity abnormally, the app field of AcitivityRecord is set to empty. Therefore, resumeTopActivityLocked is different from the entire APP being killed, and the following branch will be taken

  1. final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
  2. ...
  3.          
  4. if ( next .app != null && next .app.thread != null ) {
  5. ...
  6.          
  7. } else {
  8. Key point 1 is just restarting the Activity, which shows that the process is not dead.
  9. // Whoops, need to restart this activity!
  10.          
  11. startSpecificActivityLocked( next , true , true );
  12. }
  13.  
  14. return   true ;
  15. }

Although they are slightly different, they still follow the startSpecificActivityLocked process, but they do not create a new APP process. The rest are the same and will not be explained here. At this point, we should understand.

  • How does Android save the scene in case of prevention?
  • How does AMS know if the app is killed in the background?
  • How does AMS rebuild the scene when the app is killed based on ActivityStack?

At this point, the logic of ActivityManagerService restoring the APP scene should be explained. Let me mutter some more questions, which may be some interview points.

  • The difference between actively clearing recent tasks and abnormal killing: Is ActivityStack cleared normally?
  • Why is it restored in reverse order when restoring? Because this is the order of the stack in the HistoryRecord in the ActivityStack, strictly according to the AMS end
  • In one sentence, the principle of Android background killing and recovery is summarized as follows: the application process is killed, but the scene is saved by AMS, and AMS can restore the application scene based on the saved scene.

<<:  Android Loader Detailed Explanation

>>:  50 Tips, Tricks, and Resources for Android Developers

Recommend

How to create a hit product through seed user operations?

Seed users generally have an open and adventurous...

How fast is the Earth moving on a cosmic scale?

The earth, this blue planet, not only dances on i...

Will this year be the hottest?

The World Meteorological Organization (WMO) relea...

Tutorial: How to reduce the size of iOS app?

Q: How can I make my program installation package...

A complete guide to online event planning and promotion!

When it comes to online event planning , the most...

App promotion|Any product is a business

1. What is the product? At the end of the day, th...

High-conversion information flow account building routine, just use it directly!

Recently, a friend left a message to complain: Th...