阅读 1198

LruCache 的使用及原理

概述

LRU (Least Recently Used) 的意思就是近期最少使用算法,它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。

在我们日常开发中,UI 界面进行网络图片加载是很正常的一件事情,但是当界面上的图片过于多的时候,不可能每次都从网络上进行图片的获取,一方面效率会很低,另一方面,也会非常耗费用户的流量。

Android 为我们提供了 LruCache 类,使用它我们可以进行图片的内存缓存,今天我们就一起学习一下吧。

使用 LruCache 进行图片加载

1. 编写 MyImageLoader 类,实现图片缓存功能。

package com.keven.jianshu.part6;

import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * Created by keven on 2019/5/28.
 */
public class MyImageLoader {
    private LruCache<String, Bitmap> mLruCache;

    /**
     * 构造函数
     */
    public MyImageLoader() {
        //设置最大缓存空间为运行时内存的 1/8
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //计算一个元素的缓存大小
                return value.getByteCount();
            }
        };

    }

    /**
     * 添加图片到 LruCache
     *
     * @param key
     * @param bitmap
     */
    public void addBitmap(String key, Bitmap bitmap) {
        if (getBitmap(key) == null) {
            mLruCache.put(key, bitmap);
        }
    }

    /**
     * 从缓存中获取图片
     *
     * @param key
     * @return
     */
    public Bitmap getBitmap(String key) {
        return mLruCache.get(key);
    }

    /**
     * 从缓存中删除指定的 Bitmap
     *
     * @param key
     */
    public void removeBitmapFromMemory(String key) {
        mLruCache.remove(key);
    }
}

复制代码

至于代码的具体含义,注释已经进行了诠释。

2. 在 Activity 中进行图片的缓存及加载

public class Part6ImageActivity extends AppCompatActivity {
    private static String imgUrl = "https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1559013549&di=41b6aa8d219f05d44708d296dbf96b5f&src=http://img5.duitang.com/uploads/item/201601/03/20160103233143_4KLWs.jpeg";
    private static final int SUCCESS = 0x0001;
    private static final int FAIL = 0x0002;
    private MyHandler mHandler;
    private static ImageView mImageView;
    private static MyImageLoader mImageLoader;
    private Button mBt_load;

    static class MyHandler extends Handler {
        //创建一个类继承 Handler
        WeakReference<AppCompatActivity> mWeakReference;

        public MyHandler(AppCompatActivity activity) {
            mWeakReference = new WeakReference<>(activity);
        }

        //在 handleMessage 方法中对网络下载的图片进行处理
        @Override
        public void handleMessage(Message msg) {
            final AppCompatActivity appCompatActivity = mWeakReference.get();
            if (appCompatActivity != null) {
                switch (msg.what) {
                    case SUCCESS://成功
                        byte[] Picture = (byte[]) msg.obj;
                        Bitmap bitmap = BitmapFactory.decodeByteArray(Picture, 0, Picture.length);
                        mImageLoader.addBitmap(ImageUtils.hashKeyForCache(imgUrl), bitmap);
                        mImageView.setImageBitmap(bitmap);

                        break;
                    case FAIL://失败

                        break;
                }

            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_part6_image);
        
        //创建 Handler
        mHandler = new MyHandler(this);
        mImageView = findViewById(R.id.iv_lrucache);
        //创建自定义的图片加载类
        mImageLoader = new MyImageLoader();
        mBt_load = findViewById(R.id.bt_load);
        //点击按钮进行图片加载
        mBt_load.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bitmap bitmap = getBitmapFromCache();
                if (bitmap != null) {//有缓存
                    LogUtils.e("从缓存中取出图片");
                    mImageView.setImageBitmap(bitmap);
                } else {//没有缓存
                    LogUtils.e("从网络下载图片");
                    downLoadBitmap();
                }
            }
        });

    }

    /**
     * 从缓存中获取图片
     *
     * @return
     */
    private Bitmap getBitmapFromCache() {
        return mImageLoader.getBitmap(ImageUtils.hashKeyForCache(imgUrl));
    }

    /**
     * 从网络上下载图片
     * 使用 OKHttp 进行图片的下载
     */
    private void downLoadBitmap() {
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(imgUrl)
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                byte[] Picture_bt = response.body().bytes();
                Message message = mHandler.obtainMessage();
                message.obj = Picture_bt;
                message.what = SUCCESS;
                mHandler.sendMessage(message);

            }
        });

    }
}

复制代码

其中的布局文件就很简单,一个按钮 + 一个 Imageview

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".part6.Part6ImageActivity">
    <Button
        android:id="@+id/bt_load"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:text="加载图片"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <ImageView
        android:id="@+id/iv_lrucache"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
复制代码

代码中还用到了一个工具类,主要用于将图片的 url 转换为 md5 编码后的字符串,用作缓存文件的 key 进行存储,保证其独一性

public class ImageUtils {
    public static String hashKeyForCache(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

}
复制代码

3. 实际使用

我们进行加载图片按钮的多次点击,通过 log 进行查看是否正常缓存

com.keven.jianshu E/TAG: 从网络下载图片
com.keven.jianshu E/TAG: 从缓存中取出图片
com.keven.jianshu E/TAG: 从缓存中取出图片
复制代码

可以看出,除了第一次图片是从网络上进行下载,之后都是从缓存中进行获取。

LruCache 原理解析

LruCache 的文档描述

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.
一个包含有限数量强引用的缓存,每次访问一个值,它都会被移动到队列的头部,将一个新的值添加到已经满了的缓存队列时,该队列末尾的值将会被逐出,并且可能会被垃圾回收机制进行回收。

LruCache 构造函数

创建了一个 LinkedHashMap,三个参数分别为 初始容量、加载因子和访问顺序,当 accessOrder 为 true 时,这个集合的元素顺序就会是访问顺序,也就是访问了之后就会将这个元素放到集合的最后面。

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
复制代码

有些人可能会有疑问,初始容量传 0 的话,那岂不是没办法进行存储了,那么创建这个 LinkedHashMap 还有什么意义呢?
其实要解答这个问题并不难,看下源码你就会发现

  1. 其实第一个参数是你要设置的初始大小;而程序内部实际的初始大小是1;
  2. 如果你设置的初始大小(initialCapacity)小于1, 那么map大小就是默认的1;
  3. 否则会不断左移(乘2)直到capacity大于你设置的initialCapacity;
public LinkedHashMap(int initialCapacity,  
         float loadFactor,  
                        boolean accessOrder) {  
       super(initialCapacity, loadFactor);//这里调用父类HashMap的构造方法;  
       this.accessOrder = accessOrder;  
   }  
public HashMap(int initialCapacity, float loadFactor) {  
       if (initialCapacity < 0)  
           throw new IllegalArgumentException("Illegal initial capacity: " +  
                                              initialCapacity);  
       if (initialCapacity > MAXIMUM_CAPACITY)  
           initialCapacity = MAXIMUM_CAPACITY;  
       if (loadFactor <= 0 || Float.isNaN(loadFactor))  
           throw new IllegalArgumentException("Illegal load factor: " +  
                                              loadFactor);  
  
       // Find a power of 2 >= initialCapacity  
       int capacity = 1;  // 默认是1  
       while (capacity < initialCapacity)//不断翻倍直到大于人为设置的大小  
           capacity <<= 1;  
  
       this.loadFactor = loadFactor;  
       threshold = (int)(capacity * loadFactor);//的确如你所言,后面如果需要增大长度,按照capacity*loadFactor取整后增长;  
       table = new Entry[capacity];  
       init();  
   }  
复制代码

LruCache 的 put 方法

其中的 trimToSize() 方法用于判断加入元素后是否超过最大缓存数,如果超过就清除掉最少使用的元素。

public final V put(K key, V value) {
    // 如果 key 或者 value 为 null,则抛出异常
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized(this) {
        // 加入元素的数量,在 putCount() 用到
        putCount++;

        // 回调用 sizeOf(K key, V value) 方法,这个方法用户自己实现,默认返回 1
        size += safeSizeOf(key, value);

        // 返回之前关联过这个 key 的值,如果没有关联过则返回 null
        previous = map.put(key, value);

        if (previous != null) {
            // safeSizeOf() 默认返回 1
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        // 该方法默认方法体为空
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);

    return previous;
}

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!");
            }

            // 直到缓存大小 size 小于或等于最大缓存大小 maxSize,则停止循环
            if (size <= maxSize) {
                break;
            }

            // 取出 map 中第一个元素
            Map.Entry < K, V > 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);
    }
}

public Map.Entry<K, V> eldest() {
    return head;
}

复制代码

LruCache 的 get 方法

LruCahche 的 get() 方法源码

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        //从 LinkedHashMap 中获取值
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
复制代码

LinkedHashMap 的 get() 方法源码

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    //如果访问顺序设置为 true,则执行 afterNodeAccess(e) 方法
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
复制代码

afterNodeAccess() 方法源码

// 这个方法的作用就是将刚访问过的元素放到集合的最后一位
void afterNodeAccess(Node < K, V > e) { 
    LinkedHashMap.Entry < K, V > last;
    if (accessOrder && (last = tail) != e) {
        // 将 e 转换成 LinkedHashMap.Entry
        // b 就是这个节点之前的节点
        // a 就是这个节点之后的节点
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;

        // 将这个节点之后的节点置为 null
        p.after = null;

        // b 为 null,则代表这个节点是第一个节点,将它后面的节点置为第一个节点
        if (b == null) head = a;
        // 如果不是,则将 a 上前移动一位
        else b.after = a;
        // 如果 a 不为 null,则将 a 节点的元素变为 b
        if (a != null) a.before = b;
        else last = b;
        if (last == null) head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
复制代码

LruCache 的 remove 方法

从缓存中删除内容,并更新缓存大小

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;
}
复制代码

总结

  • 当缓存满了之后,LruCache 是最近最少使用的元素会被移除
  • 内部使用了 LinkedHashMap 进行存储
  • 总缓存大小一般为可用内存的 1/8
  • 当使用 get() 访问元素后,会将该元素移动到 LinkedHashMap 的尾部