LruCache 的使用及原理

image

概述

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

在我們?nèi)粘i_發(fā)中,UI 界面進行網(wǎng)絡(luò)圖片加載是很正常的一件事情,但是當界面上的圖片過于多的時候扑眉,不可能每次都從網(wǎng)絡(luò)上進行圖片的獲取,一方面效率會很低赖钞,另一方面腰素,也會非常耗費用戶的流量。

Android 為我們提供了 LruCache 類雪营,使用它我們可以進行圖片的內(nèi)存緩存弓千,今天我們就一起學習一下吧。

使用 LruCache 進行圖片加載

1. 編寫 MyImageLoader 類献起,實現(xiàn)圖片緩存功能洋访。

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;

    /**
     * 構(gòu)造函數(shù)
     */
    public MyImageLoader() {
        //設(shè)置最大緩存空間為運行時內(nèi)存的 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);
    }
}

至于代碼的具體含義,注釋已經(jīng)進行了詮釋谴餐。

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 {
        //創(chuàng)建一個類繼承 Handler
        WeakReference<AppCompatActivity> mWeakReference;

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

        //在 handleMessage 方法中對網(wǎng)絡(luò)下載的圖片進行處理
        @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);
        
        //創(chuàng)建 Handler
        mHandler = new MyHandler(this);
        mImageView = findViewById(R.id.iv_lrucache);
        //創(chuàng)建自定義的圖片加載類
        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("從網(wǎng)絡(luò)下載圖片");
                    downLoadBitmap();
                }
            }
        });

    }

    /**
     * 從緩存中獲取圖片
     *
     * @return
     */
    private Bitmap getBitmapFromCache() {
        return mImageLoader.getBitmap(ImageUtils.hashKeyForCache(imgUrl));
    }

    /**
     * 從網(wǎng)絡(luò)上下載圖片
     * 使用 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 轉(zhuǎn)換為 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: 從網(wǎng)絡(luò)下載圖片
com.keven.jianshu E/TAG: 從緩存中取出圖片
com.keven.jianshu E/TAG: 從緩存中取出圖片

可以看出厌殉,除了第一次圖片是從網(wǎng)絡(luò)上進行下載食绿,之后都是從緩存中進行獲取。

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.
一個包含有限數(shù)量強引用的緩存公罕,每次訪問一個值器紧,它都會被移動到隊列的頭部,將一個新的值添加到已經(jīng)滿了的緩存隊列時楼眷,該隊列末尾的值將會被逐出铲汪,并且可能會被垃圾回收機制進行回收。

LruCache 構(gòu)造函數(shù)

創(chuàng)建了一個 LinkedHashMap摩桶,三個參數(shù)分別為 初始容量桥状、加載因子和訪問順序,當 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 的話士飒,那豈不是沒辦法進行存儲了查邢,那么創(chuàng)建這個 LinkedHashMap 還有什么意義呢?
其實要解答這個問題并不難酵幕,看下源碼你就會發(fā)現(xiàn)

  1. 其實第一個參數(shù)是你要設(shè)置的初始大腥排骸;而程序內(nèi)部實際的初始大小是1芳撒;
  2. 如果你設(shè)置的初始大小(initialCapacity)小于1, 那么map大小就是默認的1邓深;
  3. 否則會不斷左移(乘2)直到capacity大于你設(shè)置的initialCapacity;
public LinkedHashMap(int initialCapacity,  
         float loadFactor,  
                        boolean accessOrder) {  
       super(initialCapacity, loadFactor);//這里調(diào)用父類HashMap的構(gòu)造方法笔刹;  
       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)//不斷翻倍直到大于人為設(shè)置的大小  
           capacity <<= 1;  
  
       this.loadFactor = loadFactor;  
       threshold = (int)(capacity * loadFactor);//的確如你所言芥备,后面如果需要增大長度,按照capacity*loadFactor取整后增長舌菜;  
       table = new Entry[capacity];  
       init();  
   }  

LruCache 的 put 方法

其中的 trimToSize() 方法用于判斷加入元素后是否超過最大緩存數(shù)萌壳,如果超過就清除掉最少使用的元素。

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) {
        // 加入元素的數(shù)量袱瓮,在 putCount() 用到
        putCount++;

        // 回調(diào)用 sizeOf(K key, V value) 方法,這個方法用戶自己實現(xiàn)爱咬,默認返回 1
        size += safeSizeOf(key, value);

        // 返回之前關(guān)聯(lián)過這個 key 的值尺借,如果沒有關(guān)聯(lián)過則返回 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,則停止循環(huán)
            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;
    //如果訪問順序設(shè)置為 true,則執(zhí)行 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 轉(zhuǎn)換成 LinkedHashMap.Entry
        // b 就是這個節(jié)點之前的節(jié)點
        // a 就是這個節(jié)點之后的節(jié)點
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;

        // 將這個節(jié)點之后的節(jié)點置為 null
        p.after = null;

        // b 為 null台颠,則代表這個節(jié)點是第一個節(jié)點褐望,將它后面的節(jié)點置為第一個節(jié)點
        if (b == null) head = a;
        // 如果不是,則將 a 上前移動一位
        else b.after = a;
        // 如果 a 不為 null串前,則將 a 節(jié)點的元素變?yōu)?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 方法

從緩存中刪除內(nèi)容,并更新緩存大小

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;
}

總結(jié)

  • 當緩存滿了之后实蔽,LruCache 是最近最少使用的元素會被移除
  • 內(nèi)部使用了 LinkedHashMap 進行存儲
  • 總緩存大小一般為可用內(nèi)存的 1/8
  • 當使用 get() 訪問元素后荡碾,會將該元素移動到 LinkedHashMap 的尾部
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市局装,隨后出現(xiàn)的幾起案子坛吁,更是在濱河造成了極大的恐慌,老刑警劉巖铐尚,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拨脉,死亡現(xiàn)場離奇詭異,居然都是意外死亡宣增,警方通過查閱死者的電腦和手機玫膀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爹脾,“玉大人帖旨,你說我怎么就攤上這事箕昭。” “怎么了解阅?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵落竹,是天一觀的道長。 經(jīng)常有香客問我货抄,道長述召,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任蟹地,我火速辦了婚禮积暖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锈津。我一直安慰自己呀酸,他們只是感情好,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布琼梆。 她就那樣靜靜地躺著性誉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茎杂。 梳的紋絲不亂的頭發(fā)上错览,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音煌往,去河邊找鬼倾哺。 笑死,一個胖子當著我的面吹牛刽脖,可吹牛的內(nèi)容都是我干的羞海。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼曲管,長吁一口氣:“原來是場噩夢啊……” “哼却邓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起院水,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤腊徙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后檬某,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撬腾,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年恢恼,在試婚紗的時候發(fā)現(xiàn)自己被綠了民傻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绷落。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡包蓝,死狀恐怖慌闭,靈堂內(nèi)的尸體忽然破棺而出箕别,到底是詐尸還是另有隱情,我是刑警寧澤彭雾,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布碟刺,位于F島的核電站,受9級特大地震影響薯酝,放射性物質(zhì)發(fā)生泄漏半沽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一吴菠、第九天 我趴在偏房一處隱蔽的房頂上張望者填。 院中可真熱鬧,春花似錦做葵、人聲如沸占哟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榨乎。三九已至,卻和暖如春瘫筐,著一層夾襖步出監(jiān)牢的瞬間蜜暑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工策肝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肛捍,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓之众,卻偏偏與公主長得像拙毫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子棺禾,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容