我如何在Picasso中使用磁盘缓存?

127

我正在使用Picasso在我的安卓应用中展示图片:

/**
* load image.This is within a activity so this context is activity
*/
public void loadImage (){
    Picasso picasso = Picasso.with(this); 
    picasso.setDebugging(true);
    picasso.load(quiz.getImageUrl()).into(quizImage);
}

我已经启用了调试功能,它总是显示红色和绿色,但从不显示黄色。

现在,如果我下次加载同一张图片且没有网络连接,则该图片将无法加载。

问题:

  1. 它没有本地磁盘缓存吗?
  2. 我如何启用磁盘缓存,因为我将多次使用相同的图像?
  3. 我是否需要在Android清单文件中添加某些磁盘权限?

我遇到了相同的问题。它不会缓存! - Jono
大家应该看一下Facebook的Fresco库。它的缓存管理非常棒。 - Michel Fortes
9个回答

244

这是我所做的,效果很好。

首先,在应用程序模块的gradle构建文件中添加OkHttp:

compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.10.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'

然后创建一个扩展 Application 的类

import android.app.Application;

import com.jakewharton.picasso.OkHttp3Downloader;
import com.squareup.picasso.Picasso;

public class Global extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        Picasso.Builder builder = new Picasso.Builder(this);
        builder.downloader(new OkHttp3Downloader(this,Integer.MAX_VALUE));
        Picasso built = builder.build();
        built.setIndicatorsEnabled(true);
        built.setLoggingEnabled(true);
        Picasso.setSingletonInstance(built);

    }
}

将其添加到清单文件中,如下所示:

<application
        android:name=".Global"
        .. >

</application>

现在就像往常一样使用Picasso,不需要做任何更改。

编辑:

如果您只想使用缓存的图像,请按以下方式调用该库。我注意到如果我们不添加networkPolicy,在完全离线的情况下,即使它们被缓存,图像也不会显示出来。以下代码解决了这个问题。

Picasso.with(this)
            .load(url)
            .networkPolicy(NetworkPolicy.OFFLINE)
            .into(imageView);

编辑 #2

上面代码的问题在于,如果你清除了缓存,Picasso会继续在缓存中离线寻找它并失败,以下代码示例查看本地缓存,如果没有离线找到,它将在线获取并补充缓存。

Picasso.with(getActivity())
.load(imageUrl)
.networkPolicy(NetworkPolicy.OFFLINE)
.into(imageView, new Callback() {
    @Override
    public void onSuccess() {

    }

    @Override
    public void onError() {
        //Try again online if cache failed
        Picasso.with(getActivity())
                .load(posts.get(position).getImageUrl())
                .error(R.drawable.header)
                .into(imageView, new Callback() {
            @Override
            public void onSuccess() {

            }

            @Override
            public void onError() {
                Log.v("Picasso","Could not fetch image");
            }
        });
    }
});

@ArtjomB。是的,那很有道理。已编辑! - Sanket Berde
5
感谢您迅速回复,但我发现如果应用程序在后台运行(离线时),图像只会从内存中出现。如果我关闭应用程序,也就是清除正在运行的应用程序,然后再次打开我的应用程序,图像不会从缓存中加载。我设置了默认加载错误的图片。可能有什么问题? - TheDevMan
@TheDevMan 是的,我也注意到了这个问题。我会发布一个编辑支持完全离线操作。 - Sanket Berde
1
也许 Picasso 已经改变了事物的工作方式,因为对我来说,在没有 okhttp 和网络策略的情况下它仍然可以正常工作。在新的启动中,它立即从磁盘获取图像,并且当网络离线时,它仍然可以正常显示它们。 - zeeshan
1
使用okhttp3.OkHttpClient库时,您必须使用OkHttp3Downloader类,该类来自于compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' - VKostenc
显示剩余17条评论

47

1) 第一个问题的答案: 根据Picasso With() 方法文档

全局默认的 Picasso 实例是通过 with() 方法自动初始化的,其默认值适用于大部分实现。

  • LRU 内存缓存占可用应用内存的15%
  • 磁盘缓存占2%的存储空间,最高50MB,但不少于5MB。

但是 默认的全局 Default Picasso 的磁盘缓存操作仅适用于 API 14+。

2) 第二个问题的答案: Picasso 使用 HTTP 客户端请求进行 Disk Cache 操作,因此您可以创建自己的 http 请求标头,具有属性 Cache-Controlmax-age。并且可以通过以下方式创建自己的静态 Picasso 实例:

1] HttpResponseCache (注意: 仅适用于 API 13+)
2] OkHttpClient (适用于所有 API)

使用 OkHttpClient 创建自己的静态 Picasso 类的示例:

  • 首先创建一个新类获取您自己的单例 picasso 对象

import android.content.Context;
import com.squareup.picasso.Downloader;
import com.squareup.picasso.OkHttpDownloader;
import com.squareup.picasso.Picasso;

public class PicassoCache {

    /**
     * Static Picasso Instance
     */
    private static Picasso picassoInstance = null;

    /**
     * PicassoCache Constructor
     *
     * @param context application Context
     */
    private PicassoCache (Context context) {

        Downloader downloader   = new OkHttpDownloader(context, Integer.MAX_VALUE);
        Picasso.Builder builder = new Picasso.Builder(context);
            builder.downloader(downloader);

        picassoInstance = builder.build();
    }

    /**
     * Get Singleton Picasso Instance
     *
     * @param context application Context
     * @return Picasso instance
     */
    public static Picasso getPicassoInstance (Context context) {

        if (picassoInstance == null) {

            new PicassoCache(context);
            return picassoInstance;
        }

        return picassoInstance;
    }

} 
  • 使用自己的单例picasso对象,而不是Picasso.With()

  • PicassoCache.getPicassoInstance(getContext()).load(imagePath).into(imageView)

    3)针对第三个问题的回答:您不需要任何磁盘权限来进行磁盘缓存操作。

    参考文献有关磁盘缓存的 Github 问题,由@jake-wharton回答了两个问题 -> 问题1问题2


    4
    不,如果应用程序关闭,这是不起作用的。在应用程序被强制停止后,所有图像都消失了。 - nbumakov
    2
    这个问题给我报了一个错误:FATAL EXCEPTION: main java.lang.NoClassDefFoundError: com.squareup.okhttp.OkHttpClient - CIRCLE
    @CIRCLE 抱歉回复晚了。要使用示例,您需要先下载 okhttp 包和 okhttp 使用的 okio 包。 - ahmed hamdy
    这对我不起作用。每次我滚动到viewPager中的图像位置时,它们都会重新加载。 - Charu
    1
    您的解决方案存在问题:不要将 Android 上下文类放在静态字段中(对 Context 有指向的 Picasso 静态引用);这会导致内存泄漏(并且还会破坏 Instant Run)。 - user924
    显示剩余4条评论

    23

    缓存方面,我会使用OkHttp拦截器来控制缓存策略。可以查看OkHttp库中包含的示例。

    RewriteResponseCacheControl.java

    以下是我如何与Picasso一起使用它的方法:

    OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.networkInterceptors().add(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response originalResponse = chain.proceed(chain.request());
                return originalResponse.newBuilder().header("Cache-Control", "max-age=" + (60 * 60 * 24 * 365)).build();
            }
        });
    
        okHttpClient.setCache(new Cache(mainActivity.getCacheDir(), Integer.MAX_VALUE));
        OkHttpDownloader okHttpDownloader = new OkHttpDownloader(okHttpClient);
        Picasso picasso = new Picasso.Builder(mainActivity).downloader(okHttpDownloader).build();
        picasso.load(imageURL).into(viewHolder.image);
    

    1
    networkInterceptors()方法不再起作用,它返回一个不可变的列表。 - noev
    1
    在OkHttp 3.x中,您可以使用构建器模式(请参见https://github.com/square/okhttp/wiki/Interceptors)添加拦截器。 - Gaurav B

    23

    最新版本2.71828 这是你的答案。

    Q1: 它没有本地磁盘缓存吗?

    A1: Picasso内置了默认缓存和请求流程就像这样。

    App -> Memory -> Disk -> Server
    

    无论他们在哪里首次遇到他们的形象,他们都会使用那个形象,然后停止请求流程。 那么响应流程呢?别担心,这就是它。
    Server -> Disk -> Memory -> App
    

    默认情况下,它们将首先存储到本地磁盘中,以便进行扩展的缓存保留。然后,内存用于缓存的实例使用。
    您可以使用Picasso中的内置指示器来查看启用此功能的图像来源。
    Picasso.get().setIndicatorsEnabled(true);
    

    它会在您的图片的左上角显示一个旗帜。

    • 红色旗帜表示图片来自服务器。(首次加载时不缓存)
    • 蓝色旗帜表示图片来自本地磁盘。(缓存)
    • 绿色旗帜表示图片来自内存。(实例缓存)

    Q2:如何启用磁盘缓存,因为我将多次使用相同的图片?

    A2:您不需要启用它。这是默认设置。

    您需要做的是在您希望图片始终保持新鲜时禁用它。有两种禁用缓存的方法。

    1. .memoryPolicy()设置为NO_CACHE和/或NO_STORE,流程将如下所示。

    NO_CACHE将跳过从内存中查找图片。

    App -> Disk -> Server
    

    NO_STORE在首次加载图片时会跳过将图片存储在内存中。

    Server -> Disk -> App
    

    .networkPolicy()设置为NO_CACHE和/或NO_STORE,流程将如下所示。 NO_CACHE将跳过从磁盘查找图像的步骤。
    App -> Memory -> Server
    

    NO_STORE在首次加载图片时会跳过将图片存储到磁盘的步骤。

    Server -> Memory -> App
    

    你无法完全禁用图片的缓存。这里有一个例子。
    Picasso.get().load(imageUrl)
                 .memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE)
                 .networkPolicy(NetworkPolicy.NO_CACHE, NetworkPolicy.NO_STORE)
                 .fit().into(banner);
    

    完全不缓存和不存储的流程将如下所示。
    App -> Server //Request
    
    Server -> App //Response
    

    所以,你可能也需要这个来减少你的应用存储使用量。
    问题3:我需要在Android清单文件中添加一些磁盘权限吗?
    答案3:不需要,但别忘了为你的HTTP请求添加INTERNET权限。

    7

    1) Picasso默认具有缓存功能(请参见Ahmed Hamdy的答案)

    2) 如果您确实需要从磁盘缓存中获取图像,然后再从网络获取,我建议您编写自己的下载器:

    public class OkHttpDownloaderDiskCacheFirst extends OkHttpDownloader {
        public OkHttpDownloaderDiskCacheFirst(OkHttpClient client) {
            super(client);
        }
    
        @Override
        public Response load(Uri uri, int networkPolicy) throws IOException {
            Response responseDiskCache = null;
            try {
                responseDiskCache = super.load(uri, 1 << 2); //NetworkPolicy.OFFLINE
            } catch (Exception ignored){} // ignore, handle null later
    
            if (responseDiskCache == null || responseDiskCache.getContentLength()<=0){
                return  super.load(uri, networkPolicy); //user normal policy
            } else {
                return responseDiskCache;
            }
    
        }
    }
    

    在Application单例的OnCreate方法中使用它和Picasso:

            OkHttpClient okHttpClient = new OkHttpClient();
    
            okHttpClient.setCache(new Cache(getCacheDir(), 100 * 1024 * 1024)); //100 MB cache, use Integer.MAX_VALUE if it is too low
            OkHttpDownloader downloader = new OkHttpDownloaderDiskCacheFirst(okHttpClient); 
    
            Picasso.Builder builder = new Picasso.Builder(this);
    
            builder.downloader(downloader);
    
            Picasso built = builder.build();
    
            Picasso.setSingletonInstance(built);
    

    3) 默认应用程序缓存文件夹无需权限


    2
    我不知道那个解决方案有多好,但它绝对是我在应用程序中使用的最简单的方法,并且运行良好。
    您可以像这样加载图像:
    public void loadImage (){
    Picasso picasso = Picasso.get(); 
    picasso.setIndicatorsEnabled(true);
    picasso.load(quiz.getImageUrl()).into(quizImage);
    }
    

    您可以这样获取
    Bitmap bitmap = Picasso.get().load(quiz.getImageUrl()).get();
    

    现在将该Bitmap转换为JPG文件并存储在缓存中,下面是获取位图并将其缓存的完整代码

    Thread thread = new Thread() {
     public void run() {
     File file = new File(getCacheDir() + "/" +member.getMemberId() + ".jpg");
    
    try {
          Bitmap bitmap = Picasso.get().load(uri).get();
          FileOutputStream fOut = new FileOutputStream(file);                                        
          bitmap.compress(Bitmap.CompressFormat.JPEG, 100,new FileOutputStream(file));
    fOut.flush();
    fOut.close();
        }
    catch (Exception e) {
      e.printStackTrace();
        }
       }
    };
         thread.start();
      })
    
    

    Picassoget() 方法需要在单独的线程中调用,我也在同一线程中保存了该图像。

    图像保存后,您可以使用以下代码获取所有文件:

    List<File> files = new LinkedList<>(Arrays.asList(context.getExternalCacheDir().listFiles()));
    

    现在,您可以像下面这样找到您要查找的文件:
    for(File file : files){
                    if(file.getName().equals("fileyouarelookingfor" + ".jpg")){ // you need the name of the file, for example you are storing user image and the his image name is same as his id , you can call getId() on user to get the file name
                        Picasso.get() // if file found then load it
                                .load(file)
                                .into(mThumbnailImage);
                        return; // return 
                    }
            // fetch it over the internet here because the file is not found
           }
    

    Picasso的版本已更改并需要更新。我已建议进行编辑!回答非常好。经过一些更改后,它对我起作用了。 - Arpit Anand
    1
    @ArpitAnand 谢谢,您可以自由改善回答,我将接受这些更改。 - Abhinav Chauhan

    1
    Application.onCreate中添加以下代码,然后正常使用。
        Picasso picasso = new Picasso.Builder(context)
                .downloader(new OkHttp3Downloader(this,Integer.MAX_VALUE))
                .build();
        picasso.setIndicatorsEnabled(true);
        picasso.setLoggingEnabled(true);
        Picasso.setSingletonInstance(picasso);
    

    如果您首先缓存图像,然后在ProductImageDownloader.doBackground中执行以下操作
    final Callback callback = new Callback() {
                @Override
                public void onSuccess() {
                    downLatch.countDown();
                    updateProgress();
                }
    
                @Override
                public void onError() {
                    errorCount++;
                    downLatch.countDown();
                    updateProgress();
                }
            };
            Picasso.with(context).load(Constants.imagesUrl+productModel.getGalleryImage())
                    .memoryPolicy(MemoryPolicy.NO_CACHE).fetch(callback);
            Picasso.with(context).load(Constants.imagesUrl+productModel.getLeftImage())
                    .memoryPolicy(MemoryPolicy.NO_CACHE).fetch(callback);
            Picasso.with(context).load(Constants.imagesUrl+productModel.getRightImage())
                    .memoryPolicy(MemoryPolicy.NO_CACHE).fetch(callback);
    
            try {
                downLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            if(errorCount == 0){
                products.remove(productModel);
                productModel.isDownloaded = true;
                productsDatasource.updateElseInsert(productModel);
            }else {
                //error occurred while downloading images for this product
                //ignore error for now
                // FIXME: 9/27/2017 handle error
                products.remove(productModel);
    
            }
            errorCount = 0;
            downLatch = new CountDownLatch(3);
    
            if(!products.isEmpty() /*&& testCount++ < 30*/){
                startDownloading(products.get(0));
            }else {
                //all products with images are downloaded
                publishProgress(100);
            }
    

    您可以像平常一样加载图像,也可以使用磁盘缓存

        Picasso.with(this).load(Constants.imagesUrl+batterProduct.getGalleryImage())
            .networkPolicy(NetworkPolicy.OFFLINE)
            .placeholder(R.drawable.GalleryDefaultImage)
            .error(R.drawable.GalleryDefaultImage)
            .into(viewGallery);
    

    注意:
    红色表示从网络获取的图像。
    绿色表示从缓存中获取的图像。
    蓝色表示从磁盘内存中获取的图像。
    在发布应用程序之前,如果不需要,请删除或将其设置为false:picasso.setLoggingEnabled(true);picasso.setIndicatorsEnabled(true);。谢谢。

    0
    我使用了这段代码并且运行良好,或许对你有用:
    public static void makeImageRequest(final View parentView,final int id, final String imageUrl) {
    
        final int defaultImageResId = R.mipmap.user;
        final ImageView imageView = (ImageView) parentView.findViewById(id);
        Picasso.with(context)
                .load(imageUrl)
                .networkPolicy(NetworkPolicy.OFFLINE)
                .into(imageView, new Callback() {
                    @Override
                    public void onSuccess() {
                    Log.v("Picasso","fetch image success in first time.");
                    }
    
                    @Override
                    public void onError() {
                        //Try again online if cache failed
                        Log.v("Picasso","Could not fetch image in first time...");
                        Picasso.with(context).load(imageUrl).networkPolicy(NetworkPolicy.NO_CACHE)
                                .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE).error(defaultImageResId)
                                .into(imageView, new Callback() {
    
                                    @Override
                                    public void onSuccess() {
                                        Log.v("Picasso","fetch image success in try again.");
                                    }
    
                                    @Override
                                    public void onError() {
                                      Log.v("Picasso","Could not fetch image again...");
                                    }
    
                                });
                    }
                });
    
    }
    

    -2

    Glide的代码量比Picasso多5倍。如果已经在使用okhttp,最好使用Picasso。 - Alexander Farber
    2
    两个库都使用缓存,但缓存方式不同: https://medium.com/@multidots/glide-vs-picasso-930eed42b81d - Orri

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