softReference+LruCache优化Android缓存

近眼看世界

大家好,我叫石头.

关于 SoftReference 在缓存中的使用问题,Android 在官方文档 SoftReference,明确指出

Avoid Soft References for Caching

Google从Android 2.3+开始宣布说,他们要从此版本开始,让GC更加频繁地去回收具有软引用对象的内存,好吧。。。动不动就被GC回收了,那我们的对象岂不就会经常丢失?对的,这样的话,SoftReference虽然不会造成OOM,但是我们的数据就会丢失,就会变的十分不可靠了


Most applications should use an android.util.LruCache instead of soft references. LruCachehas an effective eviction policy and lets the user tune how much memory is allotted.

为什么Android明确要求开发者们放弃SoftReference呢,官方给出的原因:

In practice, soft references are inefficient for caching. The runtime doesn't have enough information on which references to clear and which to keep. Most fatally, it doesn't know what to do when given the choice between clearing a soft reference and growing the heap.


在实践中,软引用(soft references)在缓存中是低效的,因为runtime并没有足够的信息来判别应该清除或者保留哪个 SoftReference(持有的对象),更无法判定当 App 要求更多内存的时候,是应该清除 SoftReference,还是增大 App 的Heap。

当我们听到这句话的时候是不是感觉很合理呀,但是按照我们的理解这个根本说不过去啊。

因为在正常的 JVM中,只要不会触发 OOM(达到系统内存上限或者到达 JVM 设定的内存上限),JVM 就应该毫不留情的增大 Heap 来维持应用的正常运行。 而没有必要考虑是先清理 SoftReference,还是增大 Heap 这种无聊的问题。

Android RuntimeJVM 不一样的是:用户 App 通常没有权限来设定自己的最大可用内存,这个是由系统控制的, 单个 App 使用的最大内存容量是固定的:

Runtime.getRuntime().maxMemory()

其他就是跟 JVM 差不多了,Android 在启动每一个 App 的时候,也并不是一开始就给每个 App 分配固定的上限内存,也是按需动态分配,所以,这应该不是技术问题。
官方也为我们给出了原因:

The lack of information on the value to your application of each reference limits the usefulness of soft references. References that are cleared too early cause unnecessary work; those that are cleared too late waste memory.

让我们回顾下软引用

  • 创建软引用HashMap作为缓存
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
  • 向缓存中添加新Bitmap
public void addBitmapToCache(String path) {
        // 强引用的Bitmap对象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加该对象到Map中使其缓存
        imageCache.put(path, softBitmap);
    }

注意:由于bitmap为局部变量, 当方法结束时,bitmap被销毁,其指向的内存空间依然只有imageCache中的软引用。

  • 从缓存中读取Bitmap
public Bitmap getBitmapByPath(String path) {
        // 从缓存中取软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        if(bitmap==null){
            return null;
        }
       return bitmap;
    }

软引用释放资源是被动的, 当内存不足时, GC会对其主动回收。
下面开始我们的主菜~~~


LruCache

LruCache 是对限定数量的缓存对象持有强引用的缓存,每一次缓存对象被访问,都会被移动到队列的头部。LruCache类包含在android-support-v4包中,使用方法和其他缓存一样:加载图片前判断缓存中是否已经存在, 如果不存在就重新从图片源加载。

我们应该注意到了LruCache中的前3个单词LRU,是不是有点眼熟呢,所谓LRU,即为 Least recently used,近期最少使用策略,其实很熟悉啦,操作系统还是学过的,嘿嘿~~~。

与使用SoftReference不同,LruCache内部通过一个LinkedHashMap保存资源的强引用。其控制内存的方式是主动的,需要在内部记录当前缓存大小, 并与初始化时设置的max值比较,如果超过, 就将排序最靠前(即最近最少使用)的资源从LinkedHashMap中移除。这样, 就没有任何引用指向资源的内存空间了。该内存空间无人认领, 会在GC时得到释放。
关于LinkedHashMap, 其是HashMap的子类, 支持两种排序方式, 第一种是根据插入顺序排序, 第二种就是根据访问进行排序。采用哪种排序方式由其构造函数传入参数决定。在LruCache中, 初始化LinkedHashMap的代码如下:

this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

其中最后一个参数, 就是是否根据访问进行排序。

LruCache的具体实现:

private LruCache<String, Bitmap> mMemoryCache;

  @Override

  protected void onCreate(Bundle savedInstanceState) {

  // 获取到可用内存的最大值,使用内存超出这个值会引起  OutOfMemory异常。

  // LruCache通过构造函数传入缓存值,以KB为单位。

  int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

  // 使用最大可用内存值的1/8作为缓存的大小。

  int cacheSize = maxMemory / 8;

  mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

    @Override

    protected int sizeOf(String key, Bitmap bitmap) {

    // 必须重写此方法来衡量每张图片的大小,默认返回图片数量。

      return bitmap.getByteCount() / 1024;

    }

  };

}

 

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {

    if (getBitmapFromMemCache(key) == null) {

          mMemoryCache.put(key, bitmap);

    }

}


public Bitmap getBitmapFromMemCache(String key) {

    return mMemoryCache.get(key);

}

/**2种情况:

*1.当有条目被挤出时,evicted 为true, key与oldValue

为被挤出的条目的值

*2.有条目值发生改变时,evicted 为false ,使用put()替换值

* key 为替换条目的key oldValue为条目之前的值

newValue 为条目的新值

* 使用remove()时, key与oldValue

为被移除的条目

*/

@Override

protected void entryRemoved(boolean evicted, String key,

Bitmap oldValue, Bitmap newValue) {

System.out.println("evicted:" + evicted + "key:" + key

+ "oldValue:" + oldValue + "newValue:" + newValue);

}

public Bitmap removeBitmapFromMemCache(String key) {

return mMemoryCache.remove(key);

}

关于recycle()调用
其实最早在使用LruCache或者软引用的时候, 我产生了这样的疑问:GC可以释放没有强引用指向的内存,但Bitmap的图片资源(像素数据), 不是保存在native层, 需要显示调用recycle方法进行内存释放吗。而在一些人关于LruCache的博客中, 看到博主回复类似问题,说该操作由LruCache帮助完成了。然而我看遍了LruCache 的源码, 也没有看到哪里有释放底层资源的操作,这反而更加深了我的疑惑。 后来在网上看到了这样的说明, 即在Android 3.0(Level 11)及其以后, Bitmap的像素数据与Bitmap的对象一起保存在Java堆中, 如此, 系统GC时, 也可以一起将像素资源回收了。 要注意的是, 在使用LruCache时, 千万不要画蛇添足, 在LruCache的entryRemoved回调中实现对释放资源的手动recycle。 因为虽然该Bitmap从LinkedHashMap中被移除了, 但我们无法得知外部是否还有对当前Bitmap的引用。如果还有ImageView正显示着该图片, 那必然会导致崩溃。

LruCache源码

发现一堆int类型的变量,还有一个最重要的LinkedHashMap<K,V> 这个队列,通俗的讲LinkedHashMap<K,V>就是一个双向链表存储结构。

各个变量的意思为:
size - LruCache中已经存储的大小
maxSize - 我们定义的LruCache缓存最大的空间
putCount- put的次数(为LruCache添加缓存对象的次数)
createCount - create的次数
evictionCount - 回收的次数
hitCount - 命中的次数
missCount - 丢失的次数

结合SoftReference和LruCache的二级缓存结构

整个思路是:使用了系统提供的LruCache类做一级缓存, 大小为运行内存的1/8,当LruCache容量要满的时候,会自动将系统移除的图片放到二级缓存中,但为了避免OOM的问题,这里将SoftReference软引用加入来,当系统快要OOM的时候会自动清除里面的图片内存,当然内存充足时就会继续保存这些二级缓存的图片.强调一点,不要用SoftReference去做一级缓存,现在的java中垃圾回收加强了对SoftReference软引用的回收机制,它只适合临时的保存一些数据缓存,并不适合长期的(相对临时而言,并不是真正的长期).

package com.shi.quan.lurcache;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.widget.ImageView;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by liaoshiquan on 2017/2/7.
 */

public class ImageLoadManager {
    public enum IMAGE_LOAD_TYPE
    {
        FILE_PATH,FILE_URL,FILE_RESOURCE_ID
    }

    private String TAG = "ImageLoadManager...";

    private Context context;
    private Set<ImageLoadTask> taskCollection;
    /** 最大内存 **/
    final static int maxCacheSize = (int)(Runtime.getRuntime().maxMemory() / 8);
    /** 建立线程安全,支持高并发的容器 **/
    private static ConcurrentHashMap<String, SoftReference<Bitmap>> currentHashmap
            = new ConcurrentHashMap<String, SoftReference<Bitmap>>();

    public ImageLoadManager(Context context)
    {
        super();
        this.context = context;
        taskCollection = new HashSet<ImageLoadTask>();
    }

    private static LruCache<String, Bitmap> BitmapMemoryCache = new LruCache<String, Bitmap>(maxCacheSize)
    {
        @Override
        protected int sizeOf(String key, Bitmap value)
        {
            if(value != null)
            {
                return value.getByteCount();
                //return value.getRowBytes() * value.getHeight(); //旧版本的方法
            }
            else
            {
                return 0;
            }
        }
        //这个方法当LruCache的内存容量满的时候会调用,将oldValue的元素移除出来腾出空间给新的元素加入
        @Override
        protected void entryRemoved(boolean evicted, String key,Bitmap oldValue, Bitmap newValue)
        {
            if(oldValue != null)
            {
                // 当硬引用缓存容量已满时,会使用LRU算法将最近没有被使用的图片转入软引用缓存
                currentHashmap.put(key, new SoftReference<Bitmap>(oldValue));
            }
        }

    };

    /**
     * 针对提供图片资源ID来显示图片的方法
     * @param loadType 图片加载类型
     * @param imageResourceID 图片资源id
     * @param imageView 显示图片的ImageView
     */
    public void setImageView(IMAGE_LOAD_TYPE loadType, int imageResourceID, ImageView imageView)
    {
        if(loadType == IMAGE_LOAD_TYPE.FILE_RESOURCE_ID)
        {
//   if(ifResourceIdExist(imageResourceID))
//   {
//    imageView.setImageResource(imageResourceID);
//
//   }else{ //映射无法获取该图片,则显示默认图片
//    imageView.setImageResource(R.drawable.pic_default);
//   }
            try
            {
                imageView.setImageResource(imageResourceID);
                return;
            } catch (Exception e) {
                Log.e(TAG, "Can find the imageID of "+imageResourceID);
                e.printStackTrace();
            }
            //默认图片
            imageView.setImageResource(R.mipmap.ic_launcher);
        }
    }

    /**
     * 针对提供图片文件链接或下载链接来显示图片的方法
     * @param loadType  图片加载类型
     * @param imageFilePath 图片文件的本地文件地址或网络URL的下载链接
     * @param imageView 显示图片的ImageView
     */
    public void setImageView(IMAGE_LOAD_TYPE loadType, String imageFilePath, ImageView imageView)
    {
        if(imageFilePath == null || imageFilePath.trim().equals(""))
        {
            imageView.setImageResource(R.mipmap.ic_launcher);

        }else{
            Bitmap bitmap = getBitmapFromMemoryCache(imageFilePath);
            if(bitmap != null)
            {
                imageView.setImageBitmap(bitmap);
            }
            else
            {
                imageView.setImageResource(R.mipmap.ic_launcher);
                ImageLoadTask task = new ImageLoadTask(loadType, imageView);
                taskCollection.add(task);
                task.execute(imageFilePath);
            }
        }
    }

    /**
     * 从LruCache中获取一张图片,如果不存在就返回null
     * @param key  键值可以是图片文件的filePath,可以是图片URL地址
     * @return Bitmap对象,或者null
     */
    public Bitmap getBitmapFromMemoryCache(String key)
    {
        try
        {
            if(BitmapMemoryCache.get(key) == null)
            {
                if(currentHashmap.get(key) != null)
                {
                    return currentHashmap.get(key).get();
                }
            }
            return BitmapMemoryCache.get(key);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return BitmapMemoryCache.get(key);
    }

    /**
     * 将图片放入缓存
     * @param key
     * @param bitmap
     */
    private void addBitmapToCache(String key, Bitmap bitmap)
    {
        BitmapMemoryCache.put(key, bitmap);
    }



    /**
     * 图片异步加载
     * @author Mr.Et
     *
     */
    private class ImageLoadTask extends AsyncTask<String, Void, Bitmap>
    {
        private String imagePath;
        private ImageView imageView;
        private IMAGE_LOAD_TYPE loadType;

        public ImageLoadTask(IMAGE_LOAD_TYPE loadType , ImageView imageView)
        {
            this.loadType = loadType;
            this.imageView = imageView;
        }

        @Override
        protected Bitmap doInBackground(String...params)
        {
            imagePath = params[0];
            try
            {
                if(loadType == IMAGE_LOAD_TYPE.FILE_PATH)
                {
                    if(new File(imagePath).exists())
                    { //从本地FILE读取图片
                        BitmapFactory.Options opts = new BitmapFactory.Options();
                        opts.inSampleSize = 2;
                        Bitmap bitmap = BitmapFactory.decodeFile(imagePath, opts);
                        //将获取的新图片放入缓存
                        addBitmapToCache(imagePath, bitmap);
                        return bitmap;
                    }
                    return null;
                }
                else if(loadType == IMAGE_LOAD_TYPE.FILE_URL)
                { //从网络下载图片
                    byte[] datas = getBytesOfBitMap(imagePath);
                    if(datas != null)
                    {
//      BitmapFactory.Options opts = new BitmapFactory.Options();
//      opts.inSampleSize = 2;
//      Bitmap bitmap = BitmapFactory.decodeByteArray(datas, 0, datas.length, opts);
                        Bitmap bitmap = BitmapFactory.decodeByteArray(datas, 0, datas.length);
                        addBitmapToCache(imagePath, bitmap);
                        return bitmap;
                    }
                    return null;
                }

            } catch (Exception e) {
                e.printStackTrace();
//                FileUtils.saveExceptionLog(e);
                //可自定义其他操作
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap)
        {
            try
            {
                if(imageView != null)
                {
                    if(bitmap != null)
                    {
                        imageView.setImageBitmap(bitmap);
                    }
                    else
                    {
                        Log.e(TAG, "The bitmap result is null...");
                    }
                }
                else
                {
                    Log.e(TAG, "The imageView is null...");
                    //获取图片失败时显示默认图片
                    imageView.setImageResource(R.mipmap.ic_launcher);
                }

            } catch (Exception e) {
                e.printStackTrace();
//                FileUtils.saveExceptionLog(e);
            }
        }


    }

    /**
     * InputStream转byte[]
     * @param inStream
     * @return
     * @throws Exception
     */
    private byte[] readStream(InputStream inStream) throws Exception{
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[2048];
        int len = 0;
        while( (len=inStream.read(buffer)) != -1){
            outStream.write(buffer, 0, len);
        }
        outStream.close();
        inStream.close();
        return outStream.toByteArray();
    }

    /**
     * 获取下载图片并转为byte[]
     * @param imgUrl
     * @return
     */
    private byte[] getBytesOfBitMap(String imgUrl){
        try {
            URL url = new URL(imgUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(10 * 1000);  //10s
            conn.setReadTimeout(20 * 1000);
            conn.setRequestMethod("GET");
            conn.connect();
            InputStream in = conn.getInputStream();
            return readStream(in);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 该资源ID是否有效
     * @param resourceId 资源ID
     * @return
     */
    private boolean ifResourceIdExist(int resourceId)
    {
        try
        {
            Field field = R.drawable.class.getField(String.valueOf(resourceId));
            Integer.parseInt(field.get(null).toString());
            return true;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 取消所有任务
     */
    public void cancelAllTask()
    {
        if(taskCollection != null){
            for(ImageLoadTask task : taskCollection)
            {
                task.cancel(false);
            }
        }
    }
}

这里为了举例使用了AsyncTask,但是在实际项目中,我们不是很推荐使用AsyncTask,因为它有很多潜在的问题,这里我们推荐"泡在网上的日子"的一篇关于AsyncTaskAsyncTaskLoader的替代品使用RxJava.Observable取代AsyncTask和AsyncTaskLoader来替代代码中的AsyncTask网络部分.


每日箴言

只有登上山顶,才能看到那边的风光。

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

推荐阅读更多精彩内容