Bitmap的加载与缓存
Android系统中图片一般用Bitmap对象表示,它支持png,jpg等常见格式。通常情况下图片的体积都比较大,单个应用允许使用的内存又是有限的,所以我们需要采取一些手段减少内存占用并提高加载速度。
1、图片加载
SDK提供了BitmapFactory类供我们加载图片,常用的方法有这么几个:
- BitmapFactory.decodeFile :从文件加载。
- BitmapFactory.decodeByteArray :从字节数组加载。
- BitmapFactory.decodeStream :从输入流加载。
- BitmapFactory.decodeResource :从资源文件加载。
假设我们用ImageView显示图片,通常它的尺寸要比图片的尺寸小很多,那么把图片整个加载进内存显然是没有必要的。在图形学上有个名词叫“下采样”,作用就是降低图像的分辨率,使其符合显示区域的大小。通过BitmapFactory.Options类,我们也可以实现同样的功能。这里主要用到了它的 inSampleSize 参数,如果它的值是1,那么采样后的图片跟原图一致,如果是2,那么采样后的图片长和宽都是原来的一半,占用的内存也就是原来的四分之一。
public static Bitmap decodeSampleBitmapFromBytes(byte[] data) { final BitmapFactory.Options options = new BitmapFactory.Options(); // inJustDecodeBounds为true时仅解析图片原始信息,并不会真正加载图片。 options.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, options); // 此时图片的宽高可以通过options.outWidth和options.outHeight获取到,我们 // 可以根据自己的需求计算出采样比。 options.inSampleSize = 1; // inJustDecodeBounds设置为fales,加载图片到内存中。 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
2、图片缓存
缓存在计算机领域使用非常广泛,如HTTP缓存,DNS缓存等等,缓存既可以提高响应速度,又能节省服务器带宽,在图片加载上它同样适用。Android开发中一般会对图片做两级缓存:内存缓存和文件缓存,而且它们都有库供我们使用,分别是LruCache和DiskLruCache。从名字就可以看出两者都使用了LRU算法,即优先淘汰那些近期最少使用的缓存。
2.1、LruCache
LruCache是Android提供的一个缓存类,一般用来管理内存缓存。
// #1:确定缓存大小。 int maxMemory = (int)(Runtime.getRuntime().totalMemory() / 1024); int cacheSize = maxMemory / 8; // #2:重写sizeOf方法计算每个缓存对象的内存占用。 LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } };
LruCache是一个泛型类可以容纳各种对象,因而它无法计算被储存对象的大小,所以我们需要重写它的 sizeOf 方法,手动进行计算。那LruCache是如何实现的呢,实际上它仅仅是对LinkedHashMap进行了封装并处理了线程安全问题。LinkedHashMap的构造函数中有一个布尔类型的参数, accessOrder ,当它为 true 时元素按访问顺序存储,为 false 时按插入顺序存储。当元素按访问顺序存储时在其尾部取出的元素也就是最近最少使用的元素,也就实现了LRU算法。LruCache只需要每次 put 函数被调用后计算当前总缓存的大小,当其超出门限值时移除位于LinkedHashMap尾部的元素即可。
2.2、DiskLruCache
DiskLruCache同LruCache一样都使用LinkedHashMap实现LRU算法,但DiskLruCache在实现和使用上更复杂一些,毕竟需要对文件进行管理。
获得DiskLruCache对象需要调用 DiskLruCache.open 函数:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
它接收4个参数,第一个是缓存区目录,第二个是客户端版本号,DiskLruCache认为当版本号发生变化时缓存是无效的,第三个参数代表每个键可以关联几个文件,最后一个参数指定的缓存区的大小。在创建对象时,DiskLruCache会根据缓冲区目录下名为“journal”的日志文件在LinkedHashMap中为缓存文件建立索引,所有对缓冲区的操作都会被记录在这个文件中。当缓冲区大小到达门限值后根据LRU算法对文件进行清理。
读取缓存时使用 DiskLruCache.get 函数:
public synchronized Snapshot get(String key) throws IOException
函数返回一个Snapshot对象,通过该对象我们可以获取到缓存文件的输入流,多个线程可以同时使用各自的SnapShot对象读取同一个Key对应的缓存。
操作缓存时使用 DiskLruCache.edit 函数:
public Editor edit(String key) throws IOException
创建或更改完毕后用 Editor.commit 函数提交或用 Editor.abort 函数取消。一个Key对应的缓存被操作时仍可以使用Snapshot对象读取其内容,因为Editor的所有操作都会先作用于临时文件。注意每个Key只能同时获取一个Editor对象,也就是说即使Editor没有做任何操作也要调用 Editor.abort 或 Editor.commit 函数,不然再次获取时函数返回 null 。
2.3、代码示例
public Bitmap loadBitmap(String url) { // DiskLruCache要求键中不能含有特殊字符,所以 // 一般先做哈希处理。 String key = MD5(url); Bitmap bitmap = loadBitmapFromMemCache(key); if (bitmap != null) { return bitmap; } try { bitmap = loadBitmapFromDiskCache(key); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromHttp(url); if (bitmap != null) { return bitmap; } } catch (IOException e) { e.printStackTrace(); } return null; }
在 loadBitmapFromHttp 函数中需要将图片资源放入DiskLruCache中,在 loadBitmapFromDiskCache 函数中将加载后的Bitmap对象放入LruCache中,如此便形成了一条缓存链。