登录
原创

Android内存缓存方案LruCache的使用及原理分析

发布于 2020-10-15 阅读 390
  • 算法
  • Android
  • 数据结构
原创

0. 前言

在介绍LruCache 是什么之前,先来聊聊 Android 的缓存策略。其实缓存策略很简单,举个栗子,当用户第一次使用网络加载一张图片后,会将其存储到内存或者持久化到硬盘中,下次需要显示这张图片的时候,会优先从内存或者硬盘加载这张图片,既能节约网络流量消耗,也能提高获取图片的速度。

缓存的操作涉及到添加、获取和删除这些步骤。为什么需要删除缓存呢?因为每个设备或者程序都会有一定的容量限制,当容量满了话就需要删除。

那什么是 LruCache 呢?LruCache是 android.util 中的一个泛型类,通过该类可实现内存缓存。 LruCache是 LRU(Least Recently Used)算法 的一种运用,其核心思想是优先淘汰那些近期最少使用的缓存对象。

1. LruCache的使用

现在使用 okhttp 加载网上的一张图片:

新建一个 ImageLoader 类:

public class ImageLoader {

    private LruCache<String, Bitmap> lruCache;

    public ImageLoader() {
        int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public void addBitmap(String key, Bitmap bitmap) {
        if (getBitmap(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    public Bitmap getBitmap(String key) {
        return lruCache.get(key);
    }

}

①设置LruCache缓存的大小cacheSize,一般为当前进程可用内存的1/8。

②重写sizeOf方法,计算出要缓存的每张图片的大小。

当存放元素的总缓存大小大于 cacheSize 时,LruCache 就会删除最近最少使用的元素。

MainActivity:

public class MainActivity extends AppCompatActivity {

    private String Path = "https://zhongtai.juhe.cn/jiagou.c939ca09.jpg";

    private Button btn;

    private ImageView imageView;

    private ImageLoader imageLoader;

    private static final int SUCCESS = 1;
    private static final int FAIL = 2;

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SUCCESS:
                    byte[] Picture = (byte[]) msg.obj;
                    Bitmap bitmap = BitmapFactory.decodeByteArray(Picture, 0, Picture.length);
                    imageLoader.addBitmap("bitmap", bitmap);
                    imageView.setImageBitmap(bitmap);

                    break;
                case FAIL:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = findViewById(R.id.btn);
        imageView = findViewById(R.id.imageview);
        imageLoader = new ImageLoader();
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bitmap bitmap = getBitmapFromCache();
                if(bitmap != null) {
                    imageView.setImageBitmap(bitmap);
                } else {
                    getBitmapFromInternet();
                }
            }
        });

    }

    private Bitmap getBitmapFromCache() {
        Log.e("juhe", "<---------- getBitmapFromCache ---------->");
        return imageLoader.getBitmap("bitmap");
    }

    private void getBitmapFromInternet() {
        Log.e("juhe", "<---------- getBitmapFromInternet ---------->");
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(Path)
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                byte[] Picture_bt = response.body().bytes();
                Message message = handler.obtainMessage();
                message.obj = Picture_bt;
                message.what = SUCCESS;
                handler.sendMessage(message);
            }
            
            @Override
            public void onFailure(Call call, IOException e) {

            }
        });
    }

}

当点击Button显示一张图片时,会先查看缓存里是否有这张图片,如果存在就直接从缓存加载,不存在就通过 okhttp 从网络加载一张图片,然后添加进缓存中以便之后直接复用。

2. LruCache 原理

LruCache 底层是通过 LinkedHashMap(数组+双向链表)的数据结构来实现的。其维护了一个缓存对象列表,列表的排列方式是按照访问顺序实现的(也可以是插入顺序,通过accessOrder设置),即一直没访问的对象,将放在队头,即将被淘汰。而最近访问的对象将放在队尾,最后被淘汰。

LinkedHashMap 的构造方法:

public LinkedHashMap(int initialCapacity, loat loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

当 accessOrder 为 true 时,这个集合的元素顺序就会是访问顺序,也就是访问了之后就会将这个元素放到集合的最后面。

举个栗子:

LinkedHashMap < Integer, Integer > map = new LinkedHashMap < > (0, 0.75f, true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.get(1);
map.get(2);

for (Map.Entry < Integer, Integer > entry: map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
}

打印结果:

0:0    <--- 队头 
3:3
1:1
2:2    <--- 队尾    

以下分别来分析 LruCache 的 put 和 get 方法。

2.1 put方法分析

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) 方法,该方法由用户自己实现
        size += safeSizeOf(key, value);
        
        // 返回之前关联过这个 key 的值,如果没有关联过则返回 null
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    // 敲重点 !!!
    trimToSize(maxSize);
    
    return previous;
}

trimToSize()方法

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

put() 方法重点在于 trimToSize() 方法里面,这个方法的作用就是判断加入元素后是否超过最大缓存数,如果超过就清除掉最少使用的元素。

2.2 get方法分析

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

    V mapValue;
    synchronized(this) {
        // 获取 Value
        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;
    
    // 敲重点 !!!
    if (accessOrder) afterNodeAccess(e);
    return e.value;
}

static class Node < K, V > implements Map.Entry < K, V > {
    final int hash;
    final K key;
    V value;
    Node < K, V > next;

    Node(int hash, K key, V value, Node < K, V > next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final String toString() {
        return key + "=" + value;
    }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this) return true;
        if (o instanceof Map.Entry) {
            Map.Entry <? , ?> e = (Map.Entry <? , ?> ) o;
            if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true;
        }
        return false;
    }
}

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

关键方法 afterNodeAccess()

// 该方法的作用就是将刚访问过的元素放到集合的最后一位
// 涉及到双向链表节点的删除与插入操作
// 边界:需考虑操作节点的前后的节点是否为null以及是否为头尾节点
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;
    }
}

3. 总结

1、LruCache 中维护了一个集合 LinkedHashMap,该 LinkedHashMap 是以访问顺序或者插入顺序排序的。
2、当调用put()方法时,就会往集合中添加元素,并调用 trimToSize() 判断缓存是否已满。 如果满了就用 LinkedHashMap 的迭代器删除队尾元素,即近期最少访问的元素,直到集合内元素占用的内存小于设置的阈值 cacheSize 为止。
3、当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。

评论区

励志做一条安静的咸鱼,从此走上人生巅峰。

0

0

0

举报