使用Picasso和自定义磁盘缓存

26
Volley库中,NetworkImageView类需要一个ImageLoader来处理所有的图片请求,通过在ImageCache实现中搜索它们,用户可以自由选择缓存的方式、图片的位置和名称。
我正在从Volley切换到Retrofit,对于图片,我决定尝试使用Picasso
在前一个库中,我的每个项都有一个包含图像URL的字符串参数,然后我使用myNetworkImageView.setImageUrl(item.getURL()),它能够确定图像是否已经缓存在磁盘上。如果图像存在于缓存文件夹中,则加载该图像,否则下载并加载。
我想在Picasso中也能实现相同的功能,是否可以通过Picasso的API实现,还是需要自己编写代码?
我考虑将图像下载到文件夹(缓存文件夹)中,在下载完成后使用Picasso.with(mContext).load(File downloadedimage)。这是正确的方法吗?还有其他最佳实践吗?

1
@CommonsWare 我基本上想让Picasso将图像下载到自定义路径并使用自定义名称,以便我可以实现某种缓存来检查已下载的图像是否存在并避免重新下载。自定义路径和自定义名称都很重要,因为用户应该能够替换它们想要的自定义图像。此外,我不想担心占用的空间,我只想在用户可以访问的文件夹中永久存储图像。 - Vektor88
据我所知,这将需要对Picasso进行代码更改。 - CommonsWare
@CommonsWare 我想我可以从URL下载图像,并在下载完成后要求Picasso加载该文件,但我不知道是否有任何最佳实践来做到这一点,或者是否由于某些原因不方便。我还读到过,一种方式是Picasso支持回调函数,因此我可以在下载完成后访问位图并将其保存到我希望的文件中,然后下一次执行将检查缓存文件夹中是否存在该文件。这会是一个解决方案吗? - Vektor88
也许吧。现在我想起来,Picasso把磁盘当作缓存,而你把它看作比那更重要的东西(“永久存储的图像”),所以试图修改Picasso的磁盘缓存方法可能不是正确的答案。 - CommonsWare
@CommonsWare,您的建议是切换到另一个库吗?如果是,您有什么推荐的库符合我的需求吗? - Vektor88
4个回答

52

Picasso没有磁盘缓存。它委托给您使用的任何HTTP客户端来实现此功能(依赖于HTTP缓存语义进行缓存控制)。因此,您所寻求的行为是免费的。

底层HTTP客户端仅在本地缓存中不存在图像(且该图像未过期)时才从网络下载图像。

话虽如此,您可以为java.net.HttpUrlConnection创建自定义缓存实现(通过ResponseCache或OkHttp(通过ResponseCacheOkResponseCache),以以所需格式存储文件。但是,我强烈建议不要这样做。

让Picasso和HTTP客户端为您完成工作!

您可以在Picasso实例上调用setIndicatorsEnabled(true)以查看加载图像的指示器。它看起来像这样:

如果您从未看到蓝色指示器,则很可能是由于您的远程图像没有包含适当的缓存头,无法启用磁盘缓存。

6
我强烈不同意你关于这种需求是非典型的说法。我工作过的每个应用程序都需要离线可用的图片。这意味着,如果图片无法从网络获取但在磁盘缓存中可用,则应提供该图片。无论它有多旧。 - Daniele Segato
2
@DanieleSegato 然后使用唯一的URL并返回令人讨厌的大缓存头(1年+)。此外,如果网络检索失败3次,Picasso将使用过时的缓存图像。 - Jake Wharton
@JakeWharton 我同意你的观点,独特的URL是一个好的实践,但我经常依赖于第三方服务,我无法控制。关于库的行为:对我来说似乎不是这样,我尝试了一下并查看了代码:它从retries = 2开始,减少并在retries == 0时进入本地缓存。但是,如果retryCount <= 0,“shouldRetry”方法返回false。此外,如果没有网络,Dispatcher“performRetry”方法甚至会完全跳过第二次尝试,只执行错误。看起来你期望它有不同的工作方式。这是一个bug吗? - Daniele Segato
1
如果您需要使磁盘缓存失效,则您的服务器一开始就没有设置适当的标头。 - Jake Wharton
1
@JakeWharton - 对于服务器未设置适当标头的问题,你说得很好。但是,如果我需要从一个常量URL下载图像,而该图像偶尔会被替换,但我无法控制服务器端标头怎么办?我想定期(比如每天一次)使Picasso的内存缓存和下载器的磁盘缓存失效,以在合理的时间内获取图像更改。在Picasso中处理这种情况的最佳方法是什么? - Devon Biere
显示剩余8条评论

12

如果你的项目正在使用okhttp库,那么picasso会自动将其作为默认下载器,并且磁盘缓存会自动工作。

假设你正在使用Android Studio,只需在build.gradle文件中添加这两行代码到 dependencies下面,就可以完成设置。(无需额外配置picasso)

dependencies {
    [...]
    compile 'com.squareup.okhttp:okhttp:2.+'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.+'
}

我已经配置了一个 OkHttpClient 实例,用于应用程序中的 REST API 调用。有没有办法将这个实例传递给 Picasso? - marioosh
@marioosh 你可以使用Picasso.Builder类来实现。 - Pavel
需要添加第二个 com.squareup.okhttp:okhttp-urlconnection 吗?还是只需要第一个就可以了? - user5155835
添加这个会导致Picasso无响应。 - Ofek Ron

2
正如这里许多人所指出的那样,OkHttpClient 是缓存的最佳方案。
在使用 OkHttp 进行缓存时,您可能还想通过使用 OkHttp 拦截器更好地控制 HTTP 响应中的 Cache-Control 标头。请参见我的回答此处

1
如先前所述,Picasso使用底层Http客户端的缓存。
HttpUrlConnection内置缓存在真正离线模式下不起作用,如果由于某些原因不想使用OkHttpClient,则可以使用自己实现的磁盘缓存(当然要基于DiskLruCache)。
其中一种方法是通过子类化com.squareup.picasso.UrlConnectionDownloader并在以下位置编写整个逻辑:
@Override
public Response load(final Uri uri, int networkPolicy) throws IOException {
...
}

然后像这样使用您的实现:

new Picasso.Builder(context).downloader(<your_downloader>).build();

这是我实现的 UrlConnectionDownloader,它可以与磁盘缓存一起工作,并且即使在完全离线模式下也可以向 Picasso 提供位图:
public class PicassoBitmapDownloader extends UrlConnectionDownloader {

    private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
    private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB

    @NonNull private Context context;
    @Nullable private DiskLruCache diskCache;

    public class IfModifiedResponse extends Response {

        private final String ifModifiedSinceDate;

        public IfModifiedResponse(InputStream stream, boolean loadedFromCache, long contentLength, String ifModifiedSinceDate) {

            super(stream, loadedFromCache, contentLength);
            this.ifModifiedSinceDate = ifModifiedSinceDate;
        }

        public String getIfModifiedSinceDate() {

            return ifModifiedSinceDate;
        }
    }

    public PicassoBitmapDownloader(@NonNull Context context) {

        super(context);
        this.context = context;
    }

    @Override
    public Response load(final Uri uri, int networkPolicy) throws IOException {

        final String key = getKey(uri);
        {
            Response cachedResponse = getCachedBitmap(key);
            if (cachedResponse != null) {
                return cachedResponse;
            }
        }

        IfModifiedResponse response = _load(uri);

        if (cacheBitmap(key, response.getInputStream(), response.getIfModifiedSinceDate())) {

            IfModifiedResponse cachedResponse = getCachedBitmap(key);
            if (cachedResponse != null) {return cachedResponse;
            }
        }

        return response;
    }

    @NonNull
    protected IfModifiedResponse _load(Uri uri) throws IOException {

        HttpURLConnection connection = openConnection(uri);

        int responseCode = connection.getResponseCode();
        if (responseCode >= 300) {
            connection.disconnect();
            throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
                    0, responseCode);
        }

        long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
        String lastModified = connection.getHeaderField(Constants.HEADER_LAST_MODIFIED);
        return new IfModifiedResponse(connection.getInputStream(), false, contentLength, lastModified);
    }

    @Override
    protected HttpURLConnection openConnection(Uri path) throws IOException {

        HttpURLConnection conn = super.openConnection(path);

        DiskLruCache diskCache = getDiskCache();
        DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(getKey(path));
        if (snapshot != null) {
            String ifModifiedSince = snapshot.getString(1);
            if (!isEmpty(ifModifiedSince)) {
                conn.addRequestProperty(Constants.HEADER_IF_MODIFIED_SINCE, ifModifiedSince);
            }
        }

        return conn;
    }

    @Override public void shutdown() {

        try {
            if (diskCache != null) {
                diskCache.flush();
                diskCache.close();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }

        super.shutdown();
    }

    public boolean cacheBitmap(@Nullable String key, @Nullable InputStream inputStream, @Nullable String ifModifiedSince) {

        if (inputStream == null || isEmpty(key)) {
            return false;
        }

        OutputStream outputStream = null;
        DiskLruCache.Editor edit = null;
        try {
            DiskLruCache diskCache = getDiskCache();
            edit = diskCache == null ? null : diskCache.edit(key);
            outputStream = edit == null ? null : new BufferedOutputStream(edit.newOutputStream(0));

            if (outputStream == null) {
                return false;
            }

            ChatUtils.copy(inputStream, outputStream);
            outputStream.flush();

            edit.set(1, ifModifiedSince == null ? "" : ifModifiedSince);
            edit.commit();

            return true;
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {

            if (edit != null) {
                edit.abortUnlessCommitted();
            }

            ChatUtils.closeQuietly(outputStream);
        }
        return false;
    }

    @Nullable
    public IfModifiedResponse getCachedBitmap(String key) {

        try {
            DiskLruCache diskCache = getDiskCache();
            DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(key);
            InputStream inputStream = snapshot == null ? null : snapshot.getInputStream(0);

            if (inputStream == null) {
                return null;
            }

            return new IfModifiedResponse(inputStream, true, snapshot.getLength(0), snapshot.getString(1));
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    @Nullable
    synchronized public DiskLruCache getDiskCache() {

        if (diskCache == null) {

            try {
                File file = new File(context.getCacheDir() + "/images");
                if (!file.exists()) {
                    //noinspection ResultOfMethodCallIgnored
                    file.mkdirs();
                }

                long maxSize = calculateDiskCacheSize(file);
                diskCache = DiskLruCache.open(file, BuildConfig.VERSION_CODE, 2, maxSize);
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }

        return diskCache;
    }

    @NonNull
    private String getKey(@NonNull Uri uri) {

        String key = md5(uri.toString());
        return isEmpty(key) ? String.valueOf(uri.hashCode()) : key;
    }

    @Nullable
    public static String md5(final String toEncrypt) {

        try {
            final MessageDigest digest = MessageDigest.getInstance("md5");
            digest.update(toEncrypt.getBytes());
            final byte[] bytes = digest.digest();
            final StringBuilder sb = new StringBuilder();
            for (byte aByte : bytes) {
                sb.append(String.format("%02X", aByte));
            }
            return sb.toString().toLowerCase();
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    static long calculateDiskCacheSize(File dir) {

        long available = ChatUtils.bytesAvailable(dir);
        // Target 2% of the total space.
        long size = available / 50;
        // Bound inside min/max size for disk cache.
        return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
    }
}

2
你使用了哪些导入? - Jimit Patel

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