Android中实现图片的三级缓存

一、简介

从网络加载图片.png

如图所示,我们的Android App经常需要通过网络获取图片。但是,如果每次启动App都需要从网络获取图片,或者是想重复浏览一些图片的时候,每次浏览都需要通过网络获取,那么势必就会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的App,其用户数量级肯定会受到影响。其次,每次启动App都需要从网络获取图片,在网络情况不佳的情况下会加载很慢,非常影响用户体验。另外,从开发角度来说,Bitmap的创建非常消耗时间和内存,可能导致频繁GC,进而可能导致卡顿。
因此,提出了三级缓存策略,通过网络、本地、内存三级缓存图片,来减少不必要的流量耗费,加快图片的加载速度,减少卡顿,从而增强用户体验,进而有助于提高App的用户数量级。

二、什么是三级缓存

  • 内存缓存,优先加载,速度最快
  • 本地缓存,次优先加载,速度快
  • 网络缓存,最后加载,速度慢,浪费流量

三、三级缓存原理

  • 首次启动App时,通过网络加载图片,在获取到图片之后将其保存到内存和本地SD卡中;
  • 再次启动App时,优先加载内存中的图片缓存;
  • 如果内存中没有图片缓存,则优先加载本地SD卡中的图片缓存;
    缓存策略:内存作为一级缓存,本地SD卡作为二级缓存,最后是网络加载。加载图片的时候,优先加载内存中的图片缓存,如果内存中没有图片缓存,则优先加载本地SD卡中的图片,如果还没有就进行网络加载。

四、图片三级缓存代码实现

1、网络缓存

这里,我们使用了AsyncTask来进行网络图片的异步加载。简单来说,AsyncTask可以看作是对线程池和Handler的封装。其中,线程池用于线程调度、复用,以及执行任务,Handler则用于异步通信以及消息传递。可以看到,从网络获取图片后,将其分别保存至本地缓存和内存缓存。

public class NetworkCacheUtils {

    private LocalCacheUtils mLocalCacheUtils;
    private RamCacheUtils mRamCacheUtils;

    public NetworkCacheUtils(LocalCacheUtils localCacheUtils, RamCacheUtils ramCacheUtils) {
        this.mLocalCacheUtils = localCacheUtils;
        this.mRamCacheUtils = ramCacheUtils;
    }

    /**
     * 从网络加载图片
     * @param url
     * @return
     */
    public Bitmap loadBitmapFromNetwork(String url) {
        try {
            return new LoadBitmapTask().execute(url).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private class LoadBitmapTask extends AsyncTask<String, Void, Bitmap> {

        private String loadUrl;

        @Override
        protected Bitmap doInBackground(String... strings) {
            loadUrl = strings[0];
            try {
                URL url = new URL(loadUrl);
                HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                httpURLConnection.setConnectTimeout(10000);
                httpURLConnection.setReadTimeout(10000);
                httpURLConnection.setRequestMethod("GET");
                if (httpURLConnection.getResponseCode() == 200) {
                    return BitmapFactory.decodeStream(httpURLConnection.getInputStream());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (bitmap != null) {
                //从网络获取图片后,保存至本地缓存
                mLocalCacheUtils.setBitmapToLocal(loadUrl, bitmap);

                //从网络获取图片后,保存至内存
                mRamCacheUtils.setBitmapToMemory(loadUrl, bitmap);
            }
        }
    }
}

2、本地缓存

初次从网络获取图片后,将其保存至本地缓存。在保存的时候,我们一般会对图片的url进行MD5加密处理,以此处理后的字符串作为图片名称来进行保存。另外,在创建缓存地址的时候,我们需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况。

public class LocalCacheUtils {

    private static final String LOCAL_CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/ImageCache";
    private File cacheDir;

    public LocalCacheUtils(Context context) {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
                || !Environment.isExternalStorageRemovable()) {
            cacheDir = new File(LOCAL_CACHE_PATH);
        } else {
            cacheDir = context.getCacheDir();
        }
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
    }

    public Bitmap loadBitmapFromLocal(String url) {
        try {
            //把图片的url当做文件名,并进行MD5加密
            String fileName = MD5(url);
            File file = new File(cacheDir, fileName);
            return BitmapFactory.decodeStream(new FileInputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从本地读取图片
     *
     * @param url
     * @param bitmap
     */
    public void setBitmapToLocal(String url, Bitmap bitmap) {
        try {
            //把图片的url当做文件名,并进行MD5加密
            String fileName = MD5(url);
            File file = new File(cacheDir, fileName);

            //把图片保存至本地
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

        /**
         * 往本地中保存图片
         * @param content
         * @return
         */

    private String MD5(String content) {
        byte[] hash;
        try {
            hash = MessageDigest.getInstance("MD5").digest(content.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("NoSuchAlgorithmException", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UnsupportedEncodingException", e);
        }

        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) {
                hex.append("0");
            }
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }
}

3、内存缓存

(1)LRU (Least Recently Used),最近最少使用算法

  • 基本思想: 如果数据最近被访问过, 那么将来被访问的机率也很高, 那么这部分数据不应该被淘汰。
  • 实现思路:按照正常的思路, 内存(缓存)的空间有限, 我们假定只能放 N 个数据, 那么当空间满了的时候, 根据 LRU 的定义, 我们就应该去淘汰最近最少使用的数据。同时这个 Cache 应该是高速的, 也就是说我们必须保证缓存读取的 get,set 的时间复杂度都是 O(1), 这里用 HashTable 很容易实现。那么淘汰数据呢? HashTable 删除数据的时间复杂度是 O(n), 显然是不可行的, 那么我们可以使用一个容量为 N 的双向链表, 每次 get 数据的时候, 把该条记录移动到 head, 淘汰数据只需要删除链表的 tail 即可。
  • Java直接用 LinkedHashMap 实现即可, LinkedHashMap 底层用的正是双向链表 + HashMap
public class LRUCache {

  private int capacity;
  private Map<Integer, Integer> cache;

  public LRUCache(int capacity) {
    this.capacity = capacity;
    this.cache = new LinkedHashMap<Integer, Integer> (capacity, 0.75f, true) {
      @Override
      protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
      }
    };
  }

  public int getCapacity() {
    return capacity;
  }

  public int getCache(int key) {
    if (cache.containsKey(key)) {
      return cache.get(key);
    } else {
      return -1;
    }
  }

  public void setCache(int key, int value) {
    cache.put(key, value);
  }

  public void printLinkedHashMap() {
    cache.forEach((k, v) -> System.out.println(String.format("key: %d, value: %d", k, v)));
    System.out.println("-------");
  }

  public static void main(String[] args) {
    LRUCache lruCache = new LRUCache(5);
    lruCache.setCache(1, 100);
    lruCache.setCache(2, 200);
    lruCache.setCache(3, 300);
    lruCache.setCache(4, 400);
    lruCache.setCache(5, 500);
    lruCache.printLinkedHashMap();

    lruCache.getCache(1);
    lruCache.printLinkedHashMap();

    lruCache.getCache(3);
    lruCache.printLinkedHashMap();

    lruCache.setCache(6, 600);
    lruCache.printLinkedHashMap();
  }
}

(2)通过LruCache,实现内存缓存

public class RamCacheUtils {

    private LruCache<String, Bitmap> mRamCache;

    public RamCacheUtils() {
        //得到最大允许内存的1/8,即超过指定内存,则开始回收
        long maxMemory = Runtime.getRuntime().maxMemory();

        //指定mRamCache最大允许内存的1/8,即超过指定内存,则开始回收
        mRamCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }

    /**
     * 从内存中读图片
     * @param url
     * @return
     */
    public Bitmap loadBitmapFromRam(String url) {
        return mRamCache.get(url);
    }

    /**
     * 往内存中保存图片
     * @param url
     * @return
     */
    public void setBitmapToMemory(String url, Bitmap bitmap) {
        mRamCache.put(url, bitmap);
    }
}

五、自定义图片缓存工具类

public class ImageLoaderUtils {

    private static ImageLoaderUtils INSTANCE;
    private RamCacheUtils mRamCacheUtils;
    private LocalCacheUtils mLocalCacheUtils;
    private NetworkCacheUtils mNetworkCacheUtils;
    private Context mContext;

    private ImageLoaderUtils(Context context) {
        mContext = context.getApplicationContext();
        mRamCacheUtils = new RamCacheUtils();
        mLocalCacheUtils = new LocalCacheUtils(mContext);
        mNetworkCacheUtils = new NetworkCacheUtils(mLocalCacheUtils, mRamCacheUtils);
    }

    public static ImageLoaderUtils getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = new ImageLoaderUtils(context);
        }
        return INSTANCE;
    }

    public Bitmap load(String url) {
        //内存缓存
        if(mRamCacheUtils.loadBitmapFromRam(url) != null){
            Log.e("ImageLoaderUtils", "**** 从内存获取图片 ****");
            return mRamCacheUtils.loadBitmapFromRam(url);
        }

        //本地缓存
        if(mLocalCacheUtils.loadBitmapFromLocal(url) != null){
            Log.e("ImageLoaderUtils", "**** 从本地获取图片 ****");
            return mLocalCacheUtils.loadBitmapFromLocal(url);
        }

        //网络缓存
        if(mNetworkCacheUtils.loadBitmapFromNetwork(url) != null){
            Log.e("ImageLoaderUtils", "**** 从网络获取图片 ****");
            return mNetworkCacheUtils.loadBitmapFromNetwork(url);
        }
        return null;
    }
}

我们在加载图片的时候,直接调用工具类ImageLoaderUtils的load(String url)方法即可,大概可以直接像这样调用:

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,971评论 25 707
  • 【Android 库 Glide】 引用 Android图片加载框架最全解析(一),Glide的基本用法Andro...
    Rtia阅读 5,418评论 0 22
  • 什么是三级缓存? 内存缓存,优先加载,速度最快 本地缓存,次优先加载,速度快 网络缓存,最后加载,速度慢,浪费流量...
    白帽子耗子阅读 4,288评论 2 3
  • This is a notebook application that records all the big a...
    Blue_936d阅读 164评论 0 0
  • 最近大家都在支付宝扫福吗?表示我已经集成了哟~方法就是一个手机百度福字图片,一个手机一直扫,短短几分钟就搞定啦! ...
    谭银河阅读 191评论 0 0