安卓图片缓存

144

如何在从Web下载图像后对其进行缓存?

18个回答

178

现在来到关键的一步:使用系统缓存。

URL url = new URL(strUrl);
URLConnection connection = url.openConnection();
connection.setUseCaches(true);
Object response = connection.getContent();
if (response instanceof Bitmap) {
  Bitmap bitmap = (Bitmap)response;
} 

提供内存和flash-rom缓存,与浏览器共享。

呃。我希望有人在我编写自己的缓存管理器之前告诉我这一点。


11
connection.getContent() 总是返回一个 InputStream,我做错了什么? - Tyler Collier
3
如果现在我也能为缓存内容设置一个过期日期,我的生活将会变得轻松许多 :) - Janusz
11
@Scienceprodigy 我不知道BitmapLoader是什么,它肯定不在我所知道的任何标准Android库中,但至少它让我朝着正确的方向前进了。Bitmap response = BitmapFactory.decodeStream((InputStream)connection.getContent()); - Stephen Fuhry
6
请查看下面Joe的回答,了解有关启用缓存所需的额外步骤。 - Keith
1
@TylerCollier connection.getContent()没有返回位图,因为URLConnection中的默认内容处理程序都不处理图像类型。您需要设置自定义ContentHandlerFactory,就像这个代码片段中所示。 - sschuberth
显示剩余9条评论

66

关于上面优雅的connection.setUseCaches解决方案:遗憾的是,如果没有额外的努力,它将无法工作。 您需要使用ResponseCache.setDefault安装一个ResponseCache。 否则,HttpURLConnection将默默地忽略setUseCaches(true)

请参阅FileResponseCache.java顶部的注释以获取详细信息:

http://libs-for-android.googlecode.com/svn/reference/com/google/android/filecache/FileResponseCache.html

(我想在评论中发布此内容,但显然我的SO karma不够。)


这里是文件:(http://libs-for-android.googlecode.com/svn/reference/com/google/android/filecache/FileResponseCache.html) - Telémako
2
当您使用HttpResponseCache时,您可能会发现HttpResponseCache.getHitCount()返回0。我不确定,但我认为这是因为您请求的Web服务器在这种情况下没有使用缓存标头。为了使缓存仍然起作用,请使用connection.addRequestProperty("Cache-Control", "max-stale=" + MAX_STALE_CACHE); - Almer
1
Google CodeSearch 的链接又挂了(再次?),请更新链接。 - Felix D.
此外,我不确定这种行为是否已被修复。由于304响应没有与RFC标准相关联的响应体,因此从服务器返回304会在使用.getContent()方法时挂起HUC。 - TheRealChx101

28

将它们转换为位图,然后将它们存储在 Collection(HashMap、List 等) 中,或者可以将它们写入 SD 卡。

当使用第一种方法在应用程序空间中存储它们时,如果它们的数量很大,您可能希望将它们包装在 java.lang.ref.SoftReference 中(以便在危机期间进行垃圾回收)。这可能会导致重新加载。

HashMap<String,SoftReference<Bitmap>> imageCache =
        new HashMap<String,SoftReference<Bitmap>>();

将它们写入SD卡不需要重新加载,只需要用户权限。


我们该如何将图像写入SD卡或手机内存? - d-man
保存图像到SD卡:您可以使用普通的文件I/O操作将从远程服务器读取的图像流提交到内存中,或者如果您已将图像转换为位图对象,则可以使用Bitmap.compress()方法。 - Samuh
@d-man 我建议先将其写入磁盘,然后获取一个 Uri 路径引用,您可以将其传递给 ImageView 和其他自定义视图。因为每次压缩时,您都会失去质量。当然,这仅适用于有损算法。此方法还允许您甚至存储文件的哈希,并在下次通过 If-None-MatchETag 标头从服务器请求文件时使用它。 - TheRealChx101
@TheRealChx101,你能帮忙理解一下你所说的“下次通过If-None-Match和ETag头从服务器请求文件”的意思吗?我基本上正在寻找解决方案,使图像在定义的时间段内保持使用本地缓存,或者如果无法实现,则每当URL的内容发生更改时,它应该反映在应用程序中,并将其缓存。 - CoDe
@CoDe 现在请访问此链接:https://android.jlelse.eu/reducing-your-networking-footprint-with-okhttp-etags-and-if-modified-since-b598b8dd81a1 - TheRealChx101
谢谢,这篇文章是我找到的最好的理解这个概念的文章。我正在使用Glide并尝试手动编辑**If-None-Match(eTag)/ If-Modified-Since(Date)**,使用charles它正在工作。使用glide,我们可以更新标头信息,但有关于这些eTag和修改日期是否必须手动存储,然后在下一个标头请求中推送的想法,或者是否由Glide本身管理,所以我可以直接读取! - CoDe

27

使用LruCache有效地缓存图像。您可以从Android Developer网站了解有关LruCache的详细信息。

我在Android中使用以下解决方案进行图像下载和缓存。 您可以按照以下步骤操作:

第1步:创建名为ImagesCache的类。 我使用了Singleton对象来实现此类。

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

public class ImagesCache 
{
    private  LruCache<String, Bitmap> imagesWarehouse;

    private static ImagesCache cache;

    public static ImagesCache getInstance()
    {
        if(cache == null)
        {
            cache = new ImagesCache();
        }

        return cache;
    }

    public void initializeCache()
    {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() /1024);

        final int cacheSize = maxMemory / 8;

        System.out.println("cache size = "+cacheSize);

        imagesWarehouse = new LruCache<String, Bitmap>(cacheSize)
                {
                    protected int sizeOf(String key, Bitmap value) 
                    {
                        // The cache size will be measured in kilobytes rather than number of items.

                        int bitmapByteCount = value.getRowBytes() * value.getHeight();

                        return bitmapByteCount / 1024;
                    }
                };
    }

    public void addImageToWarehouse(String key, Bitmap value)
    {       
        if(imagesWarehouse != null && imagesWarehouse.get(key) == null)
        {
            imagesWarehouse.put(key, value);
        }
    }

    public Bitmap getImageFromWarehouse(String key)
    {
        if(key != null)
        {
            return imagesWarehouse.get(key);
        }
        else
        {
            return null;
        }
    }

    public void removeImageFromWarehouse(String key)
    {
        imagesWarehouse.remove(key);
    }

    public void clearCache()
    {
        if(imagesWarehouse != null)
        {
            imagesWarehouse.evictAll();
        }       
    }

}

步骤2:

创建另一个名为 DownloadImageTask 的类,如果缓存中没有位图,则从此处下载:

public class DownloadImageTask extends AsyncTask<String, Void, Bitmap>
{   
    private int inSampleSize = 0;

    private String imageUrl;

    private BaseAdapter adapter;

    private ImagesCache cache;

    private int desiredWidth, desiredHeight;

    private Bitmap image = null;

    private ImageView ivImageView;

    public DownloadImageTask(BaseAdapter adapter, int desiredWidth, int desiredHeight) 
    {
        this.adapter = adapter;

        this.cache = ImagesCache.getInstance();

        this.desiredWidth = desiredWidth;

        this.desiredHeight = desiredHeight;
    }

    public DownloadImageTask(ImagesCache cache, ImageView ivImageView, int desireWidth, int desireHeight)
    {
        this.cache = cache;

        this.ivImageView = ivImageView;

        this.desiredHeight = desireHeight;

        this.desiredWidth = desireWidth;
    }

    @Override
    protected Bitmap doInBackground(String... params) 
    {
        imageUrl = params[0];

        return getImage(imageUrl);
    }

    @Override
    protected void onPostExecute(Bitmap result) 
    {
        super.onPostExecute(result);

        if(result != null)
        {
            cache.addImageToWarehouse(imageUrl, result);

            if(ivImageView != null)
            {
                ivImageView.setImageBitmap(result);
            }
            else if(adapter != null)
            {
                adapter.notifyDataSetChanged();
            }
        }
    }

    private Bitmap getImage(String imageUrl)
    {   
        if(cache.getImageFromWarehouse(imageUrl) == null)
        {
            BitmapFactory.Options options = new BitmapFactory.Options();

            options.inJustDecodeBounds = true;

            options.inSampleSize = inSampleSize;

            try
            {
                URL url = new URL(imageUrl);

                HttpURLConnection connection = (HttpURLConnection)url.openConnection();

                InputStream stream = connection.getInputStream();

                image = BitmapFactory.decodeStream(stream, null, options);

                int imageWidth = options.outWidth;

                int imageHeight = options.outHeight;

                if(imageWidth > desiredWidth || imageHeight > desiredHeight)
                {   
                    System.out.println("imageWidth:"+imageWidth+", imageHeight:"+imageHeight);

                    inSampleSize = inSampleSize + 2;

                    getImage(imageUrl);
                }
                else
                {   
                    options.inJustDecodeBounds = false;

                    connection = (HttpURLConnection)url.openConnection();

                    stream = connection.getInputStream();

                    image = BitmapFactory.decodeStream(stream, null, options);

                    return image;
                }
            }

            catch(Exception e)
            {
                Log.e("getImage", e.toString());
            }
        }

        return image;
    }

步骤 3:从您的ActivityAdapter中使用

注意:如果您想从Activity类加载URL的图像,请使用DownloadImageTask的第二个构造函数,但如果您想从Adapter显示图像,请使用DownloadImageTask的第一个构造函数(例如,您在ListView中有一个图片,并且您正在从“Adapter”设置图像)

从Activity中的用法:

ImageView imv = (ImageView) findViewById(R.id.imageView);
ImagesCache cache = ImagesCache.getInstance();//Singleton instance handled in ImagesCache class.
cache.initializeCache();

String img = "your_image_url_here";

Bitmap bm = cache.getImageFromWarehouse(img);

if(bm != null)
{
  imv.setImageBitmap(bm);
}
else
{
  imv.setImageBitmap(null);

  DownloadImageTask imgTask = new DownloadImageTask(cache, imv, 300, 300);//Since you are using it from `Activity` call second Constructor.

  imgTask.execute(img);
}

适配器使用方法:

ImageView imv = (ImageView) rowView.findViewById(R.id.imageView);
ImagesCache cache = ImagesCache.getInstance();
cache.initializeCache();

String img = "your_image_url_here";

Bitmap bm = cache.getImageFromWarehouse(img);

if(bm != null)
{
  imv.setImageBitmap(bm);
}
else
{
  imv.setImageBitmap(null);

  DownloadImageTask imgTask = new DownloadImageTask(this, 300, 300);//Since you are using it from `Adapter` call first Constructor.

  imgTask.execute(img);
}

注意:

cache.initializeCache() 可以在应用程序中的第一个Activity中使用此语句。一旦你初始化了缓存,如果你正在使用 ImagesCache 实例,你将永远不需要再次初始化缓存。

我从来不擅长解释事物,但希望这对于那些想要使用 LruCache 进行缓存和使用的初学者有所帮助 :)

编辑:

现在有很多著名的库,如 PicassoGlide,可以在 Android 应用程序中高效地加载图像。尝试这个非常简单和有用的库Picasso for androidGlide For Android。你不需要担心缓存图片。

Picasso 允许在应用程序中无忧无虑地加载图像 - 往往只需一行代码!

Glide 就像 Picasso 一样,可以从许多来源加载和显示图像 ,同时还负责缓存和在进行图像操作时保持低内存影响。它已被 Google 官方应用程序(如 Google I/O 2015 应用程序)使用,并且与 Picasso 一样受欢迎。在这个系列中,我们将探索 Glide 相对于 Picasso 的差异和优势。

你还可以访问Glide 和 Picasso 之间的区别 博客了解更多信息。


3
非常出色的答案和解释!我认为这是最佳解决方案,因为它可以在离线时使用Android LruCache。我发现edrowland的解决方案即使加上Joe的补充,在飞行模式下也无法使用,需要更多的集成工作。顺便说一句,似乎Android或网络即使不做任何额外的操作也提供了大量缓存。(一个小问题:对于示例用法getImageFromWareHouse,应将“H”小写以匹配。)谢谢! - Edwin Evans
@Greyshack。基本上,LruCache 有键值对,每当您获取图像 URL 时,getImage() 将从 URL 下载图像。图像的 URL 将是 LruCachekey,而位图将是 value。如果您仔细查看 DownloadImageTask,您可以设置 desiredWidthdesiredHeight 值,您设置的宽度和高度越小,您将看到的图像质量越低。 - Zubair Ahmed
当边界正确时,它将返回位图,否则将返回null。请检查我上面定义的用法... - Zubair Ahmed
1
点赞那个 if(cache == null),它解决了我的问题! :) - Koorosh
1
还请看一下我的编辑答案。我在结尾处提到了现今大多数开发者使用的著名库。请尝试使用这些库:Picasso: http://square.github.io/picasso/ 和 Glide: https://futurestud.io/blog/glide-getting-started - Zubair Ahmed
显示剩余18条评论

18

要下载一张图片并保存到存储卡中,你可以像这样做。

//First create a new URL object 
URL url = new URL("http://www.google.co.uk/logos/holiday09_2.gif")

//Next create a file, the example below will save to the SDCARD using JPEG format
File file = new File("/sdcard/example.jpg");

//Next create a Bitmap object and download the image to bitmap
Bitmap bitmap = BitmapFactory.decodeStream(url.openStream());

//Finally compress the bitmap, saving to the file previously created
bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(file));

别忘了在你的清单文件中添加互联网权限:

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

11
为什么您要对JPEG进行解码并重新编码?更好的做法是将URL下载到一个字节数组中,然后使用该字节数组创建位图并写入文件。每次解码和重新编码JPEG都会降低图像质量。 - CommonsWare
2
说得好,这更多是为了速度。不过,如果将其保存为字节数组并且源文件不是JPEG格式,那么文件是否仍需要进行转换呢?SDK中的“decodeByteArray”返回“已解码的位图,如果无法解码图像数据,则返回null”,因此这使我认为它始终在解码图像数据,所以这不需要重新编码吗? - Ljdawson
1
我不建议将图像缓存到SD卡中。一旦应用程序被卸载,这些图像就不会被删除,导致SD卡充满了无用的垃圾。在我的看法中,将图像保存到应用程序的缓存目录中是更好的选择。 - james
@binnyb,如果您将数据存储在正确的目录中(在Android 2.2+上),那么这并不是真的。您应该使用Android/data/<your package name>/files/。当用户删除应用程序时,此目录将被清除。 - Christopher Perry
你可以使用 getExternalCacheDir 来获取一个在SD卡上的路径,当用户删除应用程序时,该路径将被清除。但是像文档所说,当达到大小限制时,它不会自动清理该文件夹,而 getCacheDir 会为您处理这个问题。不要忘记 getExternalCacheDir 需要 WRITE_EXTERNAL_STORAGE 权限。 - Gordon Glas
显示剩余2条评论

13

他自2010年以来已经重构了他的代码;这是根链接:https://github.com/kaeppler/droid-fu - esilver
3
那个链接仍然无法使用。我编写了一个类似的库,名为Android-ImageManager,链接为 https://github.com/felipecsl/Android-ImageManager。 - Felipe Lima

9
我尝试过使用软引用,但在安卓系统中,它们被回收得太频繁了,以至于我感到使用它们毫无意义。

2
同意 - 在我测试过的设备上,软引用被很快回收。 - esilver
3
谷歌已经确认Dalvik的垃圾回收器对清理“SoftReference”非常积极。他们建议使用他们的“LruCache”代替。 - kaka

9
如Thunder Rabbit所建议,ImageDownloader是最适合这项工作的。我还发现了该类的一个轻微变化,位于以下网址:http://theandroidcoder.com/utilities/android-image-download-and-caching/ 两个之间的主要区别在于ImageDownloader使用Android缓存系统,而修改后的版本则使用内部和外部存储作为缓存,将缓存的图像保留无限期或直至用户手动删除。作者还提到了Android 2.1的兼容性。

7

在Android的官方培训章节中有一个特别的条目与此相关:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

这个章节是很新的,当提问时它还不存在。

建议的解决方法是使用LruCache。这个类在Honeycomb上被引入,但也包括在兼容库中。

你可以通过设置最大条目数来初始化LruCache,当超过限制时,它会自动为您排序并清除不常用的条目。除此之外,它像普通的Map一样使用。

下面是官方网页的示例代码:

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get memory class of this device, exceeding this amount will throw an
    // OutOfMemory exception.
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

以前,SoftReferences是一个很好的选择,但现在不再是了,引用官方网页上的话:

注意:过去,一种流行的内存缓存实现是使用SoftReference或WeakReference位图缓存,然而这不被推荐。从Android 2.3(API Level 9)开始,垃圾收集器对于收集软引用/弱引用更加积极,使它们相当无效。此外,在Android 3.0之前(API Level 11),位图的后备数据存储在本地内存中,该内存不以可预测的方式释放,可能导致应用程序短暂超出其内存限制并崩溃。


7
这是Joe做出的好发现。上面的代码示例有两个问题 - 一 - 响应对象不是Bitmap的实例(当我的URL引用jpg文件时,例如http:\website.com\image.jpg,它是org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl$LimitedInputStream)。 第二,正如Joe指出的那样,如果没有配置响应缓存,则不会发生缓存。Android开发人员需要自己编写缓存。以下是一个示例,但它只在内存中进行缓存,这并不是完整的解决方案。

http://codebycoffee.com/2010/06/29/using-responsecache-in-an-android-app/

URLConnection缓存API的描述在这里:

http://download.oracle.com/javase/6/docs/technotes/guides/net/http-cache.html

我仍然认为这是一种可行的解决方案,但你仍然需要编写一个缓存。听起来很有趣,但我更愿意编写功能。


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