An overview of the

LRU (Least Recently Used) means the Least Recently Used algorithm, and its core idea is to prioritize the Least Recently Used cache objects.

In our daily development, it is very normal for THE UI interface to load network pictures, but when there are too many pictures on the interface, it is impossible to obtain pictures from the network every time. On the one hand, the efficiency will be very low, on the other hand, it will also consume user traffic.

Android provides the LruCache class, which can be used to cache images in memory. Let’s learn about it today.

Use LruCache for image loading

1. Write MyImageLoader class to achieve image caching function.

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; /** * constructor */ publicMyImageLoader() {// Set maxMemory to 1/8 of Runtime memory int maxMemory = (int) Runtime.geTrunTime ().maxMemory(); int cacheSize = maxMemory / 8; mLruCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) {// Calculate the cache size of an elementreturnvalue.getByteCount(); }}; } public void addBitmap(String key, bitmap bitmap) {if(getBitmap(key) == null) { mLruCache.put(key, bitmap); }} /** * get the image from the cache ** @param key * @return
     */
    public Bitmap getBitmap(String key) {
        returnmLruCache.get(key); } /** * Remove the specified Bitmap from the cache ** @param key */ public void removeBitmapFromMemory(String key) {mLruCache. Remove (key); }}Copy the code

As for what the code actually means, the comments explain it.

2. Cache and load pictures in the 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=41b6aa8d21 9f05d44708d296dbf96b5f&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 {// Create a class that inherits Handler WeakReference<AppCompatActivity> mWeakReference; public MyHandler(AppCompatActivity activity) { mWeakReference = new WeakReference<>(activity); Override public void handleMessage(Message MSG) {final appactivity appCompatActivity = mWeakReference.get();if(appCompatActivity ! = null) { switch (msg.what) {case[] Picture = (byte[]) msg.obj; Bitmap bitmap = BitmapFactory.decodeByteArray(Picture, 0, Picture.length); mImageLoader.addBitmap(ImageUtils.hashKeyForCache(imgUrl), bitmap); mImageView.setImageBitmap(bitmap);break;
                    caseFAIL: / / failurebreak;
                }

            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_part6_image); MHandler = new MyHandler(this); mImageView = findViewById(R.id.iv_lrucache); // Create a custom image loading class mImageLoader = new MyImageLoader(); mBt_load = findViewById(R.id.bt_load); // Click the button to load the image mBt_load. SetOnClickListener (new View)OnClickListener() {
            @Override
            public void onClick(View v) {
                Bitmap bitmap = getBitmapFromCache();
                if(bitmap ! = null) {// There is cache logutils.e ("Fetch image from cache");
                    mImageView.setImageBitmap(bitmap);
                } else{// No cache logutils.e ("Download pictures from the Internet"); downLoadBitmap(); }}}); } /** * get the image from the cache ** @return
     */
    private Bitmap getBitmapFromCache() {
        returnmImageLoader.getBitmap(ImageUtils.hashKeyForCache(imgUrl)); } /** * use OKHttp to download images */ private voiddownLoadBitmap() {
        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); }}); }}Copy the code

The layout file is simple, a button + an 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="Load image"
        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>
Copy the code

A tool class is also used in the code, which is mainly used to convert the URL of the picture into a string encoded by MD5, which is used as the key of the cache file for storage to ensure its uniqueness

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);
        }
        returnsb.toString(); }}Copy the code

3. Actual use

We click the load image button several times and check whether the cache is normal through log

Com.keven. jianshu E/TAG: download images from the network com.keven.jianshu E/TAG: fetch images from the cache com.keven.jianshu E/TAG: Fetch images from the cacheCopy the code

As you can see, with the exception of the first time the image was downloaded from the network, it was retrieved from the cache.

LruCache principle analysis

Document description of 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. A cache containing a finite number of strong references is moved to the head of the queue each time a value is accessed, and when a new value is added to an already full cache queue, the value at the end of the queue is ejected and may be reclaimed by the garbage collection mechanism.

LruCache constructor

Create a LinkedHashMap with the initial capacity, load factor, and accessOrder. When accessOrder is true, the order of the elements in the collection will be accessOrder, that is, the elements will be accessed at the end of the collection.

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);
}
Copy the code

Some of you may wonder, what is the point of creating a LinkedHashMap if the initial capacity is 0 and there is no way to store it? In fact, to answer this problem is not difficult, look at the source code you will find

  1. The first parameter is the initial size you want to set; The actual initial size inside the program is 1;
  2. If you set the initialCapacity (initialCapacity) to less than 1, then the map size is the default 1;
  3. Otherwise it will keep moving to the left (multiplied by 2) until capacity is greater than the initialCapacity you set;
public LinkedHashMap(int initialCapacity,  
         floatloadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); // Call the constructor of the parent class 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; // The default is 1while(Capacity < initialCapacity)// Keep doubling until greater than the artificially set size capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); // If you need to increase the length, the capacity*loadFactor will be rounded to increase the length. table = new Entry[capacity]; init(); }Copy the code

LruCache’s put method

The trimToSize() method is used to determine whether the maximum number of cached elements is exceeded after the addition, and if so, the least used elements are removed.

Public final V PUT (K key, V value) {// If key or value is null, an exception is thrownif (key == null || value == null) {
        throw new NullPointerException("key == null || value == null"); } V previous; Synchronized (this) {// Add the number of elements to putCount() using putCount++; Size += safeSizeOf(key, value); sizeOf(K key, V value); // Return null previous = map.put(key, value); // Return null previous = map.put(key, value);if(previous ! = null) {// safeSizeOf() returns 1 size -= safeSizeOf(key, previous); }}if(previous ! = null) {// This method defaults to 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!"); } // Stop the loop until the cache size is less than or equal to the maximum cache size, maxSizeif (size <= maxSize) {
                break; } // Get the map element map.entry < K, V > toEvict = map.eldest();if (toEvict == null) {
                break; } key = toEvict.getKey(); value = toEvict.getValue(); // Remove the element map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null);
    }
}

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

Copy the code

Get method of LruCache

LruCahche get() method source

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null"); } V mapValue; Synchronized (this) {// obtain the value from LinkedHashMap mapValue = map.get(key);if(mapValue ! = null) { hitCount++;return mapValue;
        }
        missCount++;
    }
Copy the code

LinkedHashMap get() method source

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        returnnull; // If the access order is set totrueAfterNodeAccess (e) is executedif (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
Copy the code

AfterNodeAccess () method source

Void afterNodeAccess(Node < K, V > e) {linkedhashmap.entry < K, V > last;if(accessOrder && (last = tail) ! = e) {// convert e to linkedhashmap. Entry // b is the node before this node // A is the node after this node linkedhashmap. Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after; // set the node after this node to null. // if b is null, this node is the first node, and the node after it is the first nodeif(b == null) head = a; // If not, move a forward one bitelseb.after = a; // If a is not null, change the element of node A to bif(a ! = null) a.before = b;else last = b;
        if (last == null) head = p;
        else{ p.before = last; last.after = p; } tail = p; ++modCount; }}Copy the code

Remove method of LruCache

Remove content from the cache and update the cache size

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;
}
Copy the code

conclusion

  • When the cache is full, LruCache is the least recently used element and is removed
  • Internal storage is using LinkedHashMap
  • The total cache size is typically 1/8 of the available memory
  • When an element is accessed using get(), it is moved to the end of the LinkedHashMap