ActivityLifecycleCallbacks are not triggered when activity is killed through “Don't keep activities”

In my Android app I have two activities:

  • DemoActivity with a button to start the SearchActivity with an Intent
  • SearchActivity

The button is a custom ViewGroup:

  • Confused about testCompile and androidTestCompile in Android Gradle
  • Prevent status bar for appearing android (modified)
  • Volley seems not working after ProGuard obfuscate
  • Modify AIRPLANE_MODE_ON on Android 4.2 (and above)
  • How to get Android system boot time
  • Streaming to VideoView only plays on Wifi when using Samsung phones
    • SearchButton

    As soon as the SearchButton comes to life it registers for lifecycle events (of the corresponding SearchActivity):

    public class SearchButton extends CardView implements 
        Application.ActivityLifecycleCallbacks {
    
        @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext instanceof Application) {
                ((Application) applicationContext)
                    .registerActivityLifecycleCallbacks(this);
            }
        }
    
    // ...
    

    The events are consumed as follows:

    // ...
    
        @Override
        public void onActivityStarted(Activity activity) {
            if (activity instanceof SearchActivity) {
                SearchActivity searchActivity = (SearchActivity) activity;
                searchActivity.addSomeListener(someListener);
            }
        }
    
        @Override
        public void onActivityStopped(Activity activity) {
            if (activity instanceof SearchActivity) {
                SearchActivity searchActivity = (SearchActivity) activity;
                searchActivity.removeSomeListener(someListener);
            }
        }
    

    Once the SearchActivity has been launched I put the app into background and get it back into foreground. The following call stack can be seen:

    1. SearchButton.onActivityStarted // triggered by DemoActivity
    2. DemoActivity.onStart
    3. SearchButton.onActivityStarted // triggered by SearchActivity
    4. SearchActivity.addSomeListener
    5. SearchActivity.onStart
    

    As you can see the listener is added. This works fine.


    The problem

    As soon as I enable Don't keep activities in the developer options the call stack looks like this when I get the app foreground again:

    1. DemoActivity.onCreate
    2. SearchButton.init // Constructor
    3. DemoActivity.onStart
    4. SearchActivity.onStart
    5. SearchButton.onAttachedToWindow
    6. DemoApplication.registerActivityLifecycleCallbacks
    

    Here the listener is not added. The desired onActivityStarted callback triggered by SearchActivity.onStart is missing.

    Related posts:

    No ActionBar in PreferenceActivity after upgrade to Support Library v21
    (Smooth)ScrollToPosition doesn't work properly with RecyclerView
    how can I use Android dexOptions?
    Invalid project description when importing project into Eclipse
    How to preview xml drawable?
    Is it possible to set a background color for the icon in the notification drawer on Android if using...
  • view on press onpress: Change background color on press? How do I show that the View is being pressed?
  • Authentication Error when using HttpPost with DefaultHttpClient on Android
  • Android - Try to send fake sms to myself without mobile network usage
  • When to close cursor in Android?
  • “Starting emulator for AVD” then Panic: could not open…"
  • Slow loading of layout
  • One Solution collect form web for “ActivityLifecycleCallbacks are not triggered when activity is killed through “Don't keep activities””

    Short answer

    You’re seeing the onStart calls from the view only when the activity has been brought to the foreground after being in the background for some time. Currently it’s impossible to see earlier activity events from your activity’s views since the view hierarchy is still being created and the views are not attached to the window yet.

    When an activity is initialised from scratch, the view hierarchy isn’t fully attached until after onResume. This means that once your view’s onAttachedToWindow is called, onStart has already been executed. If you exit the activity you mentioned in the question, you should still see the events for onPause and so on.

    Normally if you put an activity to the background by pressing the home button for example, the activity is stopped but not destroyed. It stays in memory with its view hierarchy if there are adequate system resources to do so. When the activity is restored to the foreground, instead of creating it from scratch it calls onStart and resumes from where it left off, without recreating the view hierarchy.

    The “Don’t keep activities” option makes sure that each activity is destroyed right away after it leaves the foreground, making sure that your view’s onAttachedToWindow is always called after onResume since the view hierarchy needs to be recreated every time.

    What you could do instead

    Without sharing more code it’s not immediately clear why you need to set the listener within the view. It seems that you need to listen to the activity’s lifecycle method in any case.

    If the listener is only tied to the activity’s lifecycle, you might be able to extract it completely out from the view and into the activity.

    If it’s tied both to view’s and the activity’s lifecycle, you could try registering the activity lifecycle callbacks in the constructor of the view since a context is already available at that point.

    Alternatively you could go for the solution that Google Maps currently has e.g. in MapView. It requires the activity to proxy all lifecycle methods to the view. This might be useful in case your view is very tightly knit with the activity’s lifecycle. You can see the documentation here.

    A fourth option is to use a fragment instead of a view since it has its own set of lifecycle methods. Personally I don’t feel quite comfortable with fragments since their lifecycles are potentially even more complicated.

    Longer answer

    To explain why this is happening, we need to delve into Android’s source code. The things I’m explaining here are specific to this implementation and might differ between SDK versions and even between Android devices due to the manufacturer’s changes. You should not rely on these details in your code. I’ll be using the SDK 23 source code that ships with Android Studio and a Nexus 6P with build MTC19T.

    The easiest place to start investigating is the onAttachedToWindow method. When is it actually called? Its documentation says that it’s called after the view’s surface is created for drawing, but we’re not satisfied with that.

    To find out, we set a breakpoint to a view, restart the app so that the activity is recreated, and investigate the first few frames in Android Studio:

    "main@4092" prio=5 runnable
      java.lang.Thread.State: RUNNABLE
          at com.lnikkila.callbacktest.TestView.onAttachedToWindow(TestView.java:18)
          at android.view.View.dispatchAttachedToWindow(View.java:14520)
          at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:2843)
          at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1372)
          at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1115)
          at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6023)
          at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
          at android.view.Choreographer.doCallbacks(Choreographer.java:670)
          at android.view.Choreographer.doFrame(Choreographer.java:606)
          at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
          at android.os.Handler.handleCallback(Handler.java:739)
          at android.os.Handler.dispatchMessage(Handler.java:95)
          at android.os.Looper.loop(Looper.java:148)
          at android.app.ActivityThread.main(ActivityThread.java:5422)
          ...
    

    We can see that the first frames are from the view’s internal logic, from the parent ViewGroup, from something called a ViewRootImpl, and then from some callbacks from Choreographer and Handler.

    We’re not sure what created those callbacks, but the closest callback implementation is named ViewRootImpl$TraversalRunnable so we’ll check that out:

        final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
        final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    

    There’s the definition, and right below is the callback instance that’s given to Choreographer in this method:

        void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    Choreographer is something that runs on every UI thread on Android. It’s used to synchronise events with the display’s frame rate. One reason to use it is to avoid wasting processing power by drawing things faster than what the display can show.

    Since Choreographer uses the thread’s message queue, we couldn’t see this call in the previous frames because the call wasn’t made until Looper handled the message. We can set a breakpoint to this method to see where this call is coming from:

    "main@4091" prio=5 runnable
      java.lang.Thread.State: RUNNABLE
          at android.view.ViewRootImpl.scheduleTraversals(ViewRootImpl.java:1084)
          at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:913)
          at android.view.ViewRootImpl.setView(ViewRootImpl.java:526)
          - locked <0x100a> (a android.view.ViewRootImpl)
          at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
          at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
          at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3169)
          at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481)
          at android.app.ActivityThread.-wrap11(ActivityThread.java:-1)
          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
          at android.os.Handler.dispatchMessage(Handler.java:102)
          at android.os.Looper.loop(Looper.java:148)
          at android.app.ActivityThread.main(ActivityThread.java:5422)
          ...
    

    If we look into ActivityThread’s handleLaunchActivity, there’s the call to handleResumeActivity. Before that is a call to performLaunchActivity, and in that method are calls to Instrumentation#callActivityOnCreate, Activity#performStart and so on.

    So there we have our proof that the views aren’t attached until after onResume.

    Android Babe is a Google Android Fan, All about Android Phones, Android Wear, Android Dev and Android Games Apps and so on.