Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • 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.

like image 771
JJD Avatar asked Oct 06 '16 12:10

JJD


People also ask

Which method is always fired whenever an activity is killed or pushed into the background?

onPause (): Called as part of the activity lifecycle when an activity is going into the background, but has not (yet) been killed. The counterpart to onResume().

Is called before the activity is destroyed by the system?

onDestroy() is called before the activity is destroyed.


1 Answers

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.

like image 96
Leo Nikkilä Avatar answered Sep 24 '22 02:09

Leo Nikkilä