Android 缓存策略LruCache和DiskLruCache学习

学习资料:

  • Android 开发艺术探索

其实就是完完全全摘抄,读书笔记 : )

LruCacheDiskLruCache是采用了LRU(Least Recently Used)近期最少使用算法的两种缓存。LruCache内存缓存,DiskLruCache存储设备缓存


1.LruCache 内存缓存

LruCache是一个泛型类,内部是一个LinkedHashMap以强引用的方式存储缓存对象,提供了getput方法进行对缓存对象的操作。当缓存满时,移除近期最少使用的缓存对象,添加新的缓存对象

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时,会被gc回收
  • 弱引用:当一个对象只有弱引用存在时,该对象会随时被gc回收

LruCache是线程安全的


LruCache典型的初始化,重写sizeOf()方法

int MaxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);// kB
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
    @Override
    protected int sizeOf(String key,Bitmap bitmap){
        //bitmap.getByteCount() = bitmap.getRowBytes() * bitmap.getHeight(); 
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;// KB
    } 
}

计算缓存对象大小的单位和总容量的单位要保持一致

一些特殊时候,还需要重写entryRemoved()方法。LruCache移除旧缓存对象时会调用这个方法,根据需求可以在这个方法中完成一些资源回收工作

获取一个缓存对象,mMemoryCache.get(key)
添加一个缓存对象,mMemoryCache.put(key,bitmap)


2.DiskLruCache 磁盘缓存

DiskLruCache并不是Android SDK中的类。不明白为啥,官方只进行推荐,为何不加入SDK

2.1创建

private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;// 50MB

File diskCacheDir = new File(mContext,"bitmap");
if(!diskCacheDir.exists()){
   diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
  • diskCacheDir 缓存文件夹,具体指sdcard/Android/data/package_name/cache
  • 1 应用版本号,一般写为1
  • 1 单个节点所对应的数据的个数,一般写1
  • DISK_CACHE_SIZE 缓存大小

2.2添加

DishLruCache缓存添加的操作通过Eidtor完成,Editor为一个缓存对象的编辑对象。

首先需要获取图片的url所对应的key,根据key利用edit()来获取Editor对象。若此时,这个缓存正在被编辑,edit()会返回nullDiskLruCache不允许同时编辑同一个缓存对象。之所以把url转换成key,因为图片的url中可能存在特殊字符,会影响使用,一般将urlmd5值作为key

private String hashKeyFromUrl(String url){
    String cacheKey;
    try {
        final MessageDigest  mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = byteToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(url.hashCode());
    }
     return  cacheKey;
}

private String byteToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i ++){
          String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六进制字符串
          if (hex.length() == 1){
             sb.append('0');
          }
          sb.append(hex);
    }
    return  sb.toString();
}

url转成key,利用这key值获取Editor对象。若这个keyEditor对象不存在,edit()方法就创建一个新的出来。通过Editor对象可以获取一个输出流对象。DiskLruCacheopen()方法中,一个节点只能有一个数据,edit.newOutputStream(DISK_CACHE_INDEX)参数设置为0

String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor =mDiskLruCache.edit(key);
if (editor != null){
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}

有了这个文件输出流,从网络加载一个图片后,通过这个OutputStream outputStream写入文件系统

private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        try {
            final URL url   = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream(),8 * 1024);
            bos = new BufferedOutputStream(outputStream,8 * 1024);
            int b ;
            while((b = bis.read())!= -1){
                bos.write(b);
            }
            return  true;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (urlConnection != null){
                urlConnection.disconnect();
            }
            closeIn(bis) ;
            closeOut(bos);
        }
        return   false;
}

上面的代码并没有将图片写入文件系统,还需要通过Editor.commit()提交写入操作,若写入失败,调用abort()方法,进行回退整个操作

if (downLoadUrlToStream(url,outputStream)){
    editor.commit();//提交
}else {
    editor.abort();//重复操作
}

这时,图片已经正确写入文件系统,接下来的图片获取就不需要请求网络


2.3 缓存查找

查找过程,也需要将url转换为key,然后通过DiskLruCacheget方法得到一个Snapshot对象,再通过Snapshot对象可得到缓存的文件输入流,有了输入流就可以得到Bitmap对象

为了避免oom,会使用ImageResizer进行缩放。若直接对FileInputStream进行操作,缩放会出现问题。FileInputStream是有序的文件流,两次decodeStream调用会影响文件流的位置属性。可以通过文件流得到其所对应的文件描述符,利用BitmapFactory.decodeFileDescriptor()方法进行缩放

Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null){
    FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
    FileDescriptor fileDescriptor = fis.getFD();
    bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor,targetWidth,targetHeight);
    if (bitmap != null){
        addBitmapToMemoryCache(key,bitmap);
    }
}

在查找得到Bitmap后,把key,bitmap添加到内存缓存中


3.ImageLoader的实现

主要思路:

  1. 拿到图片请求地址url后,先把url变作对应的key
  2. 利用key在内存缓存中查找,查找到了就进行加载显示图片;若没有查到就进行3
  3. 在磁盘缓存中查找,在若查到了,就加载到内存缓存,后加载显示图片;若没有查找到,进行4
  4. 进行网络求,拿到数据图片,把图片写进磁盘缓存成功后,再加入到内存缓存中,并根据实际所需显示大小进行合理缩放显示

类比较长,查看顺序:构造方法->bindBitmap(),之后顺着方法内的方法,结合4个步骤,并不难理解

public class ImageLoader {
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;// 50MB
    private Context mContext;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private boolean mIsDiskLruCacheCreated = false;//用来标记mDiskLruCache是否创建成功
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT+ 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10;
    private final int DISK_CACHE_INDEX = 0;

    private static final int MESSAGE_POST_RESULT = 101;

    private ImageResizer imageResizer = new ImageResizer();

    private static final ThreadFactory mThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
        }
    };
    /**
     * 创建线程池
     */
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),mThreadFactory
    );

    /**
     * 创建Handler
     */
    private Handler mHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MESSAGE_POST_RESULT){
                LoaderResult loadResult = (LoaderResult) msg.obj;
                ImageView iv = loadResult.iv;
                String url = (String) iv.getTag();
                if (url.equals(loadResult.uri)){//防止加载列表形式时,滑动复用的错位
                    iv.setImageBitmap(loadResult.bitmap);
                }
            }
        }
    };

    private ImageLoader(Context mContext) {
        this.mContext = mContext.getApplicationContext();
        init();
    }
   /**
     * 创建一个ImageLoader
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    
   /**
     * 初始化
     * LruCache<String,Bitmap> mMemoryCache
     * DiskLruCache mDiskLruCache
     */
    private void init() {
        // LruCache<String,Bitmap> mMemoryCache
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                 //return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                return bitmap.getByteCount() / 1024;
            }
        };
        // DiskLruCache mDiskLruCache
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

   
    /**
     *  加载原始大小的图
     */
    public  void bindBitmap(String uri,ImageView iv){
        bindBitmap(uri,iv,0,0);
    }

    /**
     * 异步加载网络图片 指定大小
     */
    public void bindBitmap(final String uri, final ImageView iv, final int targetWidth, final int targetHeight){
         iv.setTag(uri);
         Bitmap bitmap = loadBitmapFormMemCache(uri);
        if (bitmap != null){
            iv.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri,targetWidth,targetHeight);
                if (bitmap != null){
                    LoaderResult result = new LoaderResult(iv,uri,bitmap);
                    Message message = mHandler.obtainMessage();
                    message.obj = result;
                    message.what = MESSAGE_POST_RESULT;
                    mHandler.sendMessage(message);
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    /**
     * 同步加载网络图片
     */
    private Bitmap loadBitmap(String url, int targetWidth, int targetHeight) {
        Bitmap bitmap = loadBitmapFormMemCache(url);
        if (bitmap != null) {
            return bitmap;
        }
        try {
            bitmap = loadBitmapFromDiskCache(url, targetWidth, targetHeight);
            if (bitmap != null) {
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url, targetWidth, targetHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated) {//缓存文件夹创建失败
            bitmap = downLoadFromUrl(url);
        }
        return bitmap;
    }
    
    /**
     * 向缓存中添加Bitmap
     */
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 通过key拿到bitmap
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

    private Bitmap loadBitmapFormMemCache(String url) {
        final String key = hashKeyFromUrl(url);
        return getBitmapFromMemoryCache(key);
    }

    /**
     * 从网络进行请求
     */
    private Bitmap loadBitmapFromHttp(String url, int targetWidth, int targetHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("UI 线程不能进行网络访问");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        String key = hashKeyFromUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downLoadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();//重复操作
            }
            mDiskLruCache.flush();//刷新
        }
        return loadBitmapFromDiskCache(url, targetWidth, targetHeight);
    }

    /**
     * 从硬盘缓存中读取Bitmap
     */
    private Bitmap loadBitmapFromDiskCache(String url, int targetWidth, int targetHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("硬盘读取Bitmap在UI线程,UI 线程不进行耗时操作");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFromUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null) {
            FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fis.getFD();
            bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor, targetWidth, targetHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

    /**
     * 将数据请求到流之中
     */
    private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            bos = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            closeIn(bis);
            closeOut(bos);
        }
        return false;
    }

    /**
     * 直接通过网络请求图片 也不做任何的缩放处理
     */
    private Bitmap downLoadFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream bis = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream());
            bitmap = BitmapFactory.decodeStream(bis);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            closeIn(bis);
        }
        return bitmap;
    }


    /**
     * 得到MD5值key
     */
    private String hashKeyFromUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = byteToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    /**
     * 将byte转换成16进制字符串
     */
    private String byteToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六进制字符串
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }


    /**
     * 得到缓存文件夹
     */
    private File getDiskCacheDir(Context mContext, String uniqueName) {
        //判断储存卡是否可以用
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = mContext.getExternalCacheDir().getPath();//储存卡
        } else {
            cachePath = mContext.getCacheDir().getPath();//手机自身内存
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 得到可用空间大小
     */
    private long getUsableSpace(File file) {
        return file.getUsableSpace();
    }

    /**
     * 关闭输入流
     */
    private void closeIn(BufferedInputStream in) {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                in = null;
            }
        }
    }

    /**
     * 关闭输输出流
     */
    private void closeOut(BufferedOutputStream out) {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                out = null;
            }
        }
    }

    private static class LoaderResult {
        public ImageView iv ;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView iv, String uri, Bitmap bitmap) {
            this.iv = iv;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}

经过测试,是可以正常加载出图片的,缓存文件也正常,主要是学习过程思路


3.1补充 Closeable接口

ImageLoader中,代码最后写了closeIn(),closeOut()方法,完全没必要这样写。在jdk1.6之后,所有的流都实现了Closeable接口,可以用这个接口来关闭所有的流

/**
 * 关闭流
 */
 private void closeStream(Closeable closeable){
    if (closeable != null){
        try {
            closeable.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

downLoadFromUrl()downLoadUrlToStream()方法中需要关闭流,就可以直接closeStream(bis),closeStream(bos)来进行关闭流操作


4.最后

这个ImgageLoader还非常简陋,和GlidePicasso根本无法相比。并不会在实际工作开发中使用,还是使用Glide或者Picasso。主要是学习基础实现原理,学习了下Android中缓存的部分知识。之前面试时,被问到过ImageLoader原理。

最近的学习,感觉不会的东西太多了,得理一理学习的顺序。

注意多锻炼身体。 共勉:)

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

推荐阅读更多精彩内容