Bitmap之内存缓存与磁盘缓存详解

作者: 迷途小码农h | 来源:发表于2019-04-14 10:39 被阅读86次

    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 的使用

    加载网络图片

    LruCache 的使用

    初始化 LruCache

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

    1//第一种方式:

    2ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);

    3//获取当前硬件条件下应用所占的大致内存大小,单位为M

    4int memorySize = manager.getMemoryClass();//M

    5int cacheSize = memorySize/ 8;

    6//第二种方式(比较常用)

    7int memorySize = (int) Runtime.getRuntime().maxMemory();//bytes

    8int cacheSize = memorySize / 8;

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

    1//初始化 LruCache 且设置了缓存大小

    2LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){

    3    @Override

    4    protected int sizeOf(String key, Bitmap value) {

    5        //计算每一个缓存Bitmap的所占内存的大小,内存单位应该和 cacheSize 的单位保持一致

    6        return value.getByteCount();

    7    }

    8};

    添加 Bitmap 对象到 LruCache 缓存中

    1//参数put(String key,Bitmap bitmap)

    2lruCache.put(key,bitmap)

    获取缓存中的图片并显示

    1//参数get(String key)

    2Bitmap bitmap = lruCache.get(key);

    3imageView.setImageBitmap(bitmap);

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

    加载网络图片

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

    1//ImageLoader

    2public class ImageLoader {

    3    private LruCache<String , Bitmap> lruCache;

    4    public ImageLoader() {

    5        int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024;

    6

    7        int cacheSize = memorySize / 8;

    8        lruCache = new LruCache<String, Bitmap>(cacheSize){

    9            @Override

    10            protected int sizeOf(String key, Bitmap value) {

    11                //计算每一个缓存Bitmap的所占内存的大小

    12                return value.getByteCount()/1024;

    13            }

    14        };

    15    }

    16

    17    /**

    18    * 添加Bitmapd到LruCache中

    19    * @param key

    20    * @param bitmap

    21    */

    22    public void addBitmapToLruCache(String key, Bitmap bitmap){

    23        if (getBitmapFromLruCache(key)==null){

    24            lruCache.put(key,bitmap);

    25        }

    26    }

    27

    28    /**

    29    * 获取缓存的Bitmap

    30    * @param key

    31    */

    32    public Bitmap getBitmapFromLruCache(String key){

    33        if (key!=null){

    34            return lruCache.get(key);

    35        }

    36        return null;

    37    }

    38

    39    /**

    40    * 移出缓存

    41    * @param key

    42    */

    43    public void removeBitmapFromLruCache(String key){

    44        if (key!=null){

    45            lruCache.remove(key);

    46        }

    47    }

    48}

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

    1//加载图片的线程

    2public class LoadImageThread extends Thread {

    3    private Activity mActivity;

    4    private String mImageUrl;

    5    private ImageLoader mImageLoader;

    6    private ImageView mImageView;

    7

    8    public LoadImageThread(Activity activity,ImageLoader imageLoader, ImageView imageView,String imageUrl) {

    9        this.mActivity = activity;

    10        this.mImageLoader = imageLoader;

    11        this.mImageView = imageView;

    12        this.mImageUrl = imageUrl;

    13    }

    14

    15    @Override

    16    public void run() {

    17        HttpURLConnection connection = null;

    18        InputStream is = null;

    19        try {

    20            URL url = new URL(mImageUrl);

    21            connection = (HttpURLConnection) url.openConnection();

    22            is = connection.getInputStream();

    23            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK){

    24                final Bitmap bitmap = BitmapFactory.decodeStream(is);

    25                mImageLoader.addBitmapToLruCache("bitmap",bitmap);

    26                mActivity.runOnUiThread(new Runnable() {

    27                    @Override

    28                    public void run() {

    29                        mImageView.setImageBitmap(bitmap);

    30                    }

    31                });

    32            }

    33        } catch (IOException e) {

    34            e.printStackTrace();

    35        } finally {

    36            if (connection!=null){

    37                connection.disconnect();

    38            }

    39            if (is!=null){

    40                try {

    41                    is.close();

    42                } catch (IOException e) {

    43                    e.printStackTrace();

    44                }

    45            }

    46        }

    47    }

    48}

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

    1//获取图片

    2private void loadImage(){

    3    Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap");

    4  if (bitmap==null){

    5      Log.i(TAG,"从网络获取图片");

    6      new LoadImageThread(this,imageLoader,imageView,url).start();

    7  }else{

    8      Log.i(TAG,"从缓存中获取图片");

    9      imageView.setImageBitmap(bitmap);

    10  }

    11}

    12

    13// 移出缓存

    14private void removeBitmapFromL(String key){

    15    imageLoader.removeBitmapFromLruCache(key);

    16}

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

    1@Override

    2public void onClick(View v) {

    3    switch (v.getId()){

    4        case R.id.btnLoadLruCache:

    5            loadImage();

    6            break;

    7        case R.id.btnRemoveBitmapL:

    8            removeBitmapFromL("bitmap");

    9            break;

    10    }

    11}

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

    image

    jzman-blog

    磁盘缓存

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

    https://android.googlesource.com/platform/libcore/+/android-4.3_r3/luni/src/main/java/libcore/io/DiskLruCache.java,

    主要内容如下:

    DiskLruCache 的创建

    DiskLruCache 缓存的添加

    DiskLruCache 缓存的获取

    DiskLruCache 的创建

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

    1/**

    2 * 返回相应目录中的缓存,如果不存在则创建

    3 * @param directory 缓存目录

    4 * @param appVersion 表示应用的版本号,一般设为1

    5 * @param valueCount 每个Key所对应的Value的数量,一般设为1

    6 * @param maxSize 缓存大小

    7 * @throws IOException if reading or writing the cache directory fails

    8 */

    9public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

    10        throws IOException {

    11    ...

    12    // 创建DiskLruCache

    13    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);

    14    if (cache.journalFile.exists()) {

    15        ...

    16        return cache;

    17    }

    18    //如果缓存目录不存在,创建缓存目录以及DiskLruCache

    19    directory.mkdirs();

    20    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);

    21    ...

    22    return cache;

    23}

    注意:缓存目录可以选择 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 ,计算方式如下:

    1//计算url的MD5值作为key

    2private String hashKeyForDisk(String url) {

    3    String cacheKey;

    4    try {

    5        final MessageDigest mDigest = MessageDigest.getInstance("MD5");

    6        mDigest.update(url.getBytes());

    7        cacheKey = bytesToHexString(mDigest.digest());

    8    } catch (NoSuchAlgorithmException e) {

    9        cacheKey = String.valueOf(url.hashCode());

    10    }

    11    return cacheKey;

    12}

    13

    14private String bytesToHexString(byte[] bytes) {

    15    StringBuilder sb = new StringBuilder();

    16    for (int i = 0; i < bytes.length; i++) {

    17        String hex = Integer.toHexString(0xFF & bytes[i]);

    18        if (hex.length() == 1) {

    19            sb.append('0');

    20        }

    21        sb.append(hex);

    22    }

    23    return sb.toString();

    24}

    通过 url 的 MD5 的值获取到 key 之后,就可以通过 DiskLruCache 对象的 edit(String key) 方法获取 Editor 对象,然后通过 Editor 对象的 commit 方法,大概意思就是释放 Editir 对象,之后就可以通过 key 进行其他操作咯。

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

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

    先获得 Editor 对象,根据 Editor 对象创建出 OutputStream 直接写入要缓存的数据,最后调用 Editor 对象的  commit 方法提交;

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

    1//添加网络图片到内存缓存和磁盘缓存

    2public void putCache(final String url, final CallBack callBack){

    3    Log.i(TAG,"putCache...");

    4    new AsyncTask<String,Void,Bitmap>(){

    5        @Override

    6        protected Bitmap doInBackground(String... params) {

    7            String key = hashKeyForDisk(params[0]);

    8            DiskLruCache.Editor editor = null;

    9            Bitmap bitmap = null;

    10            try {

    11                URL url = new URL(params[0]);

    12                HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    13                conn.setReadTimeout(1000 * 30);

    14                conn.setConnectTimeout(1000 * 30);

    15                ByteArrayOutputStream baos = null;

    16                if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){

    17                    BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());

    18                    baos = new ByteArrayOutputStream();

    19                    byte[] bytes = new byte[1024];

    20                    int len = -1;

    21                    while((len=bis.read(bytes))!=-1){

    22                        baos.write(bytes,0,len);

    23                    }

    24                    bis.close();

    25                    baos.close();

    26                    conn.disconnect();

    27                }

    28                if (baos!=null){

    29                    bitmap = decodeSampledBitmapFromStream(baos.toByteArray(),300,200);

    30                    addBitmapToCache(params[0],bitmap);//添加到内存缓存

    31                    editor = diskLruCache.edit(key);

    32                    //关键

    33                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));

    34                    editor.commit();//提交

    35                }

    36            } catch (IOException e) {

    37                try {

    38                    editor.abort();//放弃写入

    39                } catch (IOException e1) {

    40                    e1.printStackTrace();

    41                }

    42            }

    43            return bitmap;

    44        }

    45

    46        @Override

    47        protected void onPostExecute(Bitmap bitmap) {

    48            super.onPostExecute(bitmap);

    49            callBack.response(bitmap);

    50        }

    51    }.execute(url);

    52}

    DiskLruCache 缓存的获取

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

    1//获取磁盘缓存

    2public InputStream getDiskCache(String url) {

    3    Log.i(TAG,"getDiskCache...");

    4    String key = hashKeyForDisk(url);

    5    try {

    6        DiskLruCache.Snapshot snapshot = diskLruCache.get(key);

    7        if (snapshot!=null){

    8            return snapshot.getInputStream(0);

    9        }

    10    } catch (IOException e) {

    11        e.printStackTrace();

    12    }

    13    return null;

    14}

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

    1//MainActivity.java

    2public class MainActivity extends AppCompatActivity {

    3    private static final String TAG = "cache_test";

    4    public static String CACHE_DIR = "diskCache";  //缓存目录

    5    public static int CACHE_SIZE = 1024 * 1024 * 10; //缓存大小

    6    private ImageView imageView;

    7    private LruCache<String, String> lruCache;

    8    private LruCacheUtils cacheUtils;

    9    private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg";

    10    @Override

    11    protected void onCreate(Bundle savedInstanceState) {

    12        super.onCreate(savedInstanceState);

    13        setContentView(R.layout.activity_main);

    14        imageView = (ImageView) findViewById(R.id.imageView);

    15    }

    16

    17    @Override

    18    protected void onResume() {

    19        super.onResume();

    20        cacheUtils = LruCacheUtils.getInstance();

    21        //创建内存缓存和磁盘缓存

    22        cacheUtils.createCache(this,CACHE_DIR,CACHE_SIZE);

    23    }

    24

    25    @Override

    26    protected void onPause() {

    27        super.onPause();

    28        cacheUtils.flush();

    29    }

    30

    31    @Override

    32    protected void onStop() {

    33        super.onStop();

    34        cacheUtils.close();

    35    }

    36

    37    public void loadImage(View view){

    38        load(url,imageView);

    39    }

    40

    41    public void removeLruCache(View view){

    42        Log.i(TAG, "移出内存缓存...");

    43        cacheUtils.removeLruCache(url);

    44    }

    45

    46    public void removeDiskLruCache(View view){

    47        Log.i(TAG, "移出磁盘缓存...");

    48        cacheUtils.removeDiskLruCache(url);

    49    }

    50

    51    private void load(String url, final ImageView imageView){

    52        //从内存中获取图片

    53        Bitmap bitmap = cacheUtils.getBitmapFromCache(url);

    54        if (bitmap == null){

    55            //从磁盘中获取图片

    56            InputStream is = cacheUtils.getDiskCache(url);

    57            if (is == null){

    58                //从网络上获取图片

    59                cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() {

    60                    @Override

    61                    public void response(Bitmap bitmap1) {

    62                        Log.i(TAG, "从网络中获取图片...");

    63                        Log.i(TAG, "正在从网络中下载图片...");

    64                        imageView.setImageBitmap(bitmap1);

    65                        Log.i(TAG, "从网络中获取图片成功...");

    66                    }

    67                });

    68            }else{

    69                Log.i(TAG, "从磁盘中获取图片...");

    70                bitmap = BitmapFactory.decodeStream(is);

    71                imageView.setImageBitmap(bitmap);

    72            }

    73        }else{

    74            Log.i(TAG, "从内存中获取图片...");

    75            imageView.setImageBitmap(bitmap);

    76        }

    77    }

    78}

    布局文件比较简单就不贴代码了

    这篇文章记录了 LruCache 和 DiskLruCache 的基本使用方式,至少应该对这两个缓存辅助类有了一定的了解。

    相关文章

      网友评论

        本文标题:Bitmap之内存缓存与磁盘缓存详解

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