如何在 Android 中的 ListView 中延迟加载图像

我正在使用ListView显示一些图像和与这些图像相关的标题。我正在从互联网上获取图像。有没有一种方法可以延迟加载图像,以便在显示文本时不阻止 UI 并在下载图像时显示它们?

图像总数不固定。

答案

这是我创建的用于保存应用程序当前正在显示的图像的内容。请注意,这里使用的 “Log” 对象是我在 Android 内部最终 Log 类周围的自定义包装。

package com.wilson.android.library;

/*
 Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
*/
import java.io.IOException;

public class DrawableManager {
    private final Map<String, Drawable> drawableMap;

    public DrawableManager() {
        drawableMap = new HashMap<String, Drawable>();
    }

    public Drawable fetchDrawable(String urlString) {
        if (drawableMap.containsKey(urlString)) {
            return drawableMap.get(urlString);
        }

        Log.d(this.getClass().getSimpleName(), "image url:" + urlString);
        try {
            InputStream is = fetch(urlString);
            Drawable drawable = Drawable.createFromStream(is, "src");


            if (drawable != null) {
                drawableMap.put(urlString, drawable);
                Log.d(this.getClass().getSimpleName(), "got a thumbnail drawable: " + drawable.getBounds() + ", "
                        + drawable.getIntrinsicHeight() + "," + drawable.getIntrinsicWidth() + ", "
                        + drawable.getMinimumHeight() + "," + drawable.getMinimumWidth());
            } else {
              Log.w(this.getClass().getSimpleName(), "could not get thumbnail");
            }

            return drawable;
        } catch (MalformedURLException e) {
            Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
            return null;
        } catch (IOException e) {
            Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
            return null;
        }
    }

    public void fetchDrawableOnThread(final String urlString, final ImageView imageView) {
        if (drawableMap.containsKey(urlString)) {
            imageView.setImageDrawable(drawableMap.get(urlString));
        }

        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message message) {
                imageView.setImageDrawable((Drawable) message.obj);
            }
        };

        Thread thread = new Thread() {
            @Override
            public void run() {
                //TODO : set imageView to a "pending" image
                Drawable drawable = fetchDrawable(urlString);
                Message message = handler.obtainMessage(1, drawable);
                handler.sendMessage(message);
            }
        };
        thread.start();
    }

    private InputStream fetch(String urlString) throws MalformedURLException, IOException {
        DefaultHttpClient httpClient = new DefaultHttpClient();
        HttpGet request = new HttpGet(urlString);
        HttpResponse response = httpClient.execute(request);
        return response.getEntity().getContent();
    }
}

我做了一个带有图像的惰性列表的简单演示 (位于 GitHub 上)。

基本用法

ImageLoader imageLoader=new ImageLoader(context); ...
imageLoader.DisplayImage(url, imageView);

不要忘记向您的 AndroidManifest.xml 添加以下权限:

<uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> Please

仅创建一个 ImageLoader 实例,并在您的应用程序中重复使用它。这样,图像缓存将更加高效。

这可能对某人有帮助。它将在后台线程中下载图像。图像被缓存在 SD 卡和内存中。缓存的实现非常简单,足以用于演示。我使用 inSampleSize 解码图像以减少内存消耗。我也尝试正确处理回收的视图。

替代文字

我建议使用开放源代码工具Universal Image Loader 。它最初是基于 Fedor Vlasov 的项目LazyList 进行的 ,此后得到了极大的改进。

  • 多线程图像加载
  • 可以广泛调整 ImageLoader 的配置(线程执行程序,下载器,解码器,内存和磁盘缓存,显示图像选项等)
  • 内存和 / 或设备文件系统(或 SD 卡)中图像缓存的可能性
  • “监听” 加载过程的可能性
  • 可以使用单独的选项自定义每个显示图像调用
  • 小部件支持
  • Android 2.0 + 支持

Gilles Debunne 撰写的教程 Multi Performanceing for Performance

这是来自 Android 开发者博客。建议的代码使用:

  • AsyncTasks
  • 有限大小的硬FIFO cache
  • 一个软的,易于garbage collect缓存。
  • 下载时Drawable占位符

在此处输入图片说明

更新:请注意,此答案现在无效。垃圾收集器对 SoftReference 和 WeakReference 采取积极措施,因此此代码不适用于新应用。 (相反,请尝试其他答案中建议的库,例如Universal Image Loader 。)

感谢 James 的代码,以及 Bao-Long 提出的使用 SoftReference 的建议。我在 James 的代码上实现了 SoftReference 更改。不幸的是,SoftReferences 导致我的图像被垃圾收集的速度过快。就我而言,没有 SoftReference 的东西就很好了,因为我的列表大小有限并且图像很小。

一年前,有一个关于 Google 组上的 SoftReferences 的讨论: 链接到 thread 。作为对过早的垃圾回收的解决方案,他们建议使用 dalvik.system.VMRuntime.setMinimumHeapSize()手动设置 VM 堆大小的可能性,这对我来说不是很有吸引力。

public DrawableManager() {
    drawableMap = new HashMap<String, SoftReference<Drawable>>();
}

public Drawable fetchDrawable(String urlString) {
    SoftReference<Drawable> drawableRef = drawableMap.get(urlString);
    if (drawableRef != null) {
        Drawable drawable = drawableRef.get();
        if (drawable != null)
            return drawable;
        // Reference has expired so remove the key from drawableMap
        drawableMap.remove(urlString);
    }

    if (Constants.LOGGING) Log.d(this.getClass().getSimpleName(), "image url:" + urlString);
    try {
        InputStream is = fetch(urlString);
        Drawable drawable = Drawable.createFromStream(is, "src");
        drawableRef = new SoftReference<Drawable>(drawable);
        drawableMap.put(urlString, drawableRef);
        if (Constants.LOGGING) Log.d(this.getClass().getSimpleName(), "got a thumbnail drawable: " + drawable.getBounds() + ", "
                + drawable.getIntrinsicHeight() + "," + drawable.getIntrinsicWidth() + ", "
                + drawable.getMinimumHeight() + "," + drawable.getMinimumWidth());
        return drawableRef.get();
    } catch (MalformedURLException e) {
        if (Constants.LOGGING) Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
        return null;
    } catch (IOException e) {
        if (Constants.LOGGING) Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
        return null;
    }
}

public void fetchDrawableOnThread(final String urlString, final ImageView imageView) {
    SoftReference<Drawable> drawableRef = drawableMap.get(urlString);
    if (drawableRef != null) {
        Drawable drawable = drawableRef.get();
        if (drawable != null) {
            imageView.setImageDrawable(drawableRef.get());
            return;
        }
        // Reference has expired so remove the key from drawableMap
        drawableMap.remove(urlString);
    }

    final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message message) {
            imageView.setImageDrawable((Drawable) message.obj);
        }
    };

    Thread thread = new Thread() {
        @Override
        public void run() {
            //TODO : set imageView to a "pending" image
            Drawable drawable = fetchDrawable(urlString);
            Message message = handler.obtainMessage(1, drawable);
            handler.sendMessage(message);
        }
    };
    thread.start();
}

毕加索

使用杰克 · 沃顿的毕加索图书馆。 (由 ActionBarSherlock 的开发人员提供的完善的 ImageLoading 库)

适用于 Android 的功能强大的图像下载和缓存库。

图像为 Android 应用程序增加了急需的上下文和视觉效果。 Picasso 允许在您的应用程序中轻松加载图像 - 通常只需一行代码!

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

毕加索会自动处理 Android 上许多常见的图片加载陷阱:

在适配器中处理 ImageView 回收和下载取消。复杂的图像转换,使用最少的内存。自动内存和磁盘缓存。

毕加索杰克 · 沃顿的图书馆

滑行

Glide 是适用于 Android 的快速高效的开源媒体管理框架,该框架将媒体解码,内存和磁盘缓存以及资源池包装到一个简单易用的界面中。

Glide 支持获取,解码和显示视频静止图像,图像和动画 GIF。 Glide 包含一个灵活的 API,允许开发人员插入几乎所有网络堆栈。默认情况下,Glide 使用基于自定义 HttpUrlConnection 的堆栈,但还包括实用程序库,它们可以插入 Google 的 Volley 项目或 Square 的 OkHttp 库。

Glide.with(this).load("http://goo.gl/h8qOq7").into(imageView);

Glide 的主要重点是使任何种类的图像列表尽可能平滑和快速地滚动,但是 Glide 在几乎所有需要获取,调整大小和显示远程图像的情况下也有效。

滑行图像加载库

Facebook 的壁画

Fresco 是一个功能强大的系统,可在 Android 应用程序中显示图像。

壁画负责图像的加载和显示,因此您不必这样做。它将从网络,本地存储或本地资源加载图像,并显示一个占位符,直到图像到达为止。它具有两个级别的缓存;一个在内存中,另一个在内部存储器中。

壁画 Github

在 Android 4.x 及更低版本中,Fresco 将图像放在 Android 内存的特殊区域中。这使您的应用程序运行得更快 - 并减少了遭受可怕的 OutOfMemoryError 的频率。

壁画文档

高性能加载程序 - 在检查了此处建议的方法后,我使用了Ben 的解决方案并进行了一些更改 -

  1. 我意识到使用可绘制对象比使用位图更快,因此我改用可绘制对象

  2. 使用 SoftReference 很棒,但是它会使缓存的图像被删除得太频繁,因此我添加了一个链接列表,该列表保存了图像引用,防止图像被删除,直到达到预定义的大小为止。

  3. 要打开 InputStream,我使用了 java.net.URLConnection,它允许我使用 Web 缓存(您需要首先设置响应缓存,但这是另一回事了)

我的代码:

import java.util.Map; 
import java.util.HashMap; 
import java.util.LinkedList; 
import java.util.Collections; 
import java.util.WeakHashMap; 
import java.lang.ref.SoftReference; 
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import android.os.Handler;
import android.os.Message;
import java.io.InputStream;
import java.net.MalformedURLException; 
import java.io.IOException; 
import java.net.URL;
import java.net.URLConnection;

public class DrawableBackgroundDownloader {    

private final Map<String, SoftReference<Drawable>> mCache = new HashMap<String, SoftReference<Drawable>>();   
private final LinkedList <Drawable> mChacheController = new LinkedList <Drawable> ();
private ExecutorService mThreadPool;  
private final Map<ImageView, String> mImageViews = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());  

public static int MAX_CACHE_SIZE = 80; 
public int THREAD_POOL_SIZE = 3;

/**
 * Constructor
 */
public DrawableBackgroundDownloader() {  
    mThreadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);  
}  


/**
 * Clears all instance data and stops running threads
 */
public void Reset() {
    ExecutorService oldThreadPool = mThreadPool;
    mThreadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    oldThreadPool.shutdownNow();

    mChacheController.clear();
    mCache.clear();
    mImageViews.clear();
}  

public void loadDrawable(final String url, final ImageView imageView,Drawable placeholder) {  
    mImageViews.put(imageView, url);  
    Drawable drawable = getDrawableFromCache(url);  

    // check in UI thread, so no concurrency issues  
    if (drawable != null) {  
        //Log.d(null, "Item loaded from mCache: " + url);  
        imageView.setImageDrawable(drawable);  
    } else {  
        imageView.setImageDrawable(placeholder);  
        queueJob(url, imageView, placeholder);  
    }  
} 


private Drawable getDrawableFromCache(String url) {  
    if (mCache.containsKey(url)) {  
        return mCache.get(url).get();  
    }  

    return null;  
}

private synchronized void putDrawableInCache(String url,Drawable drawable) {  
    int chacheControllerSize = mChacheController.size();
    if (chacheControllerSize > MAX_CACHE_SIZE) 
        mChacheController.subList(0, MAX_CACHE_SIZE/2).clear();

    mChacheController.addLast(drawable);
    mCache.put(url, new SoftReference<Drawable>(drawable));

}  

private void queueJob(final String url, final ImageView imageView,final Drawable placeholder) {  
    /* Create handler in UI thread. */  
    final Handler handler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            String tag = mImageViews.get(imageView);  
            if (tag != null && tag.equals(url)) {
                if (imageView.isShown())
                    if (msg.obj != null) {
                        imageView.setImageDrawable((Drawable) msg.obj);  
                    } else {  
                        imageView.setImageDrawable(placeholder);  
                        //Log.d(null, "fail " + url);  
                    } 
            }  
        }  
    };  

    mThreadPool.submit(new Runnable() {  
        @Override  
        public void run() {  
            final Drawable bmp = downloadDrawable(url);
            // if the view is not visible anymore, the image will be ready for next time in cache
            if (imageView.isShown())
            {
                Message message = Message.obtain();  
                message.obj = bmp;
                //Log.d(null, "Item downloaded: " + url);  

                handler.sendMessage(message);
            }
        }  
    });  
}  



private Drawable downloadDrawable(String url) {  
    try {  
        InputStream is = getInputStream(url);

        Drawable drawable = Drawable.createFromStream(is, url);
        putDrawableInCache(url,drawable);  
        return drawable;  

    } catch (MalformedURLException e) {  
        e.printStackTrace();  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  

    return null;  
}  


private InputStream getInputStream(String urlString) throws MalformedURLException, IOException {
    URL url = new URL(urlString);
    URLConnection connection;
    connection = url.openConnection();
    connection.setUseCaches(true); 
    connection.connect();
    InputStream response = connection.getInputStream();

    return response;
}
}

我已经接受了本 Android 培训,并且我认为它在下载图像方面做得很好,而且不会阻塞主界面。它还处理缓存和处理滚动浏览许多图像的步骤: 有效地加载大型位图

1. Picasso允许在您的应用程序中轻松加载图像 - 通常只需一行代码!

使用 Gradle:

implementation 'com.squareup.picasso:picasso:2.71828'

只需一行代码!

Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);

2. 滑行 Android 的图像加载和缓存库着重于平滑滚动

使用 Gradle:

repositories {
  mavenCentral() 
  google()
}

dependencies {
   implementation 'com.github.bumptech.glide:glide:4.7.1'
   annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'
}

// 对于一个简单的视图:

Glide.with(this).load("http://i.imgur.com/DvpvklR.png").into(imageView);

3. fresco是一个功能强大的系统,可在 Android 应用程序中显示图像。Fresco 负责图像的加载和显示,因此您不必这样做。

壁画入门

我写了一个教程,解释了如何在列表视图中延迟加载图像。我将详细介绍回收和并发问题。我还使用固定线程池来防止产生大量线程。

在 Listview 教程中延迟加载图像