Android - Thread pool strategy and can Loader be used to implement it?
Asked Answered
P

1

7

First the problem:

  • I'm working on the application that uses multiple FragmentLists within a customized FragmentStatePagerAdapter. There could be, potentially substantial number of such fragments say between 20 and 40.
  • Each fragment is a list in which each item could contain text or image.
  • The images need to be uploaded asynchronously from the web and cached to temp memory cache and also to SD if available
  • When Fragment goes off the screen any uploads and current activity should be cancelled (not paused)

My first implementation followed well known image loader code from Google. My problem with that code is that it basically creates one instance of AsyncTask per image. Which in my case kills the app real fast.

Since I'm using v4 compatibility package I thought that using custom Loader that extends AsyncTaskLoader would help me since that internally implements a thread pool. However to my unpleasant surprise if I execute this code multiple times each following invocation will interrupt the previous. Say I have this in my ListView#getView method:

getSupportLoaderManager().restartLoader(0, args, listener);

This method is executed in the loop for each list item that comes into view. And as I stated - each following invocation will terminate the previous one. Or at least that's what happen based on LogCat

11-03 13:33:34.910: V/LoaderManager(14313): restartLoader in LoaderManager: args=Bundle[{URL=http://blah-blah/pm.png}]
11-03 13:33:34.920: V/LoaderManager(14313):   Removing pending loader: LoaderInfo{405d44c0 #2147483647 : ImageLoader{405118a8}}
11-03 13:33:34.920: V/LoaderManager(14313):   Destroying: LoaderInfo{405d44c0 #2147483647 : ImageLoader{405118a8}}
11-03 13:33:34.920: V/LoaderManager(14313):   Enqueuing as new pending loader

Then I thought that maybe giving unique id to each loader will help the matters but it doesn't seem to make any difference. As result I end up with seemingly random images and the app never loads even 1/4 of what I need.

The Question

  • What would be the way to fix the Loader to do what I want (and is there a way?)
  • If not what is a good way to create AsyncTask pool and is there perhaps working implementation of it?

To give you idea of the code here's stripped down version of Loader where actual download/save logic is in separate ImageManager class.

    public class ImageLoader extends AsyncTaskLoader<TaggedDrawable> {
        private static final String TAG = ImageLoader.class.getName();
        /** Wrapper around BitmapDrawable that adds String field to id the drawable */
        TaggedDrawable img;
        private final String url;
        private final File cacheDir;
        private final HttpClient client;


    /**
     * @param context
     */
    public ImageLoader(final Context context, final String url, final File cacheDir, final HttpClient client) {
        super(context);
        this.url = url;
        this.cacheDir = cacheDir;
        this.client = client;
    }

    @Override
    public TaggedDrawable loadInBackground() {
        Bitmap b = null;
        // first attempt to load file from SD
        final File f = new File(this.cacheDir, ImageManager.getNameFromUrl(url)); 
        if (f.exists()) {
            b = BitmapFactory.decodeFile(f.getPath());
        } else {
            b = ImageManager.downloadBitmap(url, client);
            if (b != null) {
                ImageManager.saveToSD(url, cacheDir, b);
            }
        }
        return new TaggedDrawable(url, b);
    }

    @Override
    protected void onStartLoading() {
        if (this.img != null) {
            // If we currently have a result available, deliver it immediately.
            deliverResult(this.img);
        } else {
            forceLoad();
        }
    }

    @Override
    public void deliverResult(final TaggedDrawable img) {
        this.img = img;
        if (isStarted()) {
            // If the Loader is currently started, we can immediately deliver its results.
            super.deliverResult(img);
        }
    }

    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();
        // Ensure the loader is stopped
        onStopLoading();
        // At this point we can release the resources associated with 'apps'
        // if needed.
        if (this.img != null) {
            this.img = null;
        }

    }

}
Poker answered 3/11, 2011 at 20:39 Comment(12)
AsyncTask already uses a pool. The pool goes up to 128 threads IIRC, which may be the source of your difficulty. You can always implement your own thread pool using java.util.concurrent classes.Substance
If your development is targeting Android 3.0 (API Level 11), you can use newly added API AsyncTask.executeOnExecutor() fine control your thread pool with your AsyncTask creation life cycle.Propagate
AsynkTask nevertheless can only be executed once so I need create one instance per image I'm loading. make it 60 and that's a lot of objectsPoker
@Propagate I'm actually looking into ModernAsyncTask that comes with compatibility package (I'm not using A3). But I'm very interested if I can somehow use Loaders for thisPoker
You don't need create AsyncTask per each image. You can always pre-analyse your problem set and split/scale them into sub-set, for example, total 300 image download can be split into 6 chunks (with each download 50 image in sequence), then feed these pre-processed sub-sets into 6 AsyncTask.Propagate
@Propagate that's an interesting idea I will look into, thanksPoker
@CommonsWare. Mark - just to get a record straight on AsyncTask. When you create and AsyncTask#execute the task it is placed into task pool somewhere on UI thread, right? Would that be any/all tasks as long as concrete class extends AsyncTask?Poker
@DroidIn.net: "the task it is placed into task pool somewhere on UI thread, right?" -- tasks are not pooled. Threads are pooled. Tasks run on threads. AsyncTask maintains a static ScheduledExecutorService for its thread pool and task queue. Your task is added to the ScheduledExecutorService, which will dole it out to a thread (if there is one available) or put it in a LinkedBlockingQueue (IIRC) waiting for a thread to clear up. Actually, ModernAsyncTask from the support package is interesting -- you can use your own ScheduledExecutorService now with any API level.Substance
@CommonsWare. Sorry you right it's not a task but a thread on which it will run. My confusion comes from the fact that one has to create new AsyncTask every time something needs to be executed. API specifically states that AsyncTask#execute can only be called once. So the way I see it - somewhere there's pool of threads and when you attempt to do AsyncTask#execute one thread is yanked from pool and executes the task. Hence many AsyncTasks share some common pool. Is that the case?Poker
@DroidIn.net: I think you have it now. There is a default "common pool", and you can supply your own "common pool" via a setter if you would prefer to manage it yourself. For example, I would not want to download 128 images in parallel in 128 threads, which I think is what you'd get by executing 128 AsyncTask objects in rapid succession.Substance
You should read the accepted answerPoker
Thanks for comment. I read it before asking. Canceling tasks didn't work good with a lot of images and fragments. There was like 20 seconds delay. I combined canceling task and I created ThreadPoolExecutor in fragment. Running tasks on this executor works well.Disoblige
L
11

Ok, so first things first. The AsyncTask that comes with android shouldn't drown out your app or cause it to crash. AsyncTasks run in a thread pool where there is at most 5 threads actually executing at the same time. While you can queue up many tasks to be executed , only 5 of them are executing at a time. By executing these in the background threadpool they shouldn't have any effect on your app at all, they should just run smoothly.

Using the AsyncTaskLoader would not solve your problem if you are unhappy with the AsyncTask loader performance. The AsyncTaskLoader just takes the loader interface and marries it to an AsyncTask. So it's essentially mapping onLoadFinished -> onPostExecute, onStart -> onLoadInBackground. So it's the same exact thing.

We use the same image loader code for our app that causes an asynctask to be put onto the threadpool queue each time that we try to load an image. In google's example they associate the imageview with its async task so that they can cancel the async task if they try to reuse the imageview in some sort of adapter. You should take a similar strategy here. You should associate your imageview with the async task is loading the image in the background. When you have a fragment that is not showing you can then cycle through your image views associated with that fragment and cancel the loading tasks. Simply using the AsyncTask.cancel() should work well enough.

You should also try to implement the simple image caching mechanism the async image view example spells out. We simply create a static hashmap that goes from url -> weakreference . This way the images can be recycled when they need to be because they are only held on with a weak reference.

Here's an outline of the image loading that we do

public class LazyLoadImageView extends ImageView {
        public WeakReference<ImageFetchTask> getTask() {
        return task;
    }

    public void setTask(ImageFetchTask task) {
        this.task = new WeakReference<ImageFetchTask>(task);
    }

    private WeakReference<ImageFetchTask> task;

        public void loadImage(String url, boolean useCache, Drawable loadingDrawable){

        BitmapDrawable cachedDrawable = ThumbnailImageCache.getCachedImage(url);
        if(cachedDrawable != null){
            setImageDrawable(cachedDrawable);
            cancelDownload(url);
            return;
        }

        setImageDrawable(loadingDrawable);

        if(url == null){
            makeDownloadStop();
            return;
        }

        if(cancelDownload(url)){
            ImageFetchTask task = new ImageFetchTask(this,useCache);
            this.task = new WeakReference<ImageFetchTask>(task);
            task.setUrl(url);
            task.execute();
        }


        ......

        public boolean cancelDownload(String url){

        if(task != null && task.get() != null){

            ImageFetchTask fetchTask = task.get();
            String downloadUrl = fetchTask.getUrl();

            if((downloadUrl == null) || !downloadUrl.equals(url)){
                fetchTask.cancel(true);
                return true;
            } else
                return false;
        }

        return true;

          }
    }

So just rotate through your image views that are in your fragment and then cancel them when your fragment hides and show them when your fragment is visible.

Love answered 26/11, 2011 at 3:52 Comment(4)
This is, loosely, the strategy I already use though I use ModernAsyncTask with customized pool, weak references and cancelling non-displayed uploads. But I think Loader is more than just simple reuse of AsyncTask. For example, I can see that if I schedule multiple executions in rapid succession the latter invocation cancels the previous one. I guess I would have to spend one night just setting up the sample project and going through the code to fully understand how the Loader worksPoker
Loader does nothing. AsyncTaskLoader is an implementation of Loader using AsyncTask. As far as threading goes, it adds nothing that isn't already in AsyncTask. When it needs to cancel something, it just cancels the AsyncTask which you can do yourself with an AsyncTask.Lentiginous
All right if Dianne said so :) Said that - I still see the behavior where multiple tasks are not queued but cancelled instead with only the last one remain. Is that the right behavior?Poker
Woot! Answer endorsed by someone on the android team! They should have a badge for that.Love

© 2022 - 2024 — McMap. All rights reserved.