详细解读LruCache类

LruCache是android提供的一个缓存工具类,其算法是最近最少使用算法。它把最近使用的对象用“强引用”存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前就从内存中移除。其在API12被引进,低版本可以用support包中的类。

一、分析源码

这个源码是从网上找的,自己懒得去找源码了。

具体分析也是来自网络:http://www.open-open.com/lib/view/open1385474073171.html

package com.kale.lrucachetest;

import java.util.LinkedHashMap;
import java.util.Map;
/**
 * 它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
 */

/**
 * A cache that holds strong references to a limited number of values. Each time
 * a value is accessed, it is moved to the head of a queue. When a value is
 * added to a full cache, the value at the end of that queue is evicted and may
 * become eligible for garbage collection.
 * Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。
 * 当cache已满的时候加入新的item时,在队列尾部的item会被回收。
 * 

* If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. 如果你cache的某个值需要明确释放,重写entryRemoved() *

* If a cache miss should be computed on demand for the corresponding keys, * override {@link #create}. This simplifies the calling code, allowing it to * assume a value will always be returned, even when there's a cache miss. * 如果key相对应的item丢掉啦,重写create().这简化了调用代码,即使丢失了也总会返回。 *

* By default, the cache size is measured in the number of entries. Override * {@link #sizeOf} to size the cache in different units. For example, this cache * is limited to 4MiB of bitmaps: 默认cache大小是测量的item的数量,重写sizeof计算不同item的 大小。 * *

 * {@code
 * int cacheSize = 4 * 1024 * 1024; // 4MiB
 * LruCache bitmapCache = new LruCache(cacheSize) {
 * protected int sizeOf(String key, Bitmap value) {
 * return value.getByteCount();
 * }
 * }}
 * 
* *

* This class is thread-safe. Perform multiple cache operations atomically by * synchronizing on the cache: * *

 * {@code
 * synchronized (cache) {
 * if (cache.get(key) == null) {
 * cache.put(key, value);
 * }
 * }}
 * 
* *

* This class does not allow null to be used as a key or value. A return value * of null from {@link #get}, {@link #put} or {@link #remove} is unambiguous: * the key was not in the cache. 不允许key或者value为null * 当get(),put(),remove()返回值为null时,key相应的项不在cache中 */ public class LruCache { private final LinkedHashMap map; /** Size of this cache in units. Not necessarily the number of elements. */ private int size; // 已经存储的大小 private int maxSize; // 规定的最大存储空间 private int putCount; // put的次数 private int createCount; // create的次数 private int evictionCount; // 回收的次数 private int hitCount; // 命中的次数 private int missCount; // 丢失的次数 /** * @param maxSize * for caches that do not override {@link #sizeOf}, this is the * maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this * cache. */ public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap(0, 0.75f, true); } /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. 通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部, * 如果item的value没有被cache或者不能被创建,则返回null。 */ public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; // 命中 return mapValue; } missCount++; // 丢失 } /* * Attempt to create a value. This may take a long time, and the map may * be different when create() returns. If a conflicting value was added * to the map while create() was working, we leave that value in the map * and release the created value. 如果丢失了就试图创建一个item */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++;// 创建++ mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put // 如果前面存在oldValue,那么撤销put() map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } } /** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { // 返回的先前的value值 size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; } /** * @param maxSize * the maximum size of the cache before returning. May be -1 to * evict even 0-sized elements. 清空cache空间 */ private void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize) { break; } Map.Entry toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } /** * Removes the entry for {@code key} if it exists. 删除key相应的cache项,返回相应的value * * @return the previous value mapped by {@code key}. */ public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * 当item被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用, 或者替换item值时put调用,默认实现什么都没做。 *

* The method is called without synchronization: other threads may access * the cache while this method is executing. * * @param evicted * true if the entry is being removed to make space, false if the * removal was caused by a {@link #put} or {@link #remove}. * true---为释放空间被删除;false---put或remove导致 * @param newValue * the new value for {@code key}, if it exists. If non-null, this * removal was caused by a {@link #put}. Otherwise it was caused * by an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { } /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. 当某Item丢失时会调用到,返回计算的相应的value或者null *

* The method is called without synchronization: other threads may access * the cache while this method is executing. * *

* If a value for {@code key} exists in the cache when this method returns, * the created value will be released with {@link #entryRemoved} and * discarded. This can occur when multiple threads request the same key at * the same time (causing multiple values to be created), or when one thread * calls {@link #put} while another is creating a value for the same key. */ protected V create(K key) { return null; } private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size is * the number of entries and max size is the maximum number of entries. * 返回用户定义的item的大小,默认返回1代表item的数量,最大size就是最大item值 *

* An entry's size must not change while it is in the cache. */ protected int sizeOf(K key, V value) { return 1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. * 清空cacke */ public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final int size() { return size; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. */ public synchronized final int maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } /** * Returns the number of times {@link #create(Object)} returned a value. */ public synchronized final int createCount() { return createCount; } /** * Returns the number of times {@link #put} was called. */ public synchronized final int putCount() { return putCount; } /** * Returns the number of values that have been evicted. 返回被回收的数量 */ public synchronized final int evictionCount() { return evictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. 返回当前cache的副本,从最近最少访问到最多访问 */ public synchronized final Map snapshot() { return new LinkedHashMap(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } }

View Code

源码挺长的,内部注释也已经十分详尽了,所以有空可以看看。为了方便说明,下面说下我的分析:

  1. 其中用到的数据对象是LinkedHashMap,所以不要把这个类想的多么深不可测,还是数据结构 + 算法。既然用到了这个map,自然就要有添加修改和删除操作了,用到了最近最少使用算法,自然就要用到优先级了。
  2. 作为缓存,肯定有一个缓存的大小,这个大小是可以设定的(自定义sizeOf())。当你访问了一个item(需要缓存的对象),这个item应该被加入到内存中,然后移动到一个队列的顶部,如此循环后这个队列的顶部应该是最近访问的item了,而队尾部就是很久没有访问的item,这样我们就应该对队尾部的item优先进行回收操作。
  3. 因为用到了HashMap,那么就有这个数据存储对象的特点(KEY-VALUE),放入这个map的item应该会被强引用,要回收这个对象的时候是让这个key为空,这样就让有向图找不到对应的value,最终被GC。
  4. 缓存的最大特点是不做重复的劳动,如果你之前已经缓存过这个item了,当你再次想要缓存这个item时,应该会先判断是否已经缓存好了,如果已经缓存,那么就不执行添加的操作。
  5. 我们应该能通过某个方法来清空缓存,这个缓存在app被退出后就自动清理,不会常驻内存。
  6. sizeof()方法。这个方法默认返回的是你缓存的item数目,如果你想要自定义size的大小,直接重写这个方法,返回自定义的值即可。
    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units. The default implementation returns 1 so that size is
     * the number of entries and max size is the maximum number of entries.
     * 返回用户定义的item的大小,默认返回1代表item的数量,最大size就是最大item值
     * 

* An entry's size must not change while it is in the cache. */ protected int sizeOf(K key, V value) { return 1; }

  • 7. 如果你cache的某个值需要明确释放,重写entryRemoved()方法。这个方法会在元素被put或remove时调用,源码默认是空实现的。
    /**
     * Called for entries that have been evicted or removed. This method is
     * invoked when a value is evicted to make space, removed by a call to
     * {@link #remove}, or replaced by a call to {@link #put}. The default
     * implementation does nothing.
     * 当item被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用, 或者替换item值时put调用,默认实现什么都没做。
     * 

* The method is called without synchronization: other threads may access * the cache while this method is executing. * * @param evicted * true if the entry is being removed to make space, false if the * removal was caused by a {@link #put} or {@link #remove}. * true---为释放空间被删除;false---put或remove导致 * @param newValue * the new value for {@code key}, if it exists. If non-null, this * removal was caused by a {@link #put}. Otherwise it was caused * by an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { }

通过判断这里面的evicted值就知道当前进行的是什么操作,你可以在这里进行你想要的操作。

 

二、初始化LruCache

2.1 定义cache大小

初始化这个cache前需要设定这个cache的大小,这里的大小官方推荐是用当前app可用内存的八分之一,当然你可以视情况而定。通过:

final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

就可以得到当前app可用的内存。

为了全局调用,我在application类中定义了计算cache大小的方法。

package com.kale.lrucachetest;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;

public class KaleApplication extends Application{
    
    /**
     * @description 
     *
     * @param context
     * @return 得到需要分配的缓存大小,这里用八分之一的大小来做
     */
    public int getMemoryCacheSize() {
        // Get memory class of this device, exceeding this amount will throw an
        // OutOfMemory exception.
        final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

        // Use 1/8th of the available memory for this memory cache.
        return 1024 * 1024 * memClass / 8;
    }
}

 

2.2 初始化类

  /* 内存缓存 */
    private LruCache mMemoryCache;

设定缓存时要设定泛型,针对的是hashMap,你可以当作是key-value。我这里缓存的是bitmap,用到的key是string对象。

final int memoryCache = ((KaleApplication) getApplication()).getMemoryCacheSize();
        Log.d(TAG, "cache size = " + memoryCache / 1024 / 1024 + "M");
        mMemoryCache = new LruCache(memoryCache) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                return bitmap.getByteCount() / 1024;
            }
        }; // 初始化

我通过缓存的值来初始化了cache对象,然后重写了sizeOf()方法。

 

三、添加/删除缓存

当我们初始化缓存后我们就应该能给这个缓存添加对象和移除对象。

    /**
     * @description 将bitmap添加到内存中去
     *
     * @param key
     * @param bitmap
     */
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * @description 通过key来从内存缓存中获得bitmap对象
     *
     * @param key
     * @return
     */
    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

 

四、模拟从网络下载图片并加入缓存

4.1 判断是否已经缓存过

一般我们都是将图片显示到listview或者是gridView中,适配器读取view前应该判断在不在缓存中,如果在就直接显示,如果不在就从网络下载,其中可能还要用到viewholder类和滑动监听器来提高流畅性。

/**
     * @description 将bitmap加载到imageview中去
     *
     * @param resId
     * @param imageView
     */
    private void loadBitmapToImageView(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);

        final Bitmap bitmap = getBitmapFromMemCache(imageKey); // 先看这个资源在不在内存中,如果在直接读取为bitmap,否则返回null
        if (bitmap != null) {
            Log.d(TAG, "in memory");
            imageView.setImageBitmap(bitmap);
        } else {
            Log.d(TAG, "not in memory");
            imageView.setImageResource(R.drawable.ic_launcher); // 如果没有在内存中,先显示默认的图片,然后启动线程去下载图片
            BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            task.execute(resId); // 启动线程,模拟从网络下载图片,下载后加入缓存
        }
    }

这个方法是典型的加载模式,看缓存,如果没有就去启动asyncTask下载图片,由于内部类会保留外部类的强引用,所以asyncTask不应该作为内部类,而且一般用在10s内中的io操作,这里为了说明方便和尽可能符合官方实例,就还是用了asyncTask来说明。

4.2 异步任务中的操作

package com.kale.lrucachetest;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

public class BitmapWorkerTask extends AsyncTask{

    private MainActivity mActivity;
    private ImageView mImageView;
    
    public BitmapWorkerTask(ImageView imageView) {
        // TODO 自动生成的构造函数存根
        mImageView = imageView;
        mActivity = (MainActivity) imageView.getContext(); // 初始化activity
    }
    
    @Override
    protected Bitmap doInBackground(Integer... params) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Bitmap  bitmap = BitmapFactory.decodeResource(mActivity.getResources(), R.drawable.kale);
        mActivity.addBitmapToMemoryCache(String.valueOf(R.drawable.kale), bitmap);
        return bitmap;
    }
    
    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (result != null) {
            mImageView.setImageBitmap(result); // 将bitmap设置到imageView中去
        }
    }

}

线程暂停1秒(模拟从网络下载),然后得到图片(网络访问正常的情况下),得到后将bitmap放入缓存中,最后在imageview中展示。

 

五、通过Fragment来保存缓存

屏幕方向改变会导致Android摧毁正在运行的Activity,然后使用新的配置从新启动该Activity (详情,参考这里 Handling Runtime Changes)。

需要注意避免在配置改变的时候导致重新处理所有的图片,从而提高用户体验。

幸运的是,您在 使用内存缓存 部分已经有一个很好的图片缓存了。该缓存可以通过Fragment (Fragment会通过setRetainInstance(true)函数保存起来)来传递给新的Activity。

当Activity重新启动 后,Fragment 被重新附加到Activity中,您可以通过该Fragment来获取缓存对象。下面是一个在 Fragment中保存缓存的示例:

RetainFragment

package com.kale.bitmaptest;

import android.app.Fragment;
import android.app.FragmentManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.LruCache;

public class RetainFragment extends Fragment {

    private static final String TAG = "RetainFragment";
    public static LruCache mRetainedCache;

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

Fragment中保存了一个lruCache对象,这个fragment有一个findOrCreateRetainFragment()方法。这个方法等同构造函数,它先判断fragmentManager中有没有这个fragment,如果有就调出,如果没有就new一个。在onCreat中调用了setRetainInstance(true)来保存缓存。

 

Activity

package com.kale.bitmaptest;


import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.LruCache;

public class TestActivit extends Activity {
    private LruCache mMemoryCache;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...

        RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager());
        mMemoryCache = RetainFragment.mRetainedCache;
        if (mMemoryCache == null) {
            mMemoryCache = new LruCache(10);
            RetainFragment.mRetainedCache = mMemoryCache;
        }
        // ...
    }

}

类似的代码在bitmapfun中就可以看到:

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.bitmapfun.util;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;

/**
 * A simple non-UI Fragment that stores a single Object and is retained over configuration changes.
 * In this sample it will be used to retain the ImageCache object.
 */
public class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    private Object mObject;

    /**
     * Empty constructor as per the Fragment documentation
     */
    public RetainFragment() {}

    /**
     * Locate an existing instance of this Fragment or if not found, create and
     * add it using FragmentManager.
     *
     * @param fm The FragmentManager manager to use.
     * @return The existing instance of the Fragment or the new instance if just
     *         created.
     */
    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        // Check to see if we have retained the worker fragment.
        RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);

        // If not retained (or first time running), we need to create and add it.
        if (mRetainFragment == null) {
            mRetainFragment = new RetainFragment();
            fm.beginTransaction().add(mRetainFragment, TAG).commit();
        }

        return mRetainFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Make sure this Fragment is retained over a configuration change
        setRetainInstance(true);
    }

    /**
     * Store a single object in this Fragment.
     *
     * @param object The object to store
     */
    public void setObject(Object object) {
        mObject = object;
    }

    /**
     * Get the stored object.
     *
     * @return The stored object
     */
    public Object getObject() {
        return mObject;
    }

}
View Code

在Activity中建立一个retainFragment对象,首先看看这个对象中有没有缓存,如果是第一次启动的话肯定是没有的,所以就初始化缓存,同时让fragment去引用这个缓存对象(在fragment中备份)。如果这个activity是由于屏幕方向改变而再次产生的,那么就可以从fragment中获得之前的缓存对象,无须重新初始化缓存了,这样可以保证之前的缓存不被丢弃。

 

六、总结

这样我们就完成了对lruCache的使用,现在我们发现这个缓存类也没那么复杂,用法也十分简单,正是因为简单,所以我们可以很方便的对其进行扩展。当然了,这仅仅是做了内存缓存,熟悉缓存机制的朋友一定会知道磁盘缓存和内存缓存二者的关系,有关磁盘缓存的问题我将在以后的文章中进行讲述。

 

源码下载:http://download.csdn.net/detail/shark0017/8395797

 

参考自:

http://stormzhang.com/android/2013/11/20/android-display-bitmaps-efficiently/

http://blog.csdn.net/androidzhaoxiaogang/article/details/7910364

你可能感兴趣的:(详细解读LruCache类)