剖析Picasso中的内存缓存机制——LruCache

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html



众所周知,Picasso是一个优秀的Android图片加载库。本篇并不讨论picasso的使用,而是来谈一谈picasso的缓存机制。
我们知道,目前主流的图片解决方案大部分都是三级缓存,即内存缓存、本地缓存和服务器缓存。这其中内存和本地缓存是在客户端实现的,picasso中也使用了这种方案。
Picasso的本地缓存我们在另外的篇幅中来谈,今天我们重点谈谈picasso的内存缓存。
在picasso源码下我们可以看到一个类——LruCache,这就是今天的主角,picasso的内存缓存就在这里实现的。

那么,为什么叫LruCache?
LRU是一种内存管理算法,是Least Recently Used的缩写。该算法的详细解释大家可以自行查阅。简单来说就是一个有序的集合,新添加的数据处于顶部,同时如果某个数据被访问,那么这个数据会被移至顶部;而当淘汰数据时,会从底部进行淘汰。如图:
剖析Picasso中的内存缓存机制——LruCache_第1张图片
这个算法的核心思想是,如果一个数据近期被访问,那么被再次访问的可能性会远高于那些很久未被访问的数据,所以优先淘汰那些很久未被访问的数据。

了解了LRU算法,我们回过头再来看LruCache是如何实现这个思想的呢?
在LruCache源码中我们可以看到使用的是一个LinkedHashMap来进行数据存储的。LinkedHashMap是HashMap的子类,不同于HashMap之处在于它同时实现了双向链表,保证了数据的插入顺序,所以实际上最新插入的数据处于链表的尾部。而且 LinkedHashMap可以用来实现LRU算法。怎么实现?我们来看看LruCache的构造函数
public  LruCache( int maxSize) {
  if (maxSize <=  0) {
    throw new IllegalArgumentException( "Max size must be positive.") ;
  }
  this. maxSize = maxSize ;
  this. map new LinkedHashMap, Bitmap>( 0 0.75f , true) ;
}
可以看到初始化 LinkedHashMap时使用三个参数的构造函数
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
重点在accessOrder这个参数,当为false表示按插入顺序,当为true表示按访问顺序。 LinkedHashMap内部通过重写get函数和内部类Entry的recordAccess方法来实现这部分逻辑,代码如下:
  1.     public V get(Object key) {  
  2.         Entry e = (Entry)getEntry(key);  
  3.         if (e == null)  
  4.             return null;  
  5.         e.recordAccess(this);  
  6.         return e.value;  
  7.     }  

  1.         void recordAccess(HashMap m) {  
  2.             LinkedHashMap lm = (LinkedHashMap)m;  
  3.             if (lm.accessOrder) {  
  4.                 lm.modCount++;  
  5.                 remove();  
  6.                 addBefore(lm.header);  
  7.             }  
  8.         }
可以看到如果调用get方法就会调用recordAccess方法。而在 recordAccess中如果 accessOrder为true,会从集合中将这个元素remove并插入到链表尾部。而且注意在HashMap的put方法中,当插入的元素已经存在,也同样会调用 recordAccess方法,源码如下:
public V put(K key, V value) {
  if (key == null)
    return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  for (Entry e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
      V oldValue = e.value;
      e.value = value;
      e.recordAccess(this);
      return oldValue;
    }
  }
  modCount++;
  //将key-value添加到table[i]处
  addEntry(hash, key, value, i);
  return null;
}



以上已经实现了访问排序,那么淘汰机制又是如何的呢?
在Picasso中使用了一个阀值来限制缓存的整体大小,在上面的构造函数中我们可以看到maxSize这个参数,这个就是缓存的最大阀值。如果我们没有自己定义这个阀值,那么picasso会使用一个默认大小,我们来看看LruCache的另外一个构造函数:
public  LruCache(Context context) {
  this(Utils. calculateMemoryCacheSize(context)) ;
}
可以看到是通过Utils的相关函数计算出的默认阀值,通过源码我们来看看具体怎么得到的
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  1024 1024 memoryClass /  7 ;
}
其中memoryClass是单个应用的最大内存限制,这个数值在不同的设备上是不一样的。
注意可以看到在HONEYCOME以上版本使用了不同的函数:getMemoryClass获取的是系统为应用分配的内存,不包含额外的补充;但是我们可以在manifest中将LargeHeap设为true来获取最大内存,而getLargeMemoryClass获取的就是这个补充后的内存大小。本篇就不详细展开了。
memoryClass的单位是MB,所以最后返回的阀值是应用最大内存的1/7,也就是说picasso的内存缓存最大会占用应用最大内存的15%左右。所以它不是一个固定值,而且因设备不同而不同的。

那么如何使用这个阀值来保证缓存空间大小的呢?我们来看看LruCache的set函数:
@Override  public void  set(String key Bitmap bitmap) {
  if (key ==  null || bitmap ==  null) {
    throw new NullPointerException( "key == null || bitmap == null") ;
  }

  Bitmap previous ;
  synchronized ( this) {
    putCount++ ;
    size += Utils. getBitmapBytes(bitmap) ;
    previous =  map.put(key bitmap) ;
    if (previous !=  null) {
      size -= Utils. getBitmapBytes(previous) ;
    }
  }

  trimToSize( maxSize) ;
}
可以看到当set一个数据时,会增加缓存的当前大小,通过Utils.getBitmapBytes函数来获取图片大小并加至size参数上。
这里可以注意到,如果是已经存在的元素,则增加大小后还需要减去旧图片的大小,保证size的正确性。
最后会调用trimToSize函数,这个函数源码如下:
private void  trimToSize( int maxSize) {
  while ( true) {
    String key ;
    Bitmap value ;
    synchronized ( this) {
      if ( size || ( map.isEmpty() &&  size !=  0)) {
        throw new IllegalStateException(
            getClass().getName() +  ".sizeOf() is reporting inconsistent results!") ;
      }

      if ( size <= maxSize ||  map.isEmpty()) {
        break;
      }

      Map.Entry, Bitmap> toEvict =  map.entrySet().iterator().next() ;
      key = toEvict.getKey() ;
      value = toEvict.getValue() ;
      map.remove(key) ;
      size -= Utils. getBitmapBytes(value) ;
      evictionCount++ ;
    }
  }
}
当缓存大小超过阀值,开始从头遍历map,清除图片缓存并改变缓存大小,直到缓存大小不再超过阀值。
因为在map中尾部的数据是最新的,而头部是最久未访问的,这样便实现了淘汰机制。

综上,我们剖析了picasso的内存缓存机制,它是在LinkedHashMap的基础上实现了LRU算法来进行内存管理,并且根据系统默认了一个合理的阀值,当缓存过大时会先清除久未使用的缓存。这样既加速了图片的加载,同时也保证了不过分消耗应用的内存。

这里要提一下Facebook开源的Fresco,同样作为图片加载解决方案,但是fresco使用了另外一种内存缓存——匿名共享内存。匿名共享内存实际上是在c/c++层创建使用的,所以不受Android虚拟机的内存限制,同时也不占用应用的最大内存。由于不少本篇内容,就简单介绍一下,有时间我会专门整理一篇文章来细说。

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html


你可能感兴趣的:(android,Android进阶之路)