LruCache 的使用及原理

作者: kevenZheng | 来源:发表于2019-05-29 12:05 被阅读7次
    image

    概述

    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 的尾部

    相关文章

      网友评论

        本文标题:LruCache 的使用及原理

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