关闭

Android 之 三级缓存(内存!!!、本地、网络)及内存LruCache扩展 及源码分析--- 学习和代码讲解

标签: android三级缓存内存缓存LruCache网络缓存
1242人阅读 评论(0) 收藏 举报
分类:

一. 三级缓存简介

这里写图片描述

如上图所示,目前App中UI界面经常会涉及到图片,特别是像“今日关注”新闻这类app中,图片运用的几率十分频繁。当手机上需要显示大量图片类似listView、gridView控件并且用户会上下滑动,即将浏览过的图片又加载一遍,若是不停的进行网络请求,很快就会OOM,这时三级缓存显得尤为重要,适时地利用资源,进行图片缓存,下面就用一个新闻组图demo进行图片缓存演示。


1.三级缓存的顺序
(1)内存缓存: 比如说需要加载图片时,系统第一步不会直接网络请求,而是首先找第一级缓存—内存缓存
(2)本地缓存: 如果内存缓存中没有,就会从第二级缓存—本地缓冲(即sd卡)
(3)网络缓存: 如果本地缓存中没有,就会从网络缓存中下载图片。

这里写图片描述


2. 三级缓存级别总结
(1)内存缓存: 速度快, 优先读取
(2)本地缓存: 速度其次, 内存没有,读本地
(3)网络缓存: 速度最慢, 本地也没有,才访问网络




二. 代码实现

关于这个三级缓存的实现,其实 Xutils开源项目中BitmapUtils已经替我们封装好了,下面新建一个MyBitmapUtils,自己实现三级缓存。

1.网络缓存(NetCacheUtils )

  /**
     * 三个泛型意义:
     * 第一个泛型:doInBackground里的参数类型
     * 第二个泛型: onProgressUpdate里的参数类型
     * 第三个泛型:
     * onPostExecute里的参数类型及doInBackground的返回类型
     */
    private class BitmapTask extends AsyncTask<Object, Integer,Bitmap>{
        //1.预加载,运行在主线程
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }
        //2.正在加载,运行在子线程(核心方法),可以直接异步请求
        @Override
        protected Bitmap doInBackground(Object[] objects) {
            return null;
        }
        //3.更新进度的方法,运行在主线程
        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
        }
        //4.加载结束,运行在主线程(核心方法),可以直接更新UI
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
        }
    }

这里的逻辑就是 doInBackground方法 异步网络请求图片,onPostExecute方法将图片加载呈现出来。而 onPreExecute 的作用是预加载,使用不常。至于onProgressUpdate 可显示出请求图片过程中的进度,这两个并非核心方法。

(1)doInBackground : 核心方法,请求网络。大家都知道请求网络是一个耗时操作,需要在子线程中进行,这里也确实如此,不过不需要我们再new 一个Thread ,查看源码可知异步AsyncTask已经帮我们做到了。在这一步需要做的就是,获得方法参数中的url,进行网络请求,下载图片获得Bitmap.

(2)onPostExecute: 核心方法,图片加载完成后,显示在手机屏幕上。大家也了解子线程中无法做UI更新,需要使用消息机制,给handler发送消息,在主线程中更新UI。这里异步也都替我们做好了,查看源码可知UI更新是在异步中的hanler中进行。在这一步需要做的就是,将请求获得的Bitmap呈现到屏幕上。(更规范的是,还要将获得的Bitmap存储到内存和本地中,方便下次使用时可拿取缓存,不需重复请求网络!!!)

/**
 * 网络缓存工具类
 * 
 * 
 */
public class NetCacheUtils {

    LocalCacheUtils mLocalCacheUtils;
    MemoryCacheUtils mMemoryCacheUtils;

    public NetCacheUtils(LocalCacheUtils localCacheUtils,
            MemoryCacheUtils memoryCacheUtils) {
        mLocalCacheUtils = localCacheUtils;
        mMemoryCacheUtils = memoryCacheUtils;
    }

    public void getBitmapFromNet(ImageView ivPic, String url) {
        BitmapTask task = new BitmapTask();
        task.execute(new Object[] { ivPic, url });
    }

    class BitmapTask extends AsyncTask<Object, Void, Bitmap> {

        private ImageView imageView;
        private String url;

        /**
         * 返回的对象会自动回传到onPostExecute里面
         */
        @Override
        protected Bitmap doInBackground(Object... params) {
            imageView = (ImageView) params[0];
            url = (String) params[1];
            imageView.setTag(url);
            Bitmap bitmap = downloadBitmap(url);
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            // 这里的result就是doInBackground返回回来的对象
            if (result != null) {
                String ivUrl = (String) imageView.getTag();
                if (url.equals(ivUrl)) {// 确保imageview设置的是正确的图片(因为有时候listview有重用机制,多个item会公用一个imageview对象,从而导致图片错乱)
                    imageView.setImageBitmap(result);
                    System.out.println("从网络缓存读取图片");

                    // 向本地保存图片文件
                    mLocalCacheUtils.putBitmapToLocal(url, result);
                    // 向内存保存图片对象
                    mMemoryCacheUtils.putBitmapToMemory(url, result);
                }
            }
        }
    }

    /**
     * 下载图片
     * 
     * @param url
     * @return
     */
    private Bitmap downloadBitmap(String url) {
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(5000);
            conn.setRequestMethod("GET");
            conn.connect();

            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                InputStream inputStream = conn.getInputStream();

                //图片压缩
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inSampleSize = 2;//表示压缩比例,2表示宽高都压缩为原来的二分之一, 面积为四分之一
                options.inPreferredConfig = Config.RGB_565;//设置bitmap的格式,565可以降低内存占用

                Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
                return bitmap;
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            conn.disconnect();
        }

        return null;
    }
}



2. 本地缓存(LocalCacheUtils )

/**
 * 本地缓存工具类
 * 
 * 
 */
public class LocalCacheUtils {

    private static final String LOCAL_PATH = Environment
            .getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache";

    /**
     * 从本地读取图片
     * 
     * @param url
     * @return
     */
    public Bitmap getBitmapFromLocal(String url) {
        try {
            String fileName = MD5Encoder.encode(url);
            File file = new File(LOCAL_PATH, fileName);

            if (file.exists()) {
                // 图片压缩
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inSampleSize = 2;// 表示压缩比例,2表示宽高都压缩为原来的二分之一, 面积为四分之一
                options.inPreferredConfig = Config.RGB_565;// 设置bitmap的格式,565可以降低内存占用

                Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(
                        file), null, options);
                return bitmap;
            } else {
                return null;
            }

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

    /**
     * 向本地存图片
     * 
     * @param url
     * @param bitmap
     */
    public void putBitmapToLocal(String url, Bitmap bitmap) {
        try {
            String fileName = MD5Encoder.encode(url);
            File file = new File(LOCAL_PATH, fileName);
            File parent = file.getParentFile();

            // 创建父文件夹
            if (!parent.exists()) {
                parent.mkdirs();
            }

            bitmap.compress(CompressFormat.JPEG, 100,
                    new FileOutputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}


如上所示,这里对于本地(sd卡)缓存的操作就两个方法,比较单一,一个存储缓存数据方法—putBitmapToLocal,一个拿取缓存数据方法—getBitmapToLocal


(1)putBitmapToLocal: 这里我们将每个存储图片的文件名设为 图片对应的url地址(MD5加密后的),判断父文件是否存在,不存在则新建,存在则直接存储进去。


(2)getBitmapToLocal: 先从方法参数中获取到图片对应的url,进行查找,若存在则将图片的Bitmap返回回去(最好返回前先压缩),不存在则返回null。






3. 内存缓存(LocalCacheUtils )重点!!!


3.1 HashMap版

/**
 * 内存缓存工具类
 */
public class MemoryCacheUtils {

     HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;
    /**
     * 从内存读取图片
     * 
     * @param url
     * @return
     */
    public Bitmap getBitmapFromMemory(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
        return bitmap;
    }

    /**
     * 向内存存图片
     * 
     * @param url
     * @param bitmap
     */
    public void putBitmapToMemory(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
    }
}


如上所示,这里对于内存缓存的操作也是两个方法,一个是设置内存缓存方法—putBitmapToLocal,一个是取内存缓存方法—getBitmapToLocal。用对象来存储图片,集合来存储对象,集合都在内存里面,所以决定用集合。

关于Android,集合就涉及到两个,ArrayList用的多,但是取数据时必须要传递数组位置;但是Hashmap用的是键值对结构,只要有了key,就可以找到对应的value。(而我们这里的key就是每张图片对应的urlvalue就是每个图片 Bitmap对象




3.2 软引用版

你说以上就是内存缓存的重点?绝对不可能,Bitmap对象虽存在于集合中,但我们每次都 new 一个新的Bitmap,如果有大量的图片,集合内存根本不够,很快就会OOM,也就是内存溢出。也许你的手机内存很大,但是不管安卓设备总内存有多大,它只给每个APP分配一定内存大小(16M),所以内存是非常有限的,而且在这里 垃圾回收机制是不起作用的!

这里写图片描述

3.2.1 栈、堆、垃圾回收器
如上图所示,内存缓存这里涉及到栈和堆。java里的一般存的是成员变量、方法声明、引用之类的。里存储的是一个又一个的对象。(例如,new了一个 p,p存在栈里,但是 person对象存储在 堆中,p引用,指向一个person对象)。垃圾回收器会定时地从堆里回收圾释放内存。(例如上图,只要栈与堆中的连接断掉,堆中的对象就是垃圾,回收站可进行回收。所以说,垃圾回收器有个特点:只回收没有引用的对象!

再回到内存溢出上,我们
HashMap<String,Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;
集合中有许多个对象,都被集合引用!这个引用一直在!垃圾回收器并不会回收,所以会导致内存溢出。以上只是一方面,而且即使它会回收这些引用的集合,可它是隔一段时间才会回收,无法及时清理内存!



现在我们需要解决的是:能否在引用的情况下,垃圾回收器可以照样回收?

3.2.2 内存缓存中的 引用级别

(1) 强引用 默认引用, 即使内存溢出,也不会回收

(2) 软引用 SoftReference, 内存不够时, 会考虑回收

(3) 弱引用 WeakReference 内存不够时, 更会考虑回收

(4)虚引用 PhantomReference 内存不够时, 最优先考虑回收!


Person p = new Person();就属于强引用。回收器断然不会回收!而虚引用则太容易被回收,所以最常用的是软引用弱引用,在需求不强烈或内存实在是不够的情况下,垃圾回收器才会回收引用的对象。我们主要回收的是Bitmap对象,对集合进行包装,使用软引用

//用法举例
Bitmap bitmap = new Bitmap();
SoftReference<Bitmap> sBitmap = new SoftReference<Bitmap>(bitmap);
Bitmap bitmap2 = sBitmap.get();


( 软引用版):

/**
 * 内存缓存工具类
 */
public class MemoryCacheUtils {

     HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>> ;
    /**
     * 从内存读取图片
     * 
     * @param url
     * @return
     */
    public Bitmap getBitmapFromMemory(String url) {
    SoftReference<Bitmap> softBitmap = mMemoryCache.get(url);

    if(softReference != null){
        Bitmap bitmap = softReference.get();
        return bitmap;
     }
    return null;    
}

    /**
     * 向内存存图片
     * 
     * @param url
     * @param bitmap
     */
    public void putBitmapToMemory(String url, Bitmap bitmap) {
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        mMemoryCache.put(url, bitmap);
    }
}



3.3 LruCache 版(重点!!!)

(一位大神关于这一点的讲解,很好,下面有引用部分
http://blog.csdn.net/fancylovejava/article/details/25705169

可是自从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。这么说来即使内存很充分的情况下,也有优先回收弱引用和软引用。
官方文档的截图:
这里写图片描述
https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html官方链接

翻译:
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。
但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,
这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,
这就有潜在的风险造成应用程序的内存溢出并崩溃。所以看到还有很多相关文章还在推荐用软引用或弱引用 (SoftReference or WeakReference),就有点out了。

所以为了解决这个问题,google为我们推荐了LruCache类,这个类在 V4包 下,非常适合用来缓存图片。


3.3.1 LruCache
Lru定义 :least recentlly used 最近最少使用的算法。(比如说,先后使用A、B、C、A、C、D对象,这时会回收的则是B对象。)
LruCache : 可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围!

3.3.2 分配空间
这里写图片描述

获得分配给App最大的内存大小 —— 16M(16777216/1024)

long maxMemory = Runtime.getRuntime().maxMemory();
mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) 

但是在分配内存的过程中,切不可一次分配全部内存出去,毕竟这只是App的一部分模块,其余部分还需要空间。(分配1/8 —— 2M)


3.3.2 重写LruCache 的 sizeOf方法
这个方法要返回每个对象的大小。Lru要控制内存的总大小,所以它需要知道每个Bitmap有多大。所以需要重写这个方法,让开发者自己计算,返回大小。

            protected int sizeOf(String key, Bitmap value) {
                // int byteCount = value.getByteCount();
                int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
                return byteCount;
            }



( LruCache版):

private LruCache<String, Bitmap> mMemoryCache;

    public MemoryCacheUtils() {

        long maxMemory = Runtime.getRuntime().maxMemory();// 获取分配给app的内存大小
        System.out.println("maxMemory:" + maxMemory);

        mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) {

            // 返回每个对象的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // int byteCount = value.getByteCount();//有版本兼容问题
                int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
                return byteCount;
            }
        };
    }

    /**
     * 写缓存
     */
    public void setMemoryCache(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
    }

    /**
     * 读缓存
     */
    public Bitmap getMemoryCache(String url) {
        return mMemoryCache.get(url);
    }





4. 工具类,将以上三级缓存封装起来

/**
 * 自定义三级缓存图片加载工具
 */
public class MyBitmapUtils {

    private NetCacheUtils mNetCacheUtils;
    private LocalCacheUtils mLocalCacheUtils;
    private MemoryCacheUtils mMemoryCacheUtils;

    public MyBitmapUtils() {
        mMemoryCacheUtils = new MemoryCacheUtils();
        mLocalCacheUtils = new LocalCacheUtils();
        mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils, mMemoryCacheUtils);
    }

    public void display(ImageView imageView, String url) {
        // 设置默认图片
        imageView.setImageResource(R.drawable.pic_item_list_default);

        // 优先从内存中加载图片, 速度最快, 不浪费流量
        Bitmap bitmap = mMemoryCacheUtils.getMemoryCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            System.out.println("从内存加载图片啦");
            return;
        }

        // 其次从本地(sdcard)加载图片, 速度快, 不浪费流量
        bitmap = mLocalCacheUtils.getLocalCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            System.out.println("从本地加载图片啦");

            // 写内存缓存
            mMemoryCacheUtils.setMemoryCache(url, bitmap);
            return;
        }

        // 最后从网络下载图片, 速度慢, 浪费流量
        mNetCacheUtils.getBitmapFromNet(imageView, url);
    }

}

以上,将工具类封装号之后,我们可以不使用 Xutils里的方法,使用我们自定义的MyBitmapUtils,以下代码为调用过程。

class PhotoAdapter extends BaseAdapter {

        //private BitmapUtils mBitmapUtils;
        private MyBitmapUtils mBitmapUtils;

        public PhotoAdapter() {
            mBitmapUtils = new MyBitmapUtils();
            //mBitmapUtils = new BitmapUtils(mActivity);
//          mBitmapUtils
//                  .configDefaultLoadingImage(R.drawable.pic_item_list_default);
        }





三. 结果呈现

呈现出来的顺序就是:

这里写图片描述

这是我测试之后的,如果是第一次打开这个模块,最先使用的只能是网络缓存,一旦第一次进行网络缓存后,本地缓存和内存缓存就会有相应的数据。下一次再打开此模块时,首先加载的是本地缓存,得到Bitmap**对象后,之后进行的都是 内存缓存**了。



四. LruCache扩展及源码分析(重点)

Lru 就像我们家用的洗漱池里小开口,水龙头流出来的水就像是内存,所以我们的洗漱池会堵吗?不会!如果你把口子给堵起来防水,很快水就会满出来,就像是 内存溢出。这里,我们来看下 V4 包下的 LruCache 源码

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

点进去一看,Lrucache是一个泛型,它维护了一个 LinkedHashMap,将来在存图片的时候,底层也是存在一个HashMap里。



1. LruCache 的 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++;
 //!!!!!!!  size += safeSizeOf(key, value);
//!!!!!!!   previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

//!!!!!!! trimToSize(maxSize);
        return previous;
    }

我们去找它的一个put 方法。
previous = map.put(key, value);标记感叹号地方 的 map 就是 一开始的 LinkedHashMap,底层就是对HashMap的封装。
size += safeSizeOf(key, value);
全局维护了一个变量size,时时在统计集合目前对象大小。它走的就是sizeOf方法。但是源码中方法返回的是1,

protected int sizeOf(K key, V value) {
        return 1;
    }


所以我们需要去重写它的sizeOf方法。所以LruCache 在put 的时候都会把总大小计算出来,然后调用trimToSize(maxSize);方法,来看下此方法源码

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

一上来就是一个While循环,先不看抛出异常,直接看if判断if (size <= maxSize || map.isEmpty()) { break; },如果内存正常,则break出去,否则

   Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;


通过map 拿到迭代器的第一个对象,再直接拿到key,再remove出去,所以总内存大小就减少了。这时继续While循环,因为减少一个不一定符合大小,所以一直减少直到内存大小少于规定值为止!

所以LruCache所谓的算法:可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围
其中的核心原理就在这里,不停的删掉开头的key,这就是最近最少用的对象。



2. LruCache 的 get 方法

 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) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

这里的get方法则更简单,参数将 key 传过来,直接从map中get出对象,再return出来就行了。



3. LruCache 的 核心
最核心的地方其实就是维护一个 HashMap,再设置了一个全局变量 size来计算变量的总大小。一旦超出大小,就开始删除对象,从而保证内存量在规定范围内!





呼~这篇文章总算写完了,拖了好多天,希望对你们有帮助 :)

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:142076次
    • 积分:2468
    • 等级:
    • 排名:第16931名
    • 原创:101篇
    • 转载:1篇
    • 译文:0篇
    • 评论:62条
    博客专栏
    最新评论