Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can an AsyncTask still use an Activity if the user has already navigated away from it?

On Android you can do work in a separate Thread for example by using a Runnable or AsyncTask. In both cases you might need to do some work after the work is done, for example by overriding onPostExecute() in the AsyncTask. However the user might navigate away or close the app while work is being done in the background.

My question is: What happens if the user navigates away or closes the app while I still have a reference to the Activity the user just closed in my AsyncTask?

My guess is that it should be destroyed as soon as the user navigates away, however when I test it out on a device for some reason I can still call methods on the Activity even though it is already gone! What is going on here?

like image 833
Fattum Avatar asked Jan 29 '16 14:01

Fattum


1 Answers

Simple answer: You have just discovered

Memory Leaks

As long as some part of the app like an AsyncTask still holds a reference to the Activity it will not be destroyed. It will stick around until the AsyncTask is done or releases its reference in some other way. This can have very bad consequences like your app crashing, but the worst consequences are the ones you don't notice: your app may keep reference to Activities which should have been released ages ago and each time the user does whatever leaks the Activity the memory on the device might get more and more full until seemingly out of nowhere Android kills your app for consuming too much memory. Memory leaks are the single most frequent and worst mistakes I see in Android questions on Stack Overflow


The solution

Avoiding memory leaks however is very simple: Your AsyncTask should never have a reference to an Activity, Service or any other UI component.

Instead use the listener pattern and always use a WeakReference. Never hold strong references to something outside the AsyncTask.


A few examples

Referencing a View in an AsyncTask

A correctly implemented AsyncTask which uses an ImageView could look like this:

public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {

    private final WeakReference<ImageView> mImageViewReference;

    public ExampleTask(ImageView imageView) {
        mImageViewReference = new WeakReference<>(imageView);
    }

    @Override
    protected Bitmap doInBackground(Void... params) {
        ...
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);

        final ImageView imageView = mImageViewReference.get();
        if (imageView != null) {
            imageView.setImageBitmap(bitmap);
        }
    }
}

This illustrates perfectly what a WeakReference does. WeakReferences allow for the Object they are referencing to be garbage collected. So in this example We create a WeakReference to an ImageView in the constructor of the AsyncTask. Then in onPostExecute() which might be called 10 seconds later when the ImageView does not exist anymore we call get() on the WeakReference to see if the ImageView exists. As long as the ImageView returned by get() is not null then the ImageView has not been garbage collected and we can therefore use it without worry! Should in the meantime the user quit the app then the ImageView becomes eligible for garbage collection immediately and if the AsyncTask finishes some time later it sees that the ImageView is already gone. No memory leaks, no problems.


Using a listener

public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {

    public interface Listener {
        void onResult(Bitmap image);
    }

    private final WeakReference<Listener> mListenerReference;

    public ExampleTask(Listener listener) {
        mListenerReference = new WeakReference<>(listener);
    }

    @Override
    protected Bitmap doInBackground(Void... params) {
        ...
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);

        final Listener listener = mListenerReference.get();
        if (listener != null) {
            listener.onResult(bitmap);
        }
    }
}

This looks quite similar because it actually is quite similar. You can use it like this in an Activity or Fragment:

public class ExampleActivty extends AppCompatActivity implements ExampleTask.Listener {

    private ImageView mImageView;

    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        new ExampleTask(this).execute();
    }

    @Override
    public void onResult(Bitmap image) {
        mImageView.setImageBitmap(image);
    }
} 

Or you can use it like this:

public class ExampleFragment extends Fragment {

    private ImageView mImageView;

    private final ExampleTask.Listener mListener = new ExampleTask.Listener() {

        @Override
        public void onResult(Bitmap image) {
            mImageView.setImageBitmap(image);   
        }
    };

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        new ExampleTask(mListener).execute(); 
    }

    ...
}

WeakReference and its consequences when using a listener

However there is another thing you have to be aware of. A consequence of only having a WeakReference to the listener. Imagine you implement the listener interface like this:

private static class ExampleListener implements ExampleTask.Listener {

    private final ImageView mImageView;

    private ExampleListener(ImageView imageView) {
        mImageView = imageView;
    }

    @Override
    public void onResult(Bitmap image) {
        mImageView.setImageBitmap(image);
    }
}

public void doSomething() {
   final ExampleListener listener = new ExampleListener(someImageView);
   new ExampleTask(listener).execute();
}

Quite an unusual way to do this - I know - but something similar might sneak into your code somewhere without you knowing it and the consequences can be difficult to debug. Have you noticed by now what might be wrong with the above example? Try figuring out, otherwise continue reading below.

The problem is simple: You create an instance of the ExampleListener which contains your reference to the ImageView. Then you pass it into the ExampleTask and start the task. And then the doSomething() method finishes, so all local variables become eligible for garbage collection. There is no strong reference left to the ExampleListener instance you passed into the ExampleTask, there is just a WeakReference. So the ExampleListener will be garbage collected and when the ExampleTask finishes nothing will happen. If the ExampleTask executes fast enough the garbage collector might not have collected the ExampleListener instance yet, so it may work some of the time or not at all. And debugging issues like this can be a nightmare. So the moral of the story is: Always be aware of your strong and weak references and when objects become eligible for garbage collection.


Nested classes and using static

Another thing which probably is the cause of most memory leaks I see on Stack Overflow people using nested classes in the wrong way. Look at the following example and try to spot what causes a memory leak in the following example:

public class ExampleActivty extends AppCompatActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        final ImageView imageView = (ImageView) findViewById(R.id.image);
        new ExampleTask(imageView).execute();
    }

    public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {

        private final WeakReference<ImageView> mListenerReference;

        public ExampleTask(ImageView imageView) {
            mListenerReference = new WeakReference<>(imageView);
        }

        @Override
        protected Bitmap doInBackground(Void... params) {
            ...
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);

            final ImageView imageView = mListenerReference.get();
            if (imageView != null) {
                imageView.setImageAlpha(bitmap);
            }
        }
    }
}

Do you see it? Here is another example with the exact same problem, it just looks different:

public class ExampleActivty extends AppCompatActivity {


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        final ImageView imageView = (ImageView) findViewById(R.id.image);
        final Thread thread = new Thread() {

            @Override
            public void run() {
                ...
                final Bitmap image = doStuff();
                imageView.post(new Runnable() {
                    @Override
                    public void run() {
                        imageView.setImageBitmap(image);
                    }
                });
            }
        };
        thread.start();
    }
}

Have you figured out what the problem is? I daily see people carelessly implementing stuff like above probably without knowing what they are doing wrong. The problem is a consequence of how Java works based on a fundamental feature of Java - there is no excuse, people who implement things like above are either drunk or don't know anything about Java. Let's simplify the problem:

Imagine you have a nested class like this:

public class A {

    private String mSomeText;

    public class B {

        public void doIt() {
            System.out.println(mSomeText);
        }
    }
}

When you do that you can access members of the class A from inside the class B. That's how doIt() can print mSomeText, it has access to all the members of A even private ones.
The reason you can do that is that if you nest classes like that Java implicitly creates a reference to A inside of B. It is because of that reference and nothing else that you have access to all members of A inside of B. However in the context of memory leaks that again poses a problem if you don't know what you are doing. Consider the first example (I'll strip all the parts that don't matter from the example):

public class ExampleActivty extends AppCompatActivity {

    public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
        ...
    }
}

So we have an AsyncTask as a nested class inside an Activity. Since the nested class is not static we can access members of the ExampleActivity inside the ExampleTask. It doesn't matter here that ExampleTask doesn't actually access any members from the Activity, since it is a non static nested class Java implicitly creates a reference to the Activity inside the ExampleTask and so with seemingly no visible cause we have a memory leak. How can we fix this? Very simple actually. We just need to add one word and that is static:

public class ExampleActivty extends AppCompatActivity {

    public static class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
        ...
    }
}

Just this one missing keyword on a simple nested class is the difference between a memory leak and completely fine code. Really try to understand the issue here, because it is at the core of how Java works and understanding this is crucial.

And as for the other example with the Thread? The exactly same issue, anonymous classes like that are also just non static nested classes and immediately a memory leak. However it is actually a million times worse. From every angle you look at it that Thread example is just terrible code. Avoid at all costs.


So I hope these example helped you understand the problem and how to write code free of memory leaks. If you have any other questions feel free to ask.

like image 112
Xaver Kapeller Avatar answered Nov 16 '22 02:11

Xaver Kapeller