• Bitmap之内存缓存和磁盘缓存详解


    原文首发于微信公众号:躬行之(jzman-blog)

    Android 中缓存的使用比较普遍,使用相应的缓存策略可以减少流量的消耗,也可以在一定程度上提高应用的性能,如加载网络图片的情况,不应该每次都从网络上加载图片,应该将其缓存到内存和磁盘中,下次直接从内存或磁盘中获取,缓存策略一般使用 LRU(Least Recently Used) 算法,即最近最少使用算法,下面将从内存缓存和磁盘缓存两个方面以图片为例 介绍 Android 中如何使用缓存,阅读本文之前,请先阅读上篇文章:

    内存缓存

    LruCache 是 Android 3.1 提供的一个缓存类,通过该类可以快速访问缓存的 Bitmap 对象,内部采用一个 LinkedHashMap 以强引用的方式存储需要缓存的 Bitmap 对象,当缓存超过指定的大小之前释放最近很少使用的对象所占用的内存。

    注意:Android 3.1 之前,一个常用的内存缓存是一个 SoftReference 或 WeakReference 的位图缓存,现在已经不推荐使用了。Android 3.1 之后,垃圾回收器更加注重回收 SoftWeakference/WeakReference,这使得使用该种方式实现缓存很大程度上无效,使用 support-v4 兼容包中的 LruCache 可以兼容 Android 3.1 之前的版本。

    LruCache 的使用

    1. 初始化 LruCache

    首先计算需要的缓存大小,具体如下:

    //第一种方式:
    ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    //获取当前硬件条件下应用所占的大致内存大小,单位为M
    int memorySize = manager.getMemoryClass();//M
    int cacheSize = memorySize/ 8;
    //第二种方式(比较常用)
    int memorySize = (int) Runtime.getRuntime().maxMemory();//bytes
    int cacheSize = memorySize / 8;
    
    

    然后,初始化 LruCache ,具体如下:

    //初始化 LruCache 且设置了缓存大小
    LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            //计算每一个缓存Bitmap的所占内存的大小,内存单位应该和 cacheSize 的单位保持一致
            return value.getByteCount();
        }
    };
    
    1. 添加 Bitmap 对象到 LruCache 缓存中
    //参数put(String key,Bitmap bitmap)
    lruCache.put(key,bitmap)
    
    1. 获取缓存中的图片并显示
    //参数get(String key)
    Bitmap bitmap = lruCache.get(key);
    imageView.setImageBitmap(bitmap);
    

    下面使用 LruCache 加载一张网络图片来演示 LruCache 的简单使用。

    加载网络图片

    创建一个简单的 ImageLoader,里面封装获取缓存 Bitmap 、添加 Bitmap 到缓存中以及从缓存中移出 Bitmap 的方法,具体如下:

    //ImageLoader
    public class ImageLoader {
        private LruCache<String , Bitmap> lruCache;
        public ImageLoader() {
            int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024;
    
            int cacheSize = memorySize / 8;
            lruCache = new LruCache<String, Bitmap>(cacheSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //计算每一个缓存Bitmap的所占内存的大小
                    return value.getByteCount()/1024;
                }
            };
        }
    
        /**
         * 添加Bitmapd到LruCache中
         * @param key
         * @param bitmap
         */
        public void addBitmapToLruCache(String key, Bitmap bitmap){
            if (getBitmapFromLruCache(key)==null){
                lruCache.put(key,bitmap);
            }
        }
    
        /**
         * 获取缓存的Bitmap
         * @param key
         */
        public Bitmap getBitmapFromLruCache(String key){
            if (key!=null){
                return lruCache.get(key);
            }
            return null;
        }
    
        /**
         * 移出缓存
         * @param key
         */
        public void removeBitmapFromLruCache(String key){
            if (key!=null){
                lruCache.remove(key);
            }
        }
    }
    

    然后创建一个线程类用于加载图片,具体如下:

    //加载图片的线程
    public class LoadImageThread extends Thread {
        private Activity mActivity;
        private String mImageUrl;
        private ImageLoader mImageLoader;
        private ImageView mImageView;
    
        public LoadImageThread(Activity activity,ImageLoader imageLoader, ImageView imageView,String imageUrl) {
            this.mActivity = activity;
            this.mImageLoader = imageLoader;
            this.mImageView = imageView;
            this.mImageUrl = imageUrl;
        }
    
        @Override
        public void run() {
            HttpURLConnection connection = null;
            InputStream is = null;
            try {
                URL url = new URL(mImageUrl);
                connection = (HttpURLConnection) url.openConnection();
                is = connection.getInputStream();
                if (connection.getResponseCode() == HttpURLConnection.HTTP_OK){
                    final Bitmap bitmap = BitmapFactory.decodeStream(is);
                    mImageLoader.addBitmapToLruCache("bitmap",bitmap);
                    mActivity.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mImageView.setImageBitmap(bitmap);
                        }
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (connection!=null){
                    connection.disconnect();
                }
                if (is!=null){
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    

    然后,在 MainActivity 中使用 ImageLoader 加载并缓存网络图片到内存中, 先从内存中获取,如果缓存中没有需要的 Bitmap ,则从网络上获取图片并添加到缓存中,使用过程中一旦退出应用,系统将会释放内存,关键方法如下:

    //获取图片
    private void loadImage(){
        Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap");
       if (bitmap==null){
           Log.i(TAG,"从网络获取图片");
           new LoadImageThread(this,imageLoader,imageView,url).start();
       }else{
           Log.i(TAG,"从缓存中获取图片");
           imageView.setImageBitmap(bitmap);
       }
    }
    
    // 移出缓存
    private void removeBitmapFromL(String key){
        imageLoader.removeBitmapFromLruCache(key);
    }
    
    

    然后在相应的事件里调用上述获取图片、移出缓存的方法,具体如下:

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btnLoadLruCache:
                loadImage();
                break;
            case R.id.btnRemoveBitmapL:
                removeBitmapFromL("bitmap");
                break;
        }
    }
    

    下面来一张日志截图说明执行情况:
    BitmapLruCache

    磁盘缓存

    磁盘缓存就是指将缓存对象写入文件系统,使用磁盘缓存可有助于在内存缓存不可用时缩短加载时间,从磁盘缓存中获取图片相较从缓存中获取较慢,如果可以应该在后台线程中处理;磁盘缓存使用到一个 DiskLruCache 类来实现磁盘缓存,DiskLruCache 收到了 Google 官方的推荐使用,DiskLruCache 不属于 Android SDK 中的一部分,首先贴一个 DiskLruCache 的源码链接
    DiskLruCache 源码地址

    DiskLruCache 的创建

    DiskLruCache 的构造方法是私有的,故不能用来创建 DiskLruCache,它提供一个 open 方法用于创建自身,方法如下:

    /**
     * 返回相应目录中的缓存,如果不存在则创建
     * @param directory 缓存目录
     * @param appVersion 表示应用的版本号,一般设为1
     * @param valueCount 每个Key所对应的Value的数量,一般设为1
     * @param maxSize 缓存大小
     * @throws IOException if reading or writing the cache directory fails
     */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        ...
        // 创建DiskLruCache
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            ...
            return cache;
        }
        //如果缓存目录不存在,创建缓存目录以及DiskLruCache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        ...
        return cache;
    }
    

    注意:缓存目录可以选择 SD 卡上的缓存目录,及 /sdcard/Android/data/应用包名/cache 目录,也可以选择当前应用程序 data 下的缓存目录,当然可以指定其他目录,如果应用卸载后希望删除缓存文件,就选择 SD 卡上的缓存目录,如果希望保留数据请选择其他目录,还有一点,如果是内存缓存,退出应用之后缓存将会被清除。

    DiskLruCache 缓存的添加

    DiskLruCache 缓存的添加是通过 Editor 完成的,Editor 表示一个缓存对象的编辑对象,可以通过其 edit(String key) 方法来获取对应的 Editor 对象,如果 Editor 正在使用 edit(String key) 方法将会返回 null,即 DiskLruCache 不允许同时操作同一个缓存对象。当然缓存的添加都是通过唯一的 key 来进行添加操作的,那么什么作为 key 比较方便吗,以图片为例,一般讲 url 的 MD5 值作为 key ,计算方式如下:

    //计算url的MD5值作为key
    private String hashKeyForDisk(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }
    
    private String bytesToHexString(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 的 MD5 的值获取到 key 之后,就可以通过 DiskLruCache 对象的 edit(String key) 方法获取 Editor 对象,然后通过 Editor 对象的 commit 方法,大概意思就是释放 Editir 对象,之后就可以通过 key 进行其他操作咯。

    当然,获取到 key 之后就可以向 DiskLruCache 中添加要缓存的东西咯,要加载一个网络图片到缓存中,显然就是的通过下载的方式将要缓存的东西写入文件系统中,那么就需要一个输出流往里面写东西,主要有两种处理方式:

    1. 创建 OutputStream 写入要缓存的数据,通过 DiskLruCache 的 edit(String key) 方法获得 Editor 对象,然后通过 OutputStream 转换为 Birmap,将该 Bitmap 写入由 Editor 对象创建的 OutputStream 中,最后调用 Editor 对象的 commit 方法提交;
    2. 先获得 Editor 对象,根据 Editor 对象创建出 OutputStream 直接写入要缓存的数据,最后调用 Editor 对象的 commit 方法提交;

    这里以第一种方式为例,将根据 url 将网络图片添加到磁盘缓存中,同时也添加到内存缓存中,具体如下:

    //添加网络图片到内存缓存和磁盘缓存
    public void putCache(final String url, final CallBack callBack){
        Log.i(TAG,"putCache...");
        new AsyncTask<String,Void,Bitmap>(){
            @Override
            protected Bitmap doInBackground(String... params) {
                String key = hashKeyForDisk(params[0]);
                DiskLruCache.Editor editor = null;
                Bitmap bitmap = null;
                try {
                    URL url = new URL(params[0]);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setReadTimeout(1000 * 30);
                    conn.setConnectTimeout(1000 * 30);
                    ByteArrayOutputStream baos = null;
                    if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){
                        BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
                        baos = new ByteArrayOutputStream();
                        byte[] bytes = new byte[1024];
                        int len = -1;
                        while((len=bis.read(bytes))!=-1){
                            baos.write(bytes,0,len);
                        }
                        bis.close();
                        baos.close();
                        conn.disconnect();
                    }
                    if (baos!=null){
                        bitmap = decodeSampledBitmapFromStream(baos.toByteArray(),300,200);
                        addBitmapToCache(params[0],bitmap);//添加到内存缓存
                        editor = diskLruCache.edit(key);
                        //关键
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
                        editor.commit();//提交
                    }
                } catch (IOException e) {
                    try {
                        editor.abort();//放弃写入
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }
                return bitmap;
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                callBack.response(bitmap);
            }
        }.execute(url);
    }
    

    DiskLruCache 缓存的获取

    在 DiskLruCache 缓存的添加中了解了如何获取 key,获取到 key 之后,通过 DiskLruCache 对象的 get 方法获得 Snapshot 对象,然后根据 Snapshot 对象获得 InputStream,最后通过 InputStream 就可以获得 Bitmap ,当然可以利用 上篇文章 中的对 Bitmap 采样的方式进行适当的调整,也可以在缓存之前先压缩再缓存,获取 InputStream 的方法具体如下:

    //获取磁盘缓存
    public InputStream getDiskCache(String url) {
        Log.i(TAG,"getDiskCache...");
        String key = hashKeyForDisk(url);
        try {
            DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
            if (snapshot!=null){
                return snapshot.getInputStream(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    

    DiskLruCache 的主要部分大致如上,下面实现一个简单的三级缓存来说明 LruCache 和 DiskLruCache 的具体使用,MainActivity 代码如下:

    //MainActivity.java
    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "cache_test";
        public static String CACHE_DIR = "diskCache";  //缓存目录
        public static int CACHE_SIZE = 1024 * 1024 * 10; //缓存大小
        private ImageView imageView;
        private LruCache<String, String> lruCache;
        private LruCacheUtils cacheUtils;
        private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            imageView = (ImageView) findViewById(R.id.imageView);
        }
        
        @Override
        protected void onResume() {
            super.onResume();
            cacheUtils = LruCacheUtils.getInstance();
            //创建内存缓存和磁盘缓存
            cacheUtils.createCache(this,CACHE_DIR,CACHE_SIZE);
        }
        
        @Override
        protected void onPause() {
            super.onPause();
            cacheUtils.flush();
        }
    
        @Override
        protected void onStop() {
            super.onStop();
            cacheUtils.close();
        }
        
        public void loadImage(View view){
            load(url,imageView);
        }
        
        public void removeLruCache(View view){
            Log.i(TAG, "移出内存缓存...");
            cacheUtils.removeLruCache(url);
        }
        
        public void removeDiskLruCache(View view){
            Log.i(TAG, "移出磁盘缓存...");
            cacheUtils.removeDiskLruCache(url);
        }
        
        private void load(String url, final ImageView imageView){
            //从内存中获取图片
            Bitmap bitmap = cacheUtils.getBitmapFromCache(url);
            if (bitmap == null){
                //从磁盘中获取图片
                InputStream is = cacheUtils.getDiskCache(url);
                if (is == null){
                    //从网络上获取图片
                    cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() {
                        @Override
                        public void response(Bitmap bitmap1) {
                            Log.i(TAG, "从网络中获取图片...");
                            Log.i(TAG, "正在从网络中下载图片...");
                            imageView.setImageBitmap(bitmap1);
                            Log.i(TAG, "从网络中获取图片成功...");
                        }
                    });
                }else{
                    Log.i(TAG, "从磁盘中获取图片...");
                    bitmap = BitmapFactory.decodeStream(is);
                    imageView.setImageBitmap(bitmap);
                }
            }else{
                Log.i(TAG, "从内存中获取图片...");
                imageView.setImageBitmap(bitmap);
            }
        }
    }
    
    

    布局文件比较简单就不贴代码了,下面是日志运行截图说明执行情况,如下图所示:

    BitmapDiskLruCache

    这篇文章记录了 LruCache 和 DiskLruCache 的基本使用方式,至少应该对这两个缓存辅助类有了一定的了解,它的具体实现请参考源码。
    【文中代码】:传送门

    可以关注公众号:jzman-blog,一起交流学习。
    躬行之

  • 相关阅读:
    冒泡排序及优化
    Map的三种遍历
    抽象类以及接口的异同
    安卓仿制新浪微博(一)之OAuth2授权接口
    安卓handler.post问题
    Git——版本控制器概述
    Linux概述及简单命令
    JBoss7配置-支持IPv4和IPv6双栈环境
    作用域public,private,protected,以及不写时的区别
    UML类图画法及类之间几种关系
  • 原文地址:https://www.cnblogs.com/jzmanu/p/12688906.html
Copyright © 2020-2023  润新知