views:

291

answers:

4

I have investigated this problem for months now, came up with different solutions to it, which I am not happy with since they are all massive hacks. I still cannot believe that a class that flawed in design made it into the framework and no-one is talking about it, so I guess I just must be missing something.

The problem is with AsyncTask. According to the documentation it

"allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers."

The example then continues to show how some exemplary showDialog() method is called in onPostExecute(). This, however, seems entirely contrived to me, because showing a dialog always needs a reference to a valid Context, and an AsyncTask must never hold a strong reference to a context object.

The reason is obvious: what if the activity gets destroyed which triggered the task? This can happen all the time, e.g. because you flipped the screen. If the task would hold a reference to the context that created it, you're not only holding on to a useless context object (the window will have been destroyed and any UI interaction will fail with an exception!), you even risk creating a memory leak.

Unless my logic is flawed here, this translates to: onPostExecute() is entirely useless, because what good is it for this method to run on the UI thread if you don't have access to any context? You can't do anything meaningful here.

One workaround would be to not pass context instances to an AsyncTask, but a Handler instance. That works: since a Handler loosely binds the context and the task, you can exchange messages between them without risking a leak (right?). But that would mean that the premise of AsyncTask, namely that you don't need to bother with handlers, is wrong. It also seems like abusing Handler, since you are sending and receiving messages on the same thread (you create it on the UI thread and send through it in onPostExecute() which is also executed on the UI thread).

To top it all off, even with that workaround, you still have the problem that when the context gets destroyed, you have no record of the tasks it fired. That means that you have to re-start any tasks when re-creating the context, e.g. after a screen orientation change. This is slow and wasteful.

My solution to this (as implemented in the Droid-Fu library) is to maintain a mapping of WeakReferences from component names to their current instances on the unique application object. Whenever an AsyncTask is started, it records the calling context in that map, and on every callback, it will fetch the current context instance from that mapping. This ensures that you will never reference a stale context instance and you always have access to a valid context in the callbacks so you can do meaningful UI work there. It also doesn't leak, because the references are weak and are cleared when no instance of a given component exists anymore.

Still, it is a complex workaround and requires to sub-class some of the Droid-Fu library classes, making this a pretty intrusive approach.

Now I simply want to know: Am I just massively missing something or is AsyncTask really entirely flawed? How are your experiences working with it? How did you solve these problem?

Thanks for your input.

+2  A: 

I'm not sure it's true that you risk a memory leak with a reference to a context from an AsyncTask.

The usual way of implementing them is to create a new AsyncTask instance within the scope of one of the Activity's methods. So if the activity is destroyed, then once the AsyncTask completes won't it be unreachable and then eligible for garbage collection? So the reference to the activity won't matter because the AsyncTask itself won't hang around.

oli
true -- but what if the task blocks indefinitely? Tasks are meant to perform blocking operations, maybe even ones that never terminate. There you have your memory leak.
Matthias
what kind of task? example?
Pentium10
Any worker that performs something in an endless loop, or anything that just locks up e.g. on an I/O operation.
Matthias
A: 

Personally, I just extend Thread and use a callback interface to update the UI. I could never get AsyncTask to work right without FC issues. I also use a non blocking queue to manage the execution pool.

androidworkz
Well, your force close was probably because of the problem I mentioned: you tried referencing a context which had gone out of scope (i.e. its window had been destroyed), which will result in a framework exception.
Matthias
No... actually it was because the queue sucks that is built in to AsyncTask. I always use getApplicationContext(). I don't have problems with AsyncTask if it's only a few operations... but I am writing a media player that updates album art in the background... in my test I have 120 albums with no art... so, while my app didn't close all of the way, asynctask was throwing errors... so instead I built a singleton class with a queue that manages the processes and so far it works great.
androidworkz
A: 

The reason is obvious: what if the activity gets destroyed which triggered the task?

Manually disassociate the activity from the AsyncTask in onDestroy(). Manually re-associate the new activity to the AsyncTask in onCreate(). This requires either a static inner class or a standard Java class, plus perhaps 10 lines of code.

CommonsWare
Be careful with static references -- I have seen objects being garbage collected even though there were static strong references to them. Maybe a side-effect of Android's class loader, or a bug even, but static references are no safe way of exchanging state across an activity life-cycle. The app object is, however, that's why I use that.
Matthias
@Matthias: I did not say to use static references. I said to use a static inner class. There is a substantial difference, despite both having "static" in their names.
CommonsWare
I would like to see this in code. Maybe someone can turn into code the idea.
Pentium10
@Pentium10: http://github.com/commonsguy/cw-android/tree/master/Rotation/RotationAsync/
CommonsWare
I see -- they key here is getLastNonConfigurationInstance(), not the static inner class though. A static inner class keeps no implicit reference to its outer class so it's semantically equivalent to a plain public class. Just a warning: onRetainNonConfigurationInstance() is NOT guaranteed to be called when an activity is interrupted (an interruption can be a phone call, too), so you'd have to parcel your Task in onSaveInstanceState(), too, for a truly solid solution. But still, nice idea.
Matthias
Um... onRetainNonConfigurationInstance() is always called when the activity is in the process of being destroyed and re-created. It makes no sense to call at other times. If a switch to another activity happens, the current activity is paused/stopped, but it is not destroyed, so the async task can continue running and using the same activity instance. If it finishes and say displays a dialog, the dialog will correctly display as part of that activity and thus not show to the user until they return to the activity. You can't put AsyncTask in a Bundle.
hackbod
Aha, that's contrary then to what Mark Murphy said (http://groups.google.com/group/android-developers/browse_thread/thread/703219aac8a55c12/4955437febff7395?show_docid=4955437febff7395). He said one must not rely on this method to be called, and one must always use both onSaveInstanceState() and onRetainLast...() since the latter is an optimization and is not guranteed to be called (that's also what the JavaDoc said). So can I assume that is wrong then?
Matthias
+4  A: 

How about something like this (warning not tested):

class MyActivity extends Activity {
    Working mCurWorker;

    static class Worker extends AsyncTask<URL, Integer, Long> {
        MyActivity mActivity;

        Worker(MyActivity activity) {
            mActivity = activity;
        }

        @Override
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
            }
            return totalSize;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mActivity != null) {
                mActivity.setProgressPercent(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(Long result) {
            if (mActivity != null) {
                mActivity.showDialog("Downloaded " + result + " bytes");
            }
        }
    }

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

        mWorker = (Worker)getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = this;
        }

        ...
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new Worker(this);
        mWorker.execute(...);
    }
}
hackbod
No -- mActivity will always be != null, since you're holding a strong reference to it so it won't be eligible for garbage collection. But even though it's non-null it doesn't have to be valid: if the window has been destroyed which was bound to this context, then this object is worthless, since you're trying to do UI ops on a window that doesn't exist anymore.
Matthias
Also, this is exactly how you create a memory leak if your task doesn't terminate.
Matthias
You're arguing with hackbod over that? Seriously?! Anyway - yes, mActivity will be != null, but if there's no references to your Worker instance, then any references that instance will be subject to garbage removal as well. If your task does run forever, then you have a memory leak anyway (your task) - not to mention that you're draining the phone battery. Besides, as mentioned elsewhere, you can set mActivity to null in onDestroy.
EboMike
The onDestroy() method sets mActivity to null. It doesn't matter who holds a reference to the activity before that, because it is still running. And the activity's window will always be valid until onDestroy() is called. By setting to null there, the async task will know that the activity is no longer valid. (And when a config changes, the previous activity's onDestroy() is called and the next one's onCreate() run without any messages on the main loop processed between them so the AsyncTask will never see an inconsistent state.)
hackbod
true, but it still doesn't solve the last problem I mentioned: imagine the task downloads something off the internet. Using this approach, if you flip the screen 3 times while the task is running, it will be restarted with every screen rotation, and every task except the last throws its result away because its activity reference is null.
Matthias
one more question: the API docs for onReatainNonConfigurationInstance() read "This function is called purely as an optimization, and you must not rely on it being called." But your code relies on it being called. Is that a problem? In which cases will that method not be called?
Matthias
No it won't be restarted. The current AsyncTask is propagated to the new Activity instance via onRetainNonConfigurationInstance().
hackbod
Your code isn't *relying* on it being called -- that is, if the app is entirely restarted (process went away), you are launched without the async task. You are using it as an optimization to get to keep the same async task running across activity instances.
hackbod
I see, thanks. What if I need to access the context in doInBackground()? Do I then need to declare the activity reference as volatile, since doInBackground() will be called in the worker thread?
Matthias
To access in the background, you would either need to put appropriate synchronization around mActivity *and* deal with running into times when it is null, or have the background thread just take the Context.getApplicationContext() which is a single global instance for the app. The application context is restricted in what you can do (no UI like Dialog for example) and requires some care (registered receivers and service bindings will be left forever if you don't clean them up), but is generally appropriate for code that isn't tied to a particular component's context.
hackbod
That was incredibly helpful, thanks Dianne! I wish the documentation would be as good in the first place.
Matthias