Android 缓存策略LruCache和DiskLruCach

作者: 英勇青铜5 | 来源:发表于2016-09-05 19:18 被阅读5515次

    学习资料:

    • 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原理。

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

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

    相关文章

      网友评论

      • liyanlei666:注意多锻炼身体。 共勉:)
      • 挡不住的柳Willow:请问ImageResizer的代码怎么写
        英勇青铜5:换电脑了,代码没了。。。这个就是个我自己写的很普通的bitmap分辨率压缩的类,你百度下bitmap二次采样,可以自己写一个出来,代码不多
      • 捡淑:马克
        恨自己不能小清新:@英勇青铜5 你不觉得这个层主的头像有点奇怪吗:flushed: http://upload.jianshu.io/users/upload_avatars/1716974/1e37c3f70250.gif
        英勇青铜5: @恨自己不能小清新 ?????啥
        恨自己不能小清新:头像怎么回事:flushed:

      本文标题:Android 缓存策略LruCache和DiskLruCach

      本文链接:https://www.haomeiwen.com/subject/wogrettx.html