Picasso 内部缓存

作者: HunkDeng | 来源:发表于2016-09-25 14:21 被阅读2919次

    Picasso是Android中常用的图片加载框架,本文注重解析其缓存逻辑。如果你没有使用过picasso,请简单的看下它的主页

    清空缓存

    项目中经常需要你手动清空picasso的缓存,比如用户更新了个人头像。如果你是带着相同问题找到这个帖子的话,那以下就是你的答案。(2016/10/9更新)

    Picasso.with(context).invalidate(url);  // clear bitmap cache in memory
    // 遍历disk cache 的url key,找出你想删的image url,然后remove掉。
    
    // Picasso不提供getDownloader(),所以你得用以下方法绕过。
    OkHttpDownloader okHttpDownloader = new OkHttpDownloader(context);
    OkHttpClient client = ReflectionHelpers.getField(okHttpDownloader, "client");
    picassoDiskCache = client.getCache(); // 拿到disk cache
    Picasso picasso = new Picasso.Builder(context)
            .downloader(okHttpDownloader)
            .build();
    Iterator<String> iterator = picassoDiskCache.urls()
    while (iterator.hasNext()) {
       String url = iterator.next();
       if (imgUrl.equals(url)) {
         // remove disk cache
         iterator.remove();
         break;
       }
     }
    

    加载图片的逻辑

    首先Picasso使用两级缓存模型,内存缓存及硬盘缓存。
    Picasso常用的方法为:

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

    内存缓存

    我们看下当picasso试图加载图片利用内存缓存的逻辑 (以下逻辑摘自上面into(imageView)方法),其中 quickMemoryCacheCheck() 就是对内存缓存进行搜索,命中则使用内存中的缓存:

    // 根据内存设定,如果启用内存缓存则当命中时使用内存图片
    if (shouldReadFromMemoryCache(memoryPolicy)) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        picasso.cancelRequest(target);
        setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
        if (picasso.loggingEnabled) {
          log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
        }
        if (callback != null) {
          callback.onSuccess();
        }
        return;
      }
    }
    

    继续深入这个quickMemoryCacheCheck() 方法

    Bitmap quickMemoryCacheCheck(String key) {
      Bitmap cached = cache.get(key);
      if (cached != null) {
        stats.dispatchCacheHit();
      } else {
        stats.dispatchCacheMiss();
      }
      return cached;
    }
    

    其实就是检查cache这个对象,picasso使用的是自己写的LRU缓存,逻辑和Android SDK的差不多。这个LruCache的内存大小为~15%

    /** Create a cache using an appropriate portion of the available RAM as the maximum size. */
    public LruCache(@NonNull Context context) {  
       this(Utils.calculateMemoryCacheSize(context));
    }
    
    // Utils.calculateMemoryCacheSize
    static int calculateMemoryCacheSize(Context context) {
      ActivityManager am = getService(context, ACTIVITY_SERVICE);
      boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
      int memoryClass = am.getMemoryClass();
      if (largeHeap && SDK_INT >= HONEYCOMB) {
        memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
      }
      // Target ~15% of the available heap.
      return (int) (1024L * 1024L * memoryClass / 7);
    }
    

    它的内存缓存键值可以看出设计者的idea,它使用的是url和显示imageView所要求的属性等。因为即使是同一张图片,在不同大小/效果的imageView所使用的bitmap也是不同的。以下是键值的例子:

    http://i.imgur.com/DAl0KB8.jpg\nresize:540x540
    

    所以在清空一个url对应的内存缓存时的逻辑时这样的(Picasso.invlidate(url)):

    /**
     * Invalidate all memory cached images for the specified {@code uri}.
     *
     * @see #invalidate(String)
     * @see #invalidate(File)
     */
    public void invalidate(@Nullable Uri uri) {
      if (uri != null) {
        cache.clearKeyUri(uri.toString());
      }
    }
    
    // cache.clearKeyUri(uri.toString());
    @Override public final synchronized void clearKeyUri(String uri) {
      int uriLength = uri.length();
      for (Iterator<Map.Entry<String, Bitmap>> i = map.entrySet().iterator(); i.hasNext();) {
        Map.Entry<String, Bitmap> entry = i.next();
        String key = entry.getKey();
        Bitmap value = entry.getValue();
        int newlineIndex = key.indexOf(KEY_SEPARATOR);
        // 如果对应的url相同,删除各种图片属性的bitmap缓存
        if (newlineIndex == uriLength && key.substring(0, newlineIndex).equals(uri)) {
          i.remove();
          size -= Utils.getBitmapBytes(value);
        }
      }
    }  
    

    以上是内存缓存逻辑,以下是硬盘缓存逻辑。因为硬盘缓存实际是控制在okhttp中,这里我们就简单的讲下picasso是怎么自定义它的硬盘缓存的。

    硬盘缓存

    依然从into(imageView)开始

    Action action =
        new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
            requestKey, tag, errorResId);
    picasso.enqueueAndSubmit(action);
    

    在picasso.enqueueAndSubmit()中,跟踪几层逻辑后,其实用的是picasso的downloader去下载响应的图片。而当服务器返回的头中有缓存头的信息的话(比如cache-control等),okhttp就会为我们缓存它。http头样例:

    cache-control: public, max-age=31536000
    

    具体的网络请求逻辑各位可以到github上看下OkHttpDownloader.java (根据不同版本的okhttp依赖Downloader类可能不一样) 这个文件的load()方法。而在初始化OkHttpDownloader时,picasso创建了一个自己的disk cache object,大小是~2%的硬盘空间。默认路径是data/data/your package name/cache/picasso-cache/。

      /** Create new downloader that uses OkHttp. This will install an image cache into your application
       * cache directory.
       */
      public OkHttpDownloader(final Context context) {
        this(Utils.createDefaultCacheDir(context));
      }
    
      /**
       * Create new downloader that uses OkHttp. This will install an image cache into the specified
       * directory.
       *
       * @param cacheDir The directory in which the cache should be stored
       */
      public OkHttpDownloader(final File cacheDir) {
        this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));
      }
    
      /**
       * Create new downloader that uses OkHttp. This will install an image cache into your application
       * cache directory.
       *
       * @param maxSize The size limit for the cache.
       */
      public OkHttpDownloader(final Context context, final long maxSize) {
        this(Utils.createDefaultCacheDir(context), maxSize);
      }
    
      @TargetApi(JELLY_BEAN_MR2)
      static long calculateDiskCacheSize(File dir) {
        long size = MIN_DISK_CACHE_SIZE;
    
        try {
          StatFs statFs = new StatFs(dir.getAbsolutePath());
          //noinspection deprecation
          long blockCount =
              SDK_INT < JELLY_BEAN_MR2 ? (long) statFs.getBlockCount() : statFs.getBlockCountLong();
          //noinspection deprecation
          long blockSize =
              SDK_INT < JELLY_BEAN_MR2 ? (long) statFs.getBlockSize() : statFs.getBlockSizeLong();
          long available = blockCount * blockSize;
          // Target 2% of the total space.
          size = available / 50;
        } catch (IllegalArgumentException ignored) {
        }
    
        // Bound inside min/max size for disk cache.
        return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
      }
    

    这个磁盘缓存是DiskLruCache。至此关于picasso的缓存归纳如下:

    • 加载图片的url及图片属性在内存缓存中存在,如果存在读取内存
    • 如果不命中内存缓存,加载图片到disk和memory中。
    • 内存和硬盘缓存相互独立。

    感谢阅读,欢迎指正!
    注:全文使用的代码片段均来自picasso 2.5.2版本。

    2016/10/9日更新:更新清除硬盘缓存的代码,以友好的方式获取cache对象。当时看的代码是master上,所以是没有okhttp3downloader的,不过2.5.2版本上的okhttpdownloader硬盘缓存的逻辑时一样的,所以无大碍。

    相关文章

      网友评论

      • _Zhaohao:请问一下作者,ReflectionHelpers这个类是哪个包下的,找不到这个类
        _Zhaohao:@HunkDeng 感谢作者的回复,不过找不到rebolectric,应该是robolectric开源库
        HunkDeng:@_Zhaohao 这个是rebolectric这个开源库的 GitHub上可以搜到这个文件
      • ZapFive69:之前对于Picasso了解的不是很透彻,只知其原理。感谢作者详细解读了机制原理。

      本文标题:Picasso 内部缓存

      本文链接:https://www.haomeiwen.com/subject/enuakttx.html