如何在Android中的ListView中使用延迟加载技术来加载图片

2078

我正在使用ListView来显示一些与图像相关的标题和图像。 我正在从互联网上获取这些图像。 有没有一种方法可以懒加载图像,以便在文本显示时不会阻塞UI,并且图像会随着下载而显示?

图片的总数并不是固定的。


13
你可以使用GreenDroid的AsyncImageView。只需调用 setUrl 即可。 - Pascal Dimassimo
8
我已经使用过它了,它是一个很棒的实现。坏消息是,AsyncImageView是大型GreenDroid项目的一部分,即使你只需要AsyncImageView,这也会使你的应用程序变得更大。此外,GreenDroid项目似乎自2011年以来就没有更新了。 - borisstr
6
你甚至可以试试这个库:Android-http-image-manager,我认为它是最适合异步加载图片的。 - Ritesh Kumar Dubey
36
只需使用 Picasso,它会自动完成所有操作。'Picasso.with(yourContext).load(img src/path/drawable here).into(imageView i.e your target);'就这样! - Anuj Sharma
10
尝试使用这个库:https://github.com/nostra13/Android-Universal-Image-Loader,它非常适用于图片的懒加载和缓存,速度快且高效。 - krunal patel
2
使用Glide图像加载库。 - Devendra Kulkarni
41个回答

1150

这是我创建的用于容纳应用程序当前显示的图像的代码。请注意,此处使用的“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(Looper.getMainLooper()) {
            @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();
    }
}

109
我认为你应该使用SoftReferences,这样你的程序就永远不会引起OutOfMemoryException。由于GC可以在堆大小增加时清除软引用……你可以像管理自己的代一样,每隔几秒钟将图片放入列表中,并在加载之前检查是否存在该图像,如果存在,则不需要再次下载,而是从该列表中收集它,并将其放回到你的软引用列表中。一段时间后,你可以清除你的硬列表 :) - AZ_
41
谷歌书架项目是一个很好的例子,看看他们是如何做到这一点的。您可以访问http://code.google.com/p/shelves/查看详情。 - AZ_
14
当drawableMap包含图像但没有启动获取线程时,您不会错过返回吗? - Karussell
7
这段代码存在几个问题。首先,您应该缓存Drawables,否则会导致内存泄漏:https://dev59.com/l2sz5IYBdhLWcg3wy7LU 。其次,缓存本身从未清除,因此它将无限增长,这是另一个内存泄漏的问题。 - satur9nine
12
有人听说过“LRU缓存”吗?http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html - Muhammad Babar
显示剩余6条评论

1051

我使用了图片制作了一个简单的懒加载列表示例(位于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解码图像以减少内存消耗。我还尝试正确处理回收的视图。

Alt text


564
我推荐使用开源工具Universal Image Loader。它最初基于Fedor Vlasov的项目LazyList,并在此基础上得到了大大的改进。
  • 多线程加载图片
  • 可以广泛调整ImageLoader的配置(线程执行器、下载器、解码器、内存和磁盘缓存、展示图片选项等)
  • 可以将图像缓存在内存中和/或设备的文件系统(或SD卡)中
  • 可以“监听”加载过程
  • 可以针对每个显示图像调用进行自定义设置
  • 支持小部件
  • 支持Android 2.0+


166

《多线程提升性能》,由Gilles Debunne所写的教程。

这是来自Android Developers Blog的内容。建议使用以下代码:

  • AsyncTasks
  • 一个硬性、有限大小的 FIFO缓存
  • 一个软性、易于垃圾回收的缓存。
  • 在下载时使用 占位符 Drawable

输入图像描述


11
在2.1版本中它也能正常工作。只需不使用AndroidHttpClient即可。 - Thomas Ahle
3
谢谢你,我看到AndroidHttpClient在2.1中会出错,因为它是从2.2开始实现的,但我并没有尝试寻找其他替代品。 - Adinia
5
@Adina 你是对的,我忘了。不过这个食谱里面没有任何东西是不能用普通的HttpClient完成的。 - Thomas Ahle
我在多个地方听说过,Google不推荐使用软引用,因为Android核心相比系统早期版本非常渴望收集这些引用。 - Muhammad Ahmed AbuTalib
你能帮忙吗?https://stackoverflow.com/questions/62624070/how-to-priorly-get-future-glide-image-size-which-will-be-stored-in-cache-in-andr - Priyanka Singh

118

更新:请注意,这个答案现在已经不太有效了。垃圾收集器在SoftReference和WeakReference上表现得很积极,因此该代码不适用于新的应用程序。 (相反,您可以尝试像其他答案中建议的Universal Image Loader等库。)

感谢James提供的代码,以及Bao-Long建议使用SoftReference的建议。我在James的代码上实现了SoftReference更改。不幸的是,SoftReferences使我的图片被垃圾回收得太快了。在我的情况下,没有SoftReference也没关系,因为我的列表大小有限,我的图片很小。

一年前在Google群组中有讨论SoftReference的话题:链接到线程。作为避免过早垃圾回收的解决方案,他们建议可能使用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();
}

4
您可以创建像硬代和软代这样的生成。您可以设置一个时间,当清除缓存时,清除所有未在3秒内访问的图像。您可以查看Google书架项目。 - AZ_
SoftReference文档中有一个关于缓存的注释,请参见“避免使用软引用进行缓存”部分。大多数应用程序应该使用android.util.LruCache而不是软引用。 - vokilam
我很欣赏你的代码,但现在在新的Android操作系统中有“激进”的垃圾回收。持有弱引用对我来说没有任何意义。 - j2emanue
@j2emanue 你是对的,正如我在答案开头尝试指出的那样,SoftReferences被垃圾回收得太快了。我会尝试编辑这个答案,使其更加清晰明了。 - TalkLittle

108

毕加索

使用Jake Wharton的Picasso库。(来自ActionBarSherlock的开发者的完美图像加载库)

Picasso是一款强大的Android图像下载和缓存库。

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

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

在Android上,Picasso自动处理许多常见的图像加载陷阱:

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

Picasso Jake Wharton's Library

Glide

Glide是一个快速高效的开源媒体管理框架,将媒体解码、内存和磁盘缓存以及资源池包装成一个简单易用的接口。

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

Glide.with(this).load("your-url-here").into(imageView);

Glide主要专注于使任何类型的图像列表滚动尽可能流畅和快速,但Glide也适用于几乎所有需要获取、调整大小和显示远程图像的情况。

Glide图片加载库

Facebook 的 Fresco

Fresco是Android应用程序中展示图片的强大系统。

Fresco会负责图像的加载和显示,因此您不需要这样做。它将从网络、本地存储或本地资源加载图像,并显示占位符,直到图像到达为止。它有两个级别的缓存:内存中的一个和内部存储中的另一个。

Fresco Github

在 Android 4.x 及更低版本中,Fresco会将图像放置在 Android 内存的特殊区域中。这可以让您的应用程序运行得更快,也可以避免经常出现的 OutOfMemoryError。

Fresco 文档


1
Picasso是由Square开发的一个库。 - LordRaydenMK

88

高性能加载器 - 在研究了这里提供的方法后,我使用了Ben的解决方案,并做了一些改动:

  1. 我发现使用Drawable比Bitmap更快,所以我使用了Drawable。

  2. 使用SoftReference很好,但它会导致缓存图片被频繁删除,因此我添加了一个保存图片引用的LinkedList,防止图片被删除,直到达到预定义的大小。

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

非常好!顺便说一下,类名有个错别字。 - Mullins
6
如果能够节省其他人的时间,以下是需要导入的内容: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; - Michael Reed
10
如果您使用 Eclipse,请尝试使用 Ctrl-Shift-O(注意是字母 O,而非数字 0)。这个功能可以自动添加导入语句并为您组织它们。如果您使用的是 Mac,可以使用 Command-Shift-O 来实现相同的功能。 - SilithCrowe
该解决方案不使用异步任务,因为线程池允许同时下载多个图像。 - Asaf Pinhassi
@Michael Reed:可以使用 ctr+shift+O 进行自动导入。 - CoDe
显示剩余5条评论

82

我已经学习了这个Android培训课程,认为它在下载图像时做得非常出色,而不会阻塞主UI。它还处理缓存并处理浏览多个图像的滚动:

高效加载大型位图


很抱歉,我只指向了 Google IO 应用程序中的一个类(而且我现在已经来不及编辑了)。你应该真正研究所有他们可以在 缓存类相同的包中找到的图像加载和缓存实用程序类 - mkuech
有人推荐从iosched应用程序的util文件夹中获取DiskLruCache、Image*.java文件来帮助处理列表视图的图像加载/缓存吗?我的意思是,遵循在线开发者指南肯定是值得的,但这些类(来自iosched)在模式方面更进一步。 - Gautam

73

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

使用Gradle:

implementation 'com.squareup.picasso:picasso:(insert latest version)'

只需要一行代码!

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

2. Glide 是一款专注于实现 Android 平台上平滑滚动的图片加载和缓存库。

使用 Gradle:

repositories {
  mavenCentral() 
  google()
}

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

// 简单视图:

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

3. fresco 是一款在Android应用中显示图片的强大系统。Fresco负责图片的加载和展示,因此您无需自行处理。

开始使用Fresco


这个教程可能会对你在PICASOO方面有所帮助:http://www.androidtutorialshub.com/picasso-image-loading-library-for-android-tutorial/,以及GLIDE方面的帮助:http://www.androidtutorialshub.com/glide-image-loading-library-for-android-tutorial/。 - lalit vasan

56

我写了一篇教程,详细解释了如何在列表视图中进行图片的懒加载。我深入讲解了回收和并发问题。同时,我使用了一个固定的线程池来避免产生大量线程。

Listview中的图片懒加载教程


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接