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