Android 缓存策略

468 阅读4分钟

1. 缓存算法

  最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用.
  基于这个思想,存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间,附加指标是使用的次数。在计算机中大量使用了这个机制,它的合理性在于优先筛选热点数据,所谓热点数据,就是最近最多使用的数据!

2.缓存数据结构 LinkedHashMap

    /**
     * @param  initialCapacity 初始化大小,默认值为1
     * @param  loadFactor      扩容比,最小扩容数量为1
     * @param  accessOrder     是否使用缓存淘汰机制排序
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

3.LinkedHashMap使用demo

    val accessOrder = true
    val linkMap: LinkedHashMap<String,Int> = LinkedHashMap(
            0, 0.75f, accessOrder)
    linkMap["a"] = 1
    linkMap["b"] = 2
    linkMap["c"] = 3
    linkMap["f"] = 4
    linkMap["e"] = 5
    linkMap["b"]
    linkMap["x"] = 10
    linkMap["c"]
    
    linkMap.forEach { (t, u) ->
        Log.e("MainActivity","key:$t----value:$u")
    }
    /**
     *  accessOrder = false时
     *  MainActivity: key:a----value:1
     *  MainActivity: key:b----value:2
     *  MainActivity: key:c----value:3
     *  MainActivity: key:f----value:4
     *  MainActivity: key:e----value:5
     *  MainActivity: key:x----value:10
     * 
     * ------------------------------------------
     *  accessOrder = true时
     * MainActivity: key:a----value:1 
     * MainActivity: key:f----value:4 
     * MainActivity: key:e----value:5 
     * MainActivity: key:b----value:2 
     * MainActivity: key:x----value:10 
     * MainActivity: key:c----value:3 
     */
    

4.内存缓存 androidx.collection.LruCache

(备注:android.util.LruCache有问题)

图片缓存demo

private LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(MAX_LEN) {

        @Override
        protected int sizeOf(@NotNull String key, Bitmap value) {
            return value.getByteCount();//每条数据的大小
        }

        /**
         * get 为空时的回调
         * @param key key
         * @return Bitmap 返回不为空会自动添加
         */
        @Nullable
        @Override
        protected Bitmap create(@NonNull String key) {
            return super.create(key);
        }


        /**
         * 移除或key对应value变化的回调
         * @param evicted 是否腾出了空间(put remove 除外)
         * @param key key
         * @param oldValue  oldValue
         * @param newValue newValue
         */
        @Override
        protected void entryRemoved(boolean evicted, @NonNull String key, @NonNull Bitmap oldValue, @Nullable Bitmap newValue) {
            super.entryRemoved(evicted, key, oldValue, newValue);
        }
    };

常用函数 (key建议使用md5加密)

/**
     * 添加一条缓存,一个key对应一个value
     *
     * @return 是否添加成功
     */
    public Bitmap addString(String key, Bitmap value) {
        return mLruCache.put(EncryptUtils.encryptMD5ToString(key), value);
    }


    public Bitmap get(String key) {
        return mLruCache.get(EncryptUtils.encryptMD5ToString(key));
    }

    public Bitmap remove(String key) {
        return mLruCache.remove(EncryptUtils.encryptMD5ToString(key));
    }

    /**
     * 清除缓存
     */
    public void clear() {
        mLruCache.evictAll();
    }

5.源码分析accessOrder原理

LinkedHashMap关键函数 afterNodeAccess将最近使用的Node 移动到尾部 tail

 //将最近使用的Node 移动到尾部 tail
 void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            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 trimToSize 当size超出时删除链表第一个

 /**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    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!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
				//删除链表第一个
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

磁盘缓存DiskLruCache

使用:implementation 'com.jakewharton:disklrucache:2.0.2'

缓存案例

创建DiskLruCache 参数appVersion改变会清除缓存

    * Opens the cache in {@code directory}, creating a cache if none exists
   * there.
   *
   * @param directory a writable directory
   * @param valueCount the number of values per cache entry. Must be positive.
   * @param maxSize the maximum number of bytes this cache should use to store
   * @throws IOException if reading or writing the cache directory fails
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

常用函数 (key建议使用md5加密)

/**
     * md5 加密key
     *
     * @param key 键
     * @return Editor
     * @throws IOException 异常
     */
    private DiskLruCache.Editor getEdit(String key) throws IOException {
        return mDiskLruCache.edit(EncryptUtils.encryptMD2ToString(key));
    }

    /**
     * md5 加密key
     *
     * @param key 键
     * @return Snapshot
     * @throws IOException 异常
     */
    private DiskLruCache.Snapshot getSnapshot(String key) throws IOException {
        return mDiskLruCache.get(EncryptUtils.encryptMD2ToString(key));
    }

添加数据

    /**
     * 添加一条缓存,一个key对应一个value
     *
     * @return 是否添加成功
     */
    public boolean addString(String key, String value) {
        DiskLruCache.Editor editor = null;
        OutputStream outputStream = null;
        try {
            editor = getEdit(key);
            outputStream = editor.newOutputStream(0);
            outputStream.write(value.getBytes());
            editor.commit();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            try {
                if (editor != null) {
                    editor.abort();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }


    public boolean addBitMap(String key, Bitmap value) {
        DiskLruCache.Editor editor = null;
        OutputStream outputStream = null;
        try {
            editor = getEdit(key);
            outputStream = editor.newOutputStream(0);
            value.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
            editor.commit();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            try {
                if (editor != null) {
                    editor.abort();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

获取数据

public String getAsString(String key) {
        DiskLruCache.Snapshot snapshot;
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            snapshot = getSnapshot(key);
            inputStream = snapshot.getInputStream(0);
            outputStream = new ByteArrayOutputStream();
            byte[] arr = new byte[1024];
            int len;
            while ((len = inputStream.read(arr)) != -1) {
                outputStream.write(len);
            }
            outputStream.flush();
            return new String(outputStream.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }


    public Bitmap getBitMap(String key) {
        DiskLruCache.Snapshot snapshot;
        InputStream inputStream = null;
        try {
            snapshot = getSnapshot(key);
            inputStream = snapshot.getInputStream(0);
            return BitmapFactory.decodeStream(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

扩展:一个key 可对应多个value

有任何疑问或建议可随时联系邮箱: zhanpples@qq.com