(1)LruCache原理分析

浅析LruCache原理

Android用LruCache(Least recently use Cache 意思就是最近使用次数最少的那个对象)来取代原来强引用和软引用实现内存缓存,因为据说自2.3以后Android将更频繁的调用GC,导致软引用缓存的数据极易被释放。

LruCache使用一个LinkedHashMap简单的实现内存的缓存,没有软引用,都是强引用。如果添加的数据大于设置的最大值,就删除最先缓存的数据来调整内存。

他的主要原理在trimToSize方法中。

首先是LruCache声明的变量

 
    private final LinkedHashMap<K, V> map;//LruCache关键的数据结构,用于存放数据

   /**划重点,这俩个变量很关键*/
    private int size;//当前LruCache的内存占用大小
    private int maxSize;//LruCache的最大容量(通过构造方法初始化的值,他表示这个缓存能缓存的最大值是多少。)

    private int putCount;//put的次数
    private int createCount;//create的次数
    private int evictionCount;//回收的次数
    private int hitCount;//命中的次数
    private int missCount;//丢失的次数

然后直接看关键方法trimToSize

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!");
                }
                //1 判断size是否超过maxSize
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                //2 如果1处判断为不超过就取出最先插入的缓存
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                //删除(这里是双向链表)
                map.remove(key);
                size -= safeSizeOf(key, value);//1 
                evictionCount++;
            }

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

这个方法是一个无限循环,跳出循环的条件是,size < maxSize或者 map 为空。主要的功能是判断当前容量时候已经超出最大的容量,如果超出了maxSize的话,就会循环移除map中的第一个元素,直到达到跳出循环的条件。

而双向链表LinkedHashMap(3个参数构造方法中accessOrder排序模式设置为访问模式时)中,每次get和put数据,则会将改对象移到链表的尾部,这样子内存缓存达到最大值时,map中的第一个元素就是最近最少使用的那个元素。此时,把它删除,从而达到避免OOM的出现。

注释1处safeSizeOf中封装了sizeOf方法,它是用来计算单个对象的大小,这里默认返回1,一般需要重写该方法来计算对象的大小,如果是计算bitmap的大小,这里会重写不返回1,而是返回bitmap的大小bitmap.getRowBytes() * bitmap.getHeight()


源码分析

变量

在上面了

image

构造函数

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);//1
}

这里设置了maxSize,以及实例化了一个LinkedHashMap对象,这个LinkedHashMap对象是实现Lru算法的关键,注释1处表示创建一个初始容量为0,加载因子是0.75(容量达到75%的时候把空间增大1半),最后这个参数上面也说了,是accessOrder,意思是排序模式,这里是true表示排序模式为访问模式(当map中数据有put和get时,当前操作的数据会移动到链表尾部)

put方法

    public final V put(K key, V value) {
         if (key == null || value == null) {
             throw new NullPointerException("key == null || value == null");
         }
 
         V previous;
         synchronized (this) {
             putCount++;//put的次数+1
             size += safeSizeOf(key, value);  //size加上预put对象(value)的大小
             previous = map.put(key, value);//previous为旧的值
             if (previous != null) {
                 //如果之前存在键为key的对象,则size应该减去原来对象的大小(把旧值大小删掉)
                 size -= safeSizeOf(key, previous);
             }
         }
 
         if (previous != null) {
             entryRemoved(false, key, previous, value);//这个是空实现
         }
         //每次新加入对象都需要调用trimToSize方法看是否需要回收
         trimToSize(maxSize);
         return previous;
     }

看注释应该明白了,先是增加size,然后判断以前有没有值,如果有就更新当前的额值,并且size要减去以前的值的大小

entryRemoved是一个空实现,如果我们使用LruCache的时候需要掌握元素移除的信息,可以重写这个方法来获取元素移除的信息。

get方法

    /**
    通过key获取相应的item,或者创建返回相应的item。相应的item会移动到队列的尾部,
    如果item的value没有被cache或者不能被创建,则返回null。*/
    public final V get(K key) {
          if (key == null) {
              throw new NullPointerException("key == null");
          }
  
          V mapValue;
          synchronized (this) {
              mapValue = map.get(key);
              if (mapValue != null) {
                  //mapValue不为空表示命中,hitCount+1并返回mapValue对象
                  hitCount++; //命中 + 1
                  return mapValue;
              }
              missCount++;  //未命中+1
          }
  
          /*
           * 如果未命中,则试图创建一个对象,这里create方法返回null,并没有实现创建对象的方法
           *如果需要事项创建对象的方法可以重写create方法。 假如在图片缓存中,因为图片缓存时内存缓存没有命中会去
           * 文件缓存中去取或者从网络下载,所以并不需要创建。
           */
          V createdValue = create(key);//默认返回null(其实这里就是创建一个空对象的意思)
          if (createdValue == null) {
              return null;
          }
          //假如创建了新的对象,则继续往下执行
          synchronized (this) {
              createCount++;  //创建 + 1
              //将createdValue加入到map中,并且将原来键为key的对象保存到mapValue
              mapValue = map.put(key, createdValue);   
              if (mapValue != null) {
                  // There was a conflict so undo that last put
                 //如果mapValue不为空,则撤销上一步的put操作。
                 map.put(key, mapValue);
             } else {
                 //加入新创建的对象之后需要重新计算size大小
                 size += safeSizeOf(key, createdValue);
             }
         }
 
         if (mapValue != null) {
             entryRemoved(false, key, createdValue, mapValue);
             return mapValue;
         } else {
             //每次新加入对象都需要调用trimToSize方法看是否需要回收
             trimToSize(maxSize);
             return createdValue;
         }
     }

remove方法

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

从内存缓存中根据key值移除某个对象并返回该对象


实例

bb再多不如亲手操作一次

LruCache的实例

这里贴出ImageLoader类源代码,项目源代码PrivateTestProject

public class ImageLoader {

    private LruCache<String, Bitmap> mBitmapLruCache;
    private RecyclerView recyclerView;//传过来的RecyclerView对象
    private Set<ImageLoaderTask> mTask = null;//存异步任务的set集合

    public ImageLoader(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
        mTask = new HashSet<>();
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        mBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
            //重写
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //每次存入缓存即调用
                return value.getByteCount();
            }
        };
    }

    /**
     * 根据图片url下载图片
     *
     * @param imgUrl 图片路径
     * @return bitmap
     */
    public Bitmap getBitmapByImgUrl(String imgUrl) {
        Bitmap bitmap = null;
        HttpURLConnection httpURLConnection = null;
        try {
            URL mUrl = new URL(imgUrl);
            try {
                httpURLConnection = (HttpURLConnection) mUrl.openConnection();
                httpURLConnection.setConnectTimeout(10 * 1000);
                httpURLConnection.setReadTimeout(10 * 1000);
                bitmap = BitmapFactory.decodeStream(httpURLConnection.getInputStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } finally {
            if (httpURLConnection != null) {
                //断开连接
                httpURLConnection.disconnect();
            }
        }
        return bitmap;
    }

    /**
     * 将bitmap加入cache
     */
    public void addBitmapToCache(String urlKey, Bitmap bitmap) {
        mBitmapLruCache.put(urlKey, bitmap);
    }

    /**
     * 从cache获取bitmap
     */
    public Bitmap getBitmapfromCache(String urlKey) {
        return mBitmapLruCache.get(urlKey);
    }

    /**
     * 取消所有下载异步任务
     */
    public void cancelAllTask() {
        if (mTask != null) {
            for (ImageLoaderTask task :
                    mTask) {
                task.cancel(false);
            }
        }
    }

    /**
     * 按当前item的序号区间显示图片
     */
    public void showImages(int startIndex, int endIndex) {
        for (int i = startIndex; i < endIndex; i++) {
            String imageUrl = ImagCacheRAdapter.URLS[i];
            Bitmap bitmap = getBitmapfromCache(imageUrl);//从缓存中获取

            if (bitmap == null) {
                //如果缓存为空,则开启异步线程
                ImageLoaderTask imageLoaderTask = new ImageLoaderTask(imageUrl);
                imageLoaderTask.execute();
                //加入HashSet中
                mTask.add(imageLoaderTask);
            } else {
                ImageView imageView = recyclerView.findViewWithTag(imageUrl);
                imageView.setImageBitmap(bitmap);
            }
        }
    }


    /**
     * 显示图片
     *
     * @param imageView
     * @param imageUrl
     */
    public void showImage(ImageView imageView, String imageUrl) {
        //从缓存中取图片
        Bitmap bitmap = getBitmapfromCache(imageUrl);
        //如果缓存中没有,则去下载
        if (bitmap == null) {
            imageView.setImageResource(R.mipmap.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }
    private class ImageLoaderTask extends AsyncTask<Void, Void, Bitmap> {
        private String mImagerUrl;

        public ImageLoaderTask(String mImagerUrl) {
            this.mImagerUrl = mImagerUrl;
        }

        @Override
        protected Bitmap doInBackground(Void... voids) {
            //获取图片并且加入缓存
            Bitmap bitmap = getBitmapByImgUrl(mImagerUrl);
            if (bitmap != null) {
                addBitmapToCache(mImagerUrl, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = recyclerView.findViewWithTag(mImagerUrl);
            if (null != imageView && null != bitmap) {
                imageView.setImageBitmap(bitmap);
            }
            //显示成功后就把当前的AsyncTask从mTask中移除
            mTask.remove(this);
        }
    }
}

ImageLoaderTask这里继承自AsyncTask,是ImageLoader的内部类

很显然,ImageLoader类最关键在LruCache对象mBitmapLruCache上,这里用了Set集合来使异步加载线程ImageLoaderTask是唯一的

ImageLoader主要逻辑是从内存LruCache对象中加载bitmap数据,如果没有,就网络加载数据变为Bitmap,然后存入LruCache对象mBitmapLruCache中。网络加载完的时候在异步线程中ImageLoaderTask把bitmap数据显示到ImageView

DiskLruCache实现硬盘缓存

Android照片墙完整版,完美结合LruCache和DiskLruCache

硬盘缓存通常用作三级缓存的第二层(本地持久化)

请参考 DiskLruCache基本用法

Android 缓存浅谈(二) DiskLruCache

大体套路

首先初始化,创建缓存目录以及线程池,然后加载图片时,先从缓存中获取(要在子线程中进行),如果缓存中有,则显示图片,如果没有则去下载并加入到缓存中,然后从缓存中获取,再显示。

如果在列表中可能会同时加载多个图片,如果只是一直创建线程,那么对app的性能以及体验都是考验,建议使用线程池机制**


参考

浅析LruCache原理

LruCache 实现原理分析

内存缓存LruCache实现原理

Android 缓存浅谈(一) LruCache

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容