cdriver
cdriver

Reputation: 155

IntentService prevents activity from destroying

As far as i know, when i rotate the screen, the activity gets destroyed and recreated. In the following scenario, it looks like this is not always the case. So the question is: Is it really destroyed? Am i leaking memory for some reason? If it is not destroyed, does it run on the same thread as the newly created activity?

The scenario is an "vertical" Activity that starts an IntentService, the service takes 5 secs to complete, and uses a ResultReceiver to send back the result. During those 5 secs, the screen gets rotated, and a new "horizontal" activity gets created. The result gets back to the "vertical" activity, not the new "horizontal". So, the "vertical" activity is not destroyed?

I've created a project to demonstrate this. Here comes the code.

First, the xml with the activity layout. Just two buttons.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Show value of variable" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start IntentService" />

</LinearLayout>

After that, our IntentService

public class SomeService extends IntentService {

    public SomeService(){
        super("SomeService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        ResultReceiver receiver = (ResultReceiver) intent.getParcelableExtra("receiver");

        long t0, t1;
        t0 = System.currentTimeMillis();
        do {
            t1 = System.currentTimeMillis();
        } while (t1-t0 < 5000);

        receiver.send(0, null);
    }
}

And finally the activity

public class TestActivityDestructionActivity extends Activity {

    private int mTestVar = 0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        if (savedInstanceState != null){
            mTestVar = savedInstanceState.getInt("tv");
        }

        mTestVar++;

        Button b1 = (Button) findViewById(R.id.button1);
        b1.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Toast.makeText(TestActivityDestructionActivity.this, "Value: " + mTestVar, Toast.LENGTH_SHORT).show();
            }
        });

        Button b2 = (Button) findViewById(R.id.button2);
        b2.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Intent intent = new Intent(TestActivityDestructionActivity.this, SomeService.class);
                intent.putExtra("receiver", new SomeReceiver(new Handler()));
                startService(intent);
                Toast.makeText(TestActivityDestructionActivity.this, "IntentService started", Toast.LENGTH_SHORT).show();
            }
        });

    }

    private class SomeReceiver extends ResultReceiver{
        public SomeReceiver(Handler handler) {
            super(handler);
        }

        @Override
        protected void onReceiveResult(int resultCode, Bundle resultData) {
            super.onReceiveResult(resultCode, resultData);
            Toast.makeText(TestActivityDestructionActivity.this, "Receiver value: " + mTestVar, Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("tv", mTestVar);
    }
}

When the activity starts the first time, mTestVar is 0, and onCreate it is inceased to 1. When the activity gets destroyed, it saves the value. The next activity loads it and increases it to 2, and so on.

Following these steps:

  1. Click Button1 : mTestVar is 1
  2. Click Button2 : Service gets started
  3. Rotate screen before 5 secs elapse
  4. Click Button1 : mTestVar is 2 (the onCreate was called)
  5. Wait for the 5 secs to elapse : mTestVar is shown as still having a value of 1

As far as i can see, that means that the first activity was not destroyed. Am i missing something?

Thanks!

EDIT

After seeing the answer to this question, i used MAT and heap dumps and found out that the first activity is never Garbage Collected, even though the service is long gone and garbage collected and there is no reference to the activity anymore. I don't know why.

However, if the receiver is never created and never passed to the service, there is no memory leak. The first activity is destroyed as expected. There is no reason not to, anyway.

Upvotes: 1

Views: 1573

Answers (1)

accordionfolder
accordionfolder

Reputation: 574

Run it through the debugger with breakpoints in your code and check the id of "this" in the ResultReceiver. It's keeping a local copy of "this" which points to your previous activity, where int mTestVar == 1. The current active Activity is a whole different object, with it's own copy of mTestVar == 2. This is a memory leak.

To answer your question more concisely: Android "destroyed" you activity, but you kept a reference to the Activity in your callback preventing it from being garbage collected. If you want something to "live" across multiple activities you should use a service to store that state (keep in mind a service is NOT a separate thread and should not be used like one).

A side note: When checking variables and general purpose logging it's best to use Log.v/e/i etc. You can filter them at the command line with adb logcat mytag:V *:S or use the logcat built into eclipse.

Upvotes: 3

Related Questions