Android源码解析之LruCache(内存缓存)

前言

在实际开发中,我们经常要加载很多大图,大多数情况下都是从服务器中下载下来,然后显示到图片的控件中。这里就会存在一个问题,如果每次加载图片都要从网络上下载,这样就会导致效率非常慢,影响用户的体验。所以我们会想到可以把图片缓存到内存中,第二次之后加载图片就从内存中加载,这样效率就会很快。但是我们要知道,如果我们把大量的图片都存到内存中,当内存不足的时候,就会导致OOM。所以这样一来LruCache就是为了解决这个问题而出现的。

一、LruCache的简单使用

LruCache用中文翻译就是最近最少使用算法,它会将图片缓存到内存中。具体原理我们先不说,我们先介绍LruCache的简单使用方法,然后再从源码的角度分析LruCache的实现原理。

/**
 * 自定义图片加载类
 */
public class MyImageLoader {
    private static final String TAG ="MyImageLoader" ;
    /**
     * LruCache 内存缓存类
     */
    private LruCache lruCache;
    private Context context;

    public MyImageLoader(Context context) {
        this.context = context;
        //获取最大可用内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //使用最大内存的1/8作为缓存大小
        int cacheSize = maxMemory / 8;
        lruCache = new LruCache(cacheSize) {
            @Override
            protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                //每次存入缓存的时候调用该方法
                //存入的图片的大小
                return super.sizeOf(key, value);
            }
        };
    }
    /**
     * 显示图片
     *
     * @param imageView 显示控件
     * @param url       url 作为key
     */
    public void showImage(ImageView imageView, String url) {
        //从缓存中获取bitmap
        Bitmap bitmap = lruCache.get(url);
        //判断缓存中的bitmap是否为空
        if (bitmap == null) {
            //如果为空,则去下载该图片
            downLoad(imageView, url);
            Log.e(TAG, "showImage: 从网上下载的图片" );

        } else {
            //如果不为空,则直接展示缓存中的bitmap
            imageView.setImageBitmap(bitmap);
            Log.e(TAG, "showImage: 从缓存中加载图片" );
        }
    }
    /**
     * 下载图片
     *
     * @param imageView
     * @param url
     */
    private void downLoad(final ImageView imageView, final String url) {
        AsyncTask asyncTask = new AsyncTask() {
            @Override
            protected Bitmap doInBackground(String... integers) {
                //在这里执行异步任务,下载图片。在这里就模拟从网络下载图片
                //从项目中加载一张图片,模拟从网络下载
                Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
                return bitmap;
            }

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                //显示图片
                imageView.setImageBitmap(bitmap);
                //并把图片放入缓存中
                lruCache.put(url, bitmap);
            }

            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);
            }
        };
        asyncTask.execute(url);
    }

}

我们自定义一个加载图片的类MyImageLoader ,这个类很简单

  • 首先在构造方法中初始化缓存的大小,一般为最大可用内存的8分之1
  • 然后创建LruCache对象,两个参数分别为key 和 value,也就是说缓存中以键值对的显示存在。当我们取缓存中的值的时候,可用通过键来取。
  • showImage方法用来加载图片,传入图片控件ImageView 以及要下载图片的url参数,并用url做完缓存的键。首先从缓存中读取bitmap,然后判断缓存中的bitmap是否为空,如果不为空直接显示图片。如果为空调用downLoad方法去下载图片,在downLoad方法中,我们就不去网络上下载了,直接从项目中加载一张图片用来模拟下载。下载成功后我们加载图片,并把图片通过url为键存入到缓存中。
//并把图片放入缓存中
  lruCache.put(url, bitmap);

接下来我们看下具体的使用:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private ImageView ivEtxt;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ivEtxt = (ImageView) findViewById(R.id.iv_etxt);
        final MyImageLoader imageLoader = new MyImageLoader(this);
        ivEtxt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                imageLoader.showImage(ivEtxt, "http://baidu.com");
            }
        });
    }


}

我们点击ImageView事件,加载图片,点击两次得到的打印结果如下:

08-23 14:24:26.377 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从网上下载的图片
08-23 14:24:27.144 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从缓存中加载图片

我们看到第一次是从网下下载的图片,第二次是从缓存中加载的图片,并成功的显示到了ImageViw中。


Android源码解析之LruCache(内存缓存)_第1张图片
image.png

以上我们就介绍了LruCache的简单使用,具体详细的用法就不具体介绍了。下面将从源码的角度分析LruCache的原理。

二、从源码的角度分析LruCache的实现原理

首先我们看下LruCache的构造方法。

 /**
     * @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);
    }

在LruCache的构造方法中传入了maxSize参数,并把maxSize设置到成员变量,也就是我们设定的缓存的大小。接着创建了一个LinkedHashMap对象。在创建LinkedHashMap对象的时候就是最重要的核心内容。 new LinkedHashMap(0, 0.75f, true);分别传入三个参数,第一个参数0表示初始化长度为0、第二个参数0.75表示已0.75作为因子,当容量达到总容量的百分之75的时候,会把内存增加一半。第三个参数最为重要,true表示以访问顺序排序,就是当我们对LinkedHashMap的某个元素进行put或者get操作的时候,会把当前元素放到最后,重新排序。所以每次我们get或者put的时候,其实就是把元素放到了最后,所以最少访问的数据自然跑到了最前面。
接下来我们看put方法,把数据放入缓存中。

 /**
     * 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) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }

在put方法中首先对size自增,就是在当前缓存中存在的元素的大小中再增加当前元素的大小,然后再返回当前缓存中元素的大小。

 size += safeSizeOf(key, value);

然后把数据以键值对的形式存到map中

previous = map.put(key, value);

在这里要注意的是,如果mao中已经存在了该元素,则返回previous ,不会重复插入。所以当previous 不为空的时候,size 需要自减。
接着如果previous 不为空,调用了entryRemoved方法,该方法是一个空方法,我们如果需要了解具体的数据信息,可以重写该方法。

  if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
 protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

最后调用trimToSize方法

     trimToSize(maxSize);
 /**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    public 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 || map.isEmpty()) {
                    break;
                }

                Map.Entry toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

在这个方法中其实是一个while循环的操作,我们看下跳出循环的条件。

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

也就是说,如果缓存中元素的大小还没有超过我们设定的缓存大小,或者map链表中还没有值,就跳出循环,不做任何操作。但是如果说,缓存中的元素的大小已经达到了我们设定的缓存的大小,那么就需要把链表中的某个元素给去掉,具体去掉哪一个呢,我们看下面的代码。

  Map.Entry toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;

我们看到移除的是map中的第一个元素,为什么移除第一个元素呢,我们在上面分析的时候知道,在创建map的时候调用了 new LinkedHashMap(0, 0.75f, true);方法,传入了true这个参数,每次被访问的元素都会放在最后然后重新进行排序,那么最少访问次数的元素自然而然就在链表的头部了。所以移除掉第一个元素,也就是达到了最近最少使用算法这个效果。也就是经常不访问的元素就被移除掉了。
下面我们在看下get方法,从缓存中读取元素。

  /**
     * 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.
     */
    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.
         */

        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
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

首先从map中根据key获取元素,如果元素不为空,说明缓存中有值,返回当前元素。

  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.
         */

        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
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }

如果为空的话,我们调用 V createdValue = create(key);方法,create方法是一个空方法,我们可以手动实现,也就是说,如果为空的话,我们可以进行手动实现,给缓存中添加指定的元素。然后调用trimToSize方法,一样的原理,根据缓存中的大小和设定的大小比较看是否要移除第一个元素。
以上我们就对LruCache的源码做了简单的分析。接下来我们做一个简单的小结。
1、在LruCache中维护了一个LinkedHashMap链表,并且该链表以访问顺序进行排序,每次访问的元素会放在最后。
2、调用put方法给LinkedHashMap中添加元素,并且每插入一个元素,就要计算当前链表中的元素大小和设定的缓存的大小进行比较,如果达到了设定缓存的大小,把链表的第一个元素移除,也就是很少访问的元素移除掉。
3、调用get方法,获取LinkedHashMap中的元素。如果元素为空,我们还可以重写create方法,手动添加特定的元素到LinkedHashMap中。

你可能感兴趣的:(Android源码解析之LruCache(内存缓存))