[Android]《Android开发艺术探索》学习笔记-Bitmap加载和Cache

一、Bitmap的高效加载

Bitmap在Andrid中指的是一张图片,其中BitmapFactory类提供了四个方法:decodeFile(文件系统)、decodeResource(资源)、decodeStream(输入流)和decodeByteArray(字节数组)来加载一个Bitmap对象,decodeFile和decodeResource也调用了decodeStream,这些都是在Android底层实现的,对应着Native方法。我们可以通过BitmapFactory.Options来加载所需尺寸的图片,因为经常 ImageView需要加载的图片并没有原图那么大,用BitmapFactory.Options进行图片的缩小后再载入ImageView能够降低内存从而一定程度上避免OOM,提高了Bitmap加载的性能。

我们在缩放图片时,用的是BitmapFactory.Options的inSampleSize参数,这个参数表示采样率。inSampleSize为1时表示原始大小,为2时表示宽高为原来的1/2,像素为原来的1/4,占用内存也为原来的1/4。采样率inSampleSize必须是大于1才有效果,并图片大小以采样率的二次方递减,那么就是缩放比例为1/(inSampleSize的二次方)。官方文档指出inSampleSize必须以2的指数,比如1、2、4、8、16,如果传给inSampleSize不为2的指数,它会向下取整并选择最接近2的指数来代替,比如3,系统会选择2来代替(并不适用于所有Android版本)。

实际情况下,通过采样率加载图片也还是要灵活结合原始图片大小来计算inSampleSize的,需要通过以下步骤:

(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;

(2)从BitmapFactory.Options取出图片的原始宽高,对应于outWidth和outHeight参数;

(3)根据采样率规则并结合目标View所需大小来计算采样率inSampleSize;

(4)将BitmapFactory.Options的inJustDecodeBounds参数设为false并重新加载图片。

BitmapFactory.Options的inJustDecodeBounds参数设为true会获取图片的原始宽高信息,并不会加载图片,这个操作是轻量级的,这个时候BitmapFactory获取的宽高是跟图片的位置和设备有关的。这个流程的代码如下(decodeResource为例):

public Bitmap decodeSampledBitmapFromResource(Resources res,
                                                  int resId, int reqWidth, int reqHeight) {
        // 先设置inJustDecodeBounds = true加载图片获取原始宽高
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // 计算采样率
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        // 再次以这个计算好的采样率加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

public int calculateInSampleSize(BitmapFactory.Options options,
                                     int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        // 原始的宽高
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d(TAG, "origin, w= " + width + " h=" + height);
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // 计算最接近2的指数的inSampleSize并保证宽高比请求要的宽高更大
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }

 

二、Android中的缓存策略

1、LruCache

LRU最近最少使用,当缓存满时会优先淘汰那些最近最少使用的缓存对象。使用LruCache时建议使用support-v4提供的兼容包。LruCache是一个泛型,内部采用LinkedHashMap以强引用的方式储存外部缓存对象,其提供了了get和put方法来完成缓存的获取和添加操作。关于强引用、软引用跟弱引用的区别可查看我博客文章:[Java]《深入理解Java虚拟机》学习笔记-垃圾收集器与内存分配策略

LruCache是线程安全的,实现内存缓存的代码如下:

private LruCache<String, Bitmap> memoryCache;
int cacheSize = (int) Runtime.getRuntime().maxMemory() / 8;//缓存应用最大可用内存的1/8
            memoryCache = new LruCache<String, Bitmap>(cacheSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight() / 1024;//定义每一个存储对象的大小
                }
            };

只需要提供缓存的大小以及重写sizeOf就行了。sizeOf的作用是计算缓存对象的大小(单位要和总容量一致),有些情况下还要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用这个方法,因此可以在entryRemoved中做一些资源回收的工作。同时可以通过如下方法获取及添加一个缓存对象:

//获取
memoryCache.get(key);
//添加
memoryCache.put(key,value);

 

2、DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘缓存,用LRU最近最少使用算法,它通过将缓存对象写入文件系统来实现缓存效果,它不属于Android SDK的一部分。

(1)DiskLruCache提供open方法创建自身,方法声明如下:

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

第一个参数表示缓存在文件系统中的存储位置,如果想要跟随应用卸载而删除可保存在/sdcard/Android/data/package_name/cache中,否则可存放在其他位置。第二个参数表示版本号,当版本号改变时将会自动清除缓存。第三个参数表示节点所对应的数据个数,一般为1。第四个参数表示缓存的总大小,当缓存大小超过这个设定值的时候就会清除一些缓存从而保证不超过这个值。

(2)DiskLruCache的缓存添加

DiskLruCache的缓存添加操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。比如从网上下载一张图片缓存,我们可以把图片URL的MD5作为Key(因为URL可能会有不合法的字符),然后缓存代码如下:

                    String imageUrl = "http://oowja04th.bkt.clouddn.com/143133543197.jpg";
                    String key = hashKeyForDisk(imageUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if (editor != null) {
                        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                        if (downloadUrlToStream(imageUrl, outputStream)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    mDiskLruCache.flush();

其中,editor的abort()是用来回退操作的,当图片下载异常可以用来回退缓存操作。

(3)DiskLruCache的缓存查找

可以用key通过DiskLruCache的get方法取得Snapshot对象,再通过Snapshot对象可以获得缓存文件输入流。

        String imageUrl = "http://oowja04th.bkt.clouddn.com/143133543197.jpg";
        String key = hashKeyForDisk(imageUrl);
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            InputStream is = snapShot.getInputStream(DISK_CACHE_INDEX);
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            mImage.setImageBitmap(bitmap);
        }

我们可以通过前面介绍的BitmapFactory.Options来显示合适缩放的图片,但是这会对FireInputStream的缩放出现问题,因为FireInputStream是有序文件流,而两次decodeStream调用影响了文件流的位置属性,导致第二次decodeStream得到的是null。为了解决这个问题我们应该通过文件流获得文件描述符,再通过BitmapFactory.decodeFireDescriptor来加载缩放后的图片。

DiskLruCache还提供了remove、delete等方法进行删除操作。

发表评论

电子邮件地址不会被公开。 必填项已用*标注