Android - 线程池策略以及可以使用Loader来实现吗?

发布于 2024-12-13 17:08:53 字数 3704 浏览 0 评论 0 原文

首先是问题:

  • 我正在开发使用多个 FragmentLists 的应用程序 在自定义的 FragmentStatePagerAdapter 中。可能有, 此类片段的数量可能在 20 到 40 之间。
  • 每个片段都是一个列表,其中每个项目都可以包含文本或图像。
  • 图像需要从网络异步上传并缓存到临时内存缓存以及 SD(如果可用)
  • 当 Fragment 离开屏幕时,任何上传和当前活动都应取消(而不是暂停)

我的第一个实现遵循众所周知的 图像加载器代码。我对该代码的问题是它基本上为每个图像创建一个 AsyncTask 实例。就我而言,这会很快杀死该应用程序。

由于我使用的是 v4 兼容性包,我认为使用扩展 AsyncTaskLoader 的自定义 Loader 会对我有所帮助,因为它在内部实现了线程池。然而,令我不愉快的是,如果我多次执行此代码,则每次后续调用都会中断前一个调用。假设我的 ListView#getView 方法中有这个:

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

此方法在每个进入视图的列表项的循环中执行。正如我所说 - 每个后续调用都会终止前一个调用。或者至少基于 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

然后我想也许为每个加载器提供唯一的 id 会有所帮助,但它似乎没有任何区别。结果,我最终得到了看似随机的图像,而应用程序甚至从未加载过我需要的 1/4。

问题

  • 修复加载器以执行我想要的操作的方法是什么(有没有办法?)
  • 如果没有,创建 AsyncTask 池的好方法是什么,是否有可能有效的实现?

为了让您了解这里的代码,这是 Loader 的精简版本,其中实际的下载/保存逻辑位于单独的 ImageManager 类中。

    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;
        }

    }

}

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;
        }

    }

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

地狱即天堂 2024-12-20 17:08:53

好的,首先要说的是第一件事。 Android 附带的 AsyncTask 不应淹没您的应用程序或导致其崩溃。 AsyncTasks 在线程池中运行,其中最多有 5 个线程同时执行。虽然您可以排队执行许多任务,但一次只能执行其中 5 个任务。通过在后台线程池中执行这些,它们根本不会对您的应用程序产生任何影响,它们应该顺利运行。

如果您对 AsyncTaskLoader 的性能不满意,那么使用 AsyncTaskLoader 并不能解决您的问题。 AsyncTaskLoader 只是采用加载器接口并将其与 AsyncTask 结合起来。所以它本质上是映射 onLoadFinished -> onPostExecute、onStart -> onLoadInBackground。所以这是完全相同的事情。

我们为应用程序使用相同的图像加载器代码,每次尝试加载图像时,都会将异步任务放入线程池队列中。在谷歌的示例中,他们将 imageview 与其异步任务相关联,以便在尝试在某种适配器中重用 imageview 时可以取消异步任务。您应该在这里采取类似的策略。您应该将您的 imageview 与正在后台加载图像的异步任务关联起来。当您有一个未显示的片段时,您可以循环浏览与该片段关联的图像视图并取消加载任务。只需使用 AsyncTask.cancel() 就可以很好地工作。

您还应该尝试实现异步图像视图示例中阐明的简单图像缓存机制。我们简单地创建一个静态哈希图,从 url ->弱引用。这样,图像就可以在需要时被回收,因为它们仅通过弱引用来保存。

这是我们所做的图像加载的概述,

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;

          }
    }

因此只需旋转片段中的图像视图,然后在片段隐藏时取消它们,并在片段可见时显示它们。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文