LruCache 的使用及原理

概述

LRU (Least Recently Used) 的意思就是近期最少使用算法,它的核心思想就是會優先淘汰那些近期最少使用的緩存對象。android

在咱們平常開發中,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);
    }
}

複製代碼

至於代碼的具體含義,註釋已經進行了詮釋。bash

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 進行存儲,保證其獨一性app

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 進行查看是否正常緩存ide

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 的尾部
相關文章
相關標籤/搜索