Android 内存缓存:手把手教你学会LrhCache算法

栏目: 数据库 · 发布时间: 6年前

内容简介:下面,将详细介绍

前言

Android
LrhCache

Android 内存缓存:手把手教你学会LrhCache算法

1. 简介

Android 内存缓存:手把手教你学会LrhCache算法

下面,将详细介绍 LrhCache 算法

2. LrhCache算法

Android 内存缓存:手把手教你学会LrhCache算法

3. 实现原理

  • LrhCache 算法的算法核心 =  LRU  算法 +  LinkedHashMap 数据结构
  • 下面,我将先介绍 LRU  算法 和  LinkedHashMap 数据结构,最后再介绍 LrhCache 算法

3.1 LRU 算法

  • 定义: Least Recently Used ,即 近期最少使用算法
  • 算法原理:当缓存满时,优先淘汰 近期最少使用的缓存对象

    采用 LRU 算法的缓存类型:内存缓存( LrhCache ) 、 硬盘缓存( DisLruCache

3.2 LinkedHashMap 介绍

  • 数据结构 = 数组 +单链表 + 双向链表
  • 其中,双向链表 实现了 存储顺序 = 访问顺序 / 插入顺序
    1. 使得 LinkedHashMap  中的 <key,value> 对 按照一定顺序进行排列
    2. 通过 构造函数 指定LinkedHashMap中双向链表的结构是访问顺序 or 插入顺序
/** 
  * LinkedHashMap 构造函数
  * 参数accessOrder = true时,存储顺序(遍历顺序) = 外部访问顺序;为false时,存储顺序(遍历顺序) = 插入顺序
  **/ 
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {

        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;

    }
  • 实例演示

accessOrder 参数设置为 true 时,存储顺序(遍历顺序) = 外部访问顺序

/** 
    * 实例演示
    **/ 
    // 1. accessOrder参数设置为true时
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);

    // 2. 插入数据
    map.put(0, 0);
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);
    map.put(4, 4);
    map.put(5, 5);
    map.put(6, 6);

    // 3. 访问数据
        map.get(1);
        map.get(2);

     // 遍历获取LinkedHashMap内的数据
     for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());

        }

  /** 
    * 测试结果
    **/ 
   0:0
   3:3
   4:4
   5:5
   6:6
   1:1
   2:2

// 即实现了 最近访问的对象 作为 最后输出
// 该逻辑 = LrhCache缓存算法思想
// 可见LruCache的实现是利用了LinkedHashMap数据结构的实现原理
// 请看LruCache的构造方法

 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);
        // 创建LinkedHashMap时传入true。即采用了存储顺序 = 外界访问顺序 = 最近访问的对象 作为 最后输出
    }

3.3 LrhCache 算法原理

Android 内存缓存:手把手教你学会LrhCache算法

  • 示意图

Android 内存缓存:手把手教你学会LrhCache算法

4. 使用流程

/** 
  * 使用流程(以加载图片为例)
  **/ 

    private LruCache<String, Bitmap> mMemoryCache; 

    // 1. 获得虚拟机能提供的最大内存
    // 注:超过该大小会抛出OutOfMemory的异常 
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 

    // 2. 设置LruCache缓存的大小 = 一般为当前进程可用容量的1/8
    // 注:单位 = Kb
    // 设置准则
    //  a. 还剩余多少内存给你的activity或应用使用
    //  b. 屏幕上需要一次性显示多少张图片和多少图片在等待显示
    //  c. 手机的大小和密度是多少(密度越高的设备需要越大的 缓存)
    //  d. 图片的尺寸(决定了所占用的内存大小)
    //  e. 图片的访问频率(频率高的在内存中一直保存)
    //  f. 保存图片的质量(不同像素的在不同情况下显示)
    final int cacheSize = maxMemory / 8; 

    // 3. 重写sizeOf方法:计算缓存对象的大小(此处的缓存对象 = 图片)
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { 

        @Override 
        protected int sizeOf(String key, Bitmap bitmap) { 

            return bitmap.getByteCount() / 1024; 
            // 此处返回的是缓存对象的缓存大小(单位 = Kb) ,而不是item的个数
            // 注:缓存的总容量和每个缓存对象的大小所用单位要一致
            // 此处除1024是为了让缓存对象的大小单位 = Kb

        } 
    }; 

    // 4. 将需缓存的图片 加入到缓存
    mMemoryCache.put(key, bitmap); 

    // 5. 当 ImageView 加载图片时,会先在LruCache中看有没有缓存该图片:若有,则直接获取
    mMemoryCache.get(key);

5. 实例讲解

  • 本实例以缓存图片为实例讲解
  • 具体代码

请看注释

MainActivity.java

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carsonTest:";
    private LruCache<String, Bitmap> mMemoryCache;
    private ImageView mImageView;
    private Button button;

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

        // 1. 获得虚拟机能提供的最大内存
        // 注:超过该大小会抛出OutOfMemory的异常
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 2. 设置LruCache缓存的大小 = 一般为当前进程可用容量的1/8
        // 注:单位 = Kb
        final int cacheSize = maxMemory / 8;

        // 3. 重写sizeOf方法:计算缓存对象的大小(此处的缓存对象 = 图片)
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
                // 此处返回的是缓存对象的缓存大小(单位 = Kb) ,而不是item的个数
                // 注:缓存的总容量和每个缓存对象的大小所用单位要一致
                // 此处除1024是为了让缓存对象的大小单位 = Kb

            }
        };

        // 4. 点击按钮,则加载图片
        mImageView = (ImageView)findViewById(R.id.image);
        button = (Button)findViewById(R.id.btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 加载图片 ->>分析1
                loadBitmap("test",mImageView);
            }

        });

    }

    /**
     * 分析1:加载图片
     * 加载前,先从内存缓存中读取;若无,则再从数据源中读取
     **/
    public void loadBitmap(String key, ImageView imageView) {

        // 读取图片前,先从内存缓存中读取:即看内存缓存中是否缓存了该图片
        // 1. 若有缓存,则直接从内存中加载
        Bitmap bitmap = mMemoryCache.get(key);

        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
            Log.d(TAG, "从缓存中加载图片 ");

            // 2. 若无缓存,则从数据源加载(此处选择在本地加载) & 添加到缓存
        } else {
            Log.d(TAG, "从数据源(本地)中加载: ");

            // 2.1 从数据源加载
            mImageView.setImageResource(R.drawable.test1);

            // 2.1 添加到缓存
            // 注:在添加到缓存前,需先将资源文件构造成1个BitMap对象(含设置大小)
            Resources res = getResources();
            Bitmap bm = BitmapFactory.decodeResource(res, R.drawable.test1);

            // 获得图片的宽高
            int width = bm.getWidth();
            int height = bm.getHeight();

            // 设置想要的大小
            int newWidth = 80;
            int newHeight = 80;

            // 计算缩放比例
            float scaleWidth = ((float) newWidth) / width;
            float scaleHeight = ((float) newHeight) / height;
            // 取得想要缩放的matrix参数
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleHeight);
            // 构造成1个新的BitMap对象
            Bitmap bitmap_s = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);

            // 添加到缓存
            if (mMemoryCache.get(key) == null) {
                mMemoryCache.put(key, bitmap_s);
                Log.d(TAG, "添加到缓存: " + (mMemoryCache.get(key)));
            }

        }
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <ImageView
       android:id="@+id/image"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:layout_gravity="center"

        />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="点击加载"
        android:layout_gravity="center"
        />

</LinearLayout>
  • 测试结果

    第1次点击加载图片时,由于无缓存则从本地加载

    第2次(以后)点击加载图片时,由于有缓存,所以直接从缓存中读取

    Android 内存缓存:手把手教你学会LrhCache算法

6. 源码分析

此处主要分析 写入缓存 & 获取缓存 ,即 put() 、  get()

6.1 添加缓存:put()

  • 源码分析
/** 
  * 使用函数(以加载图片为例)
  **/ 
  mMemoryCache.put(key,bitmap);

/** 
  * 源码分析
  **/ 
 public final V put(K key, V value) {

        // 1. 判断 key 与 value是否为空
        //  若二者之一味空,否则抛出异常
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;

        synchronized (this) {

            // 2. 插入的缓存对象值加1
            putCount++; 

            // 3. 增加已有缓存的大小
            size += safeSizeOf(key, value); 

            // 4. 向map中加入缓存对象
            previous = map.put(key, value);

            // 5. 若已有缓存对象,则缓存大小恢复到之前
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        // 6. 资源回收(移除旧缓存时会被调用)
        // entryRemoved()是个空方法,可自行实现
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        // 7. 添加缓存对象后,调用需判断缓存是否已满
        // 若满了就删除近期最少使用的对象-->分析2
        trimToSize(maxSize);

        return previous;
    }

/** 
  * 分析1:trimToSize(maxSize)
  * 原理:不断删除LinkedHashMap中队尾的元素,即近期最少访问的元素,直到缓存大小 < 最大值
  **/ 
  public void trimToSize(int maxSize) {

        //死循环
        while (true) {
            K key;
            V value;
            synchronized (this) {
                // 判断1:若 map为空 & 缓存size ≠ 0 或 缓存size < 0,则抛出异常
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                // 判断2:若缓存大小size < 最大缓存 或 map为空,则不需删除缓存对象,跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                // 开始删除缓存对象
                // 使用迭代器获取第1个对象,即队尾的元素 = 近期最少访问的元素
                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);
        }
    }

至此,关于添加缓存: put() 的源码分析完毕。

6.2 获取缓存:get()

  • 作用:获取缓存 & 更新队列
    get()
    
  • 示意图如下
    Android 内存缓存:手把手教你学会LrhCache算法

上述更新过程是在 get() 中完成

  • 源码分析
/** 
  * 使用函数(以加载图片为例)
  **/ 
  mMemoryCache.get(key);

/** 
  * 源码分析
  **/ 
  public final V get(K key) {

        // 1. 判断输入的合法性:若key为空,则抛出异常
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {

            // 2. 获取对应的缓存对象 & 将访问的元素 更新到 队列头部->>分析3
            mapValue = map.get(key);

            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

/** 
  * 分析1:map.get(key)
  * 实际上是 LinkedHashMap.get() 
  **/ 
  public V get(Object key) {

        // 1. 获取对应的缓存对象
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;

        // 2. 将访问的元素更新到队列头部 ->>分析4
        e.recordAccess(this);

        return e.value;
    }
/** 
  * 分析2:recordAccess()
  **/ 
  void recordAccess(HashMap<K,V> m) {

            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;

            // 1. 判断LinkedHashMap存储顺序是否按访问 排序 排序:根据构造函数传入的参数accessOrder判断
            if (lm.accessOrder) {
                lm.modCount++;
                // 2. 删除此元素
                remove();
                // 3. 将此元素移动到队列的头部
                addBefore(lm.header);
            }
        }

至此,关于获取缓存: get() 的源码分析完毕。

7. 总结

本文全面讲解了内存缓存的相关知识,含 LrhCache 算法、原理等,下面是部分总结

  • 原理
    Android 内存缓存:手把手教你学会LrhCache算法
  • 示意图
    Android 内存缓存:手把手教你学会LrhCache算法
  • 源码流程
    Android 内存缓存:手把手教你学会LrhCache算法

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据挖掘十大算法

数据挖掘十大算法

(美)吴信东(Xindong Wu)、(美),库玛尔 ,(Vipin Kumar) / 李文波、吴素研 / 清华大学出版社 / 2013-5 / 39.00元

《世界著名计算机教材精选:数据挖掘十大算法》详细介绍了在实际中用途最广、影响最大的十种数据挖掘算法,这十种算法是数据挖掘领域的顶级专家进行投票筛选的,覆盖了分类、聚类、统计学习、关联分析和链接分析等重要的数据挖掘研究和发展主题。《世界著名计算机教材精选:数据挖掘十大算法》对每一种算法都进行了多个角度的深入剖析,包括算法历史、算法过程、算法特性、软件实现、前沿发展等,此外,在每章最后还给出了丰富的习......一起来看看 《数据挖掘十大算法》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试