Android中实现图片的三级缓存

一、简介

Android中实现图片的三级缓存_第1张图片
从网络加载图片.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 {

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

  public LRUCache(int capacity) {
    this.capacity = capacity;
    this.cache = new LinkedHashMap (capacity, 0.75f, true) {
      @Override
      protected boolean removeEldestEntry(Map.Entry 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 mRamCache;

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

        //指定mRamCache最大允许内存的1/8,即超过指定内存,则开始回收
        mRamCache = new LruCache((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));
    }

你可能感兴趣的:(Android中实现图片的三级缓存)