Bitmap的高效加载以及缓存机制

本文探讨了在Android开发中如何高效加载Bitmap,通过设置BitmapFactory.Options的inSampleSize参数实现按需加载,以避免内存泄漏和提高性能。同时,介绍了Android的缓存策略,包括内存缓存LruCache和磁盘缓存DiskLruCache的工作原理及使用方法,帮助优化应用的图片加载,提高用户体验。
摘要由CSDN通过智能技术生成

在实际的开发过程中,经常会对图片进行获取,并为用户展示在界面。由于android系统对每个应用有一定的内存限制,如果不合理的利用就会造成内存泄漏的情况。因此,在加载bitmap时进行恰当的优化,可以节省系统资源。另外,在进行网络、文件系统获取图片资源时,采用缓存机制,减少网络获取次数,会让应用更流畅,用户体验更友好。下面就从bitmap的高效加载、LruCache、DiskLruCache这三方面进行阐述:

Bitmap的加载

首先,先了解如何加载bitmap。bitmap是一张png、jpg等多种格式的图片,通过BitmapFactory的decodeFile、decodeResource、decodeStream、decodeByteArray四个方法分别从文件系统,资源,输入流以及字节数组中加载一个bitmap对象。这四类方法最终都在android的底层实现,对应着BitmapFactory的类的几个native方法。
那么如何高效的加载bitmap呢?其核心思想是通过BitmapFactory.Options来加载所需尺寸的图片。例如,当我们通过ImageView来加载图片时,若ImageView的尺寸要小于图片的尺寸,此时将图片按原比例放入imageView中,并不会完全显示。通过BitmapFactory.Options按一定的采样率来缩小图片,将缩小后的图片放入ImageView中显示,这样减少了内存的开销,一定程度上避免了OOM,提高了bitmap加载性能。
通过BitmapFactory.Options来缩放图片,主要用到了inSampleSize参数。当参数的值为1时,采样的大小为原始图片大小;当inSampleSize值大于1时,图片将被缩小,假设inSampleSize = 2,图片的长宽均被缩小为原始比例的1/2,像素并为原来的1/4,因此所在内存同样变为原来的1/4。值得注意的是,只有当inSampleSize的值大于1时,图片才会被缩小,由于长、宽被同时作用,图片总是被缩小为inSampleSize的2次方倍,即inSampleSize = 4,则图片将会被缩小为原比例的1/16。当inSampleSize的值小于1时,无效。实际情况中,假如ImageView的大小为100100像素,图片大小为200200,此时只要将inSampleSize值设为2即可。如果图片为200*300呢?此时inSampleSize的值还是设置为2比较合适,如果设置成3,那么图片尺寸将远小于ImageView,导致图片将被拉升,从而变得模糊。
通过采样率可以高效的加载bitmap,那么如何获取图片的采样率呢?

  1. 首先将BitmapFactory.Options的inJustDecodeBounds参数设置为true;
  2. 从BitmapFactory.Options中取出原始图片的宽高,对应于onWidth、onHeight;
  3. 根据目标view所需大小结合采样率规则,计算出inSampleSize 值;
  4. 将BitmapFactory.Options的inJustDecodeBounds参数设置为false,重新加载图片。
 private Bitmap setBitmapImage(Resources res, int id, int width, int height){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,id,options);

        //获取inSampleSize值
        options.inSampleSize = getinSampleSize(options,width,height);

        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res,id,options);
    }

    private int getinSampleSize(BitmapFactory.Options options, int width, int height) {
        int w = options.outWidth;
        int h = options.outHeight;
        int inSampleSize = 1;
        if (w > width || h > height){
            int halfw = w / 2;
            int halfh = h / 2;
            while ((halfw / inSampleSize) >= width && (halfh / inSampleSize) >= height){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

经过上面的步骤,加载出来的图片就是缩放后的图片,当然也有可能不需要缩放。值得一提的是,当inJustDecodeBounds参数为true时,BitmapFactory只会去解析原始图片的宽高,并不会真正的去加载图片,并且该操作是轻量级的。在使用的时候,假如ImageView的大小为100*100,则:

mImageView.setImageBitmap(setBitmapImage(getResources(),R.id.image,100,100));

上面介绍的BitmapFactory.decodeResource方法,其他三种加载Bitmap方法同样支持采样率的使用。在类似于网络获取图片时,一般会处理InputStream流,但是流只能应用一次,如果二次使用将返回null,处理办法是重写其mark()、reSet()方法,具体实现这里就不展开讲了;另一种方法是现将InputStream通过ByteArrayOutputStream缓存,

public static byte[] readStream(InputStream inStream) throws Exception {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        outStream.close();
        inStream.close();
        return outStream.toByteArray();
    }

这样,再通过BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options)返回bitmap对象。

另外,

public void setImageResource(int resId){
        InputStream is = getContext().getResources().openRawResource(resId);
        Bitmap bmp = BitmapFactory.decodeStream(is);
        //图片重新裁剪,原图从中心点按显示大小裁剪
        int bw = bmp.getWidth(), bh = bmp.getHeight();
        float scaleX = (float) WIDTH / bw;
        float scaleY = (float) WIDTH / bh;
        Matrix matrix = new Matrix();
        matrix.setScale(scaleX,scaleY);
        mBitmap = Bitmap.createBitmap(bmp,0,0,bw,bh,matrix,true);//width 裁剪bitmap的宽度;height 裁剪bitmap的高度
        invalidate();
    }

还可以通过上面的代码,采用Matrix 类来对图片进行缩放。但值得注意的是,这样做并没有改变图片的内存大小,而是三维空间的视觉缩放。

Android缓存策略

在应用的开发过程中,经常会涉及到网络请求,以获取图片、视频等资源。但是对于移动设备来说,数据流量的消耗对用户来说是很关心的,如何为用户减少网络请求,节省流量消耗,显得很有必要。因此,就引入了缓存的概念。以图片为例,当用户第一次网络获取图片后,通过缓存机制将图片存储到存储设备上,下次需要再次用到该图片时,直接从存储设备获取,不需要网络获取。多数情况下,为了提高应用的交互性,通常会将部分图片存储到内存中,再次使用时直接从内存获取,这样速度快于从存储设备、网络中获取。
上面引入了缓存机制,那么具体怎么实现呢?针对缓存机制,有不同的缓存策略,各种策略间并没有统一的标准,但基本包括添加、获取以及删除操作。添加和获取比较好理解,但是为什么还要删除呢?我们都知道,对于手机等移动设备来说,硬件是有限的,特别是存储能力,不可能达到无穷大。因此,在进行缓存时,当缓存容量满时,需要进行旧缓存的删除,以便进行新缓存。如何进行新旧文件的替换,这里就需要缓存算法的支持。目前最常用的缓存算法为近期最少使用算法LRU,核心思想为当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用Lru缓存算法的缓存机制有两种:LruCache和DiskLruCache,LruCache实现内存缓存,DiskLruCache实现储存设备缓存。

  • LruCache
    LruCache是一个泛型类,内部采用LinkedHashMap以强引用的方式存储外部缓存对象,提供get和put方法来实现缓存对象的获取和添加,当缓存满时,LruCache会移除较早使用的缓存对象,加入新的缓存对象。介绍下几个概念:

    1、强引用:直接的对象引用
    2、软引用:当一个对象只有软引用存在时,内存资源不足时,此对象会被gc回收;
    3、 弱引用:当一个对象只有弱引用存在时,此对象随时会被gc回收;

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

LruCache是线程安全的,源码也比较简单。下面介绍其基本使用:

        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int memory = maxMemory / 8;
        mImageCache = new LruCache<String, Bitmap>(memory){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight() / 1024;
            }
        };

上面的代码,先计算出当前进程的可用内存,设置缓存大小为可用内存大小的八分之一,通过sizeOf方法完成对bitmap对象大小的判断。除了LruCache的创建外,LruCache还包括缓存对象的添加,获取,删除等。

        mImageCache.put(key,value);
        mImageCache.get(key);
        mImageCache.remove(key);
  • DisLruCache

DisLruCache用于实现磁盘缓存,通过将缓存对象写入文件系统而实现缓存的效果。值得注意的是,虽然DisLruCache得到了Android官方文档的推荐,但其并不包括在SDK中。当我们需要使用DisLruCache是,需要手动下载源文件,并引入项目中,下载地址:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
如果不能访问可以点击这里进行下载。下面分别从DiskLruCache的创建、添加以及查找来描述其使用。

1、DiskLruCache的创建
DiskLruCache的创建并不能通过构造方法实现,

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

四个参数中,directory表示磁盘缓存对象所存储的文件系统路径,通常都会存放在 /sdcard/Android/data/ 包名 /cache 这个路径下面,当然,我们还可以选择支持的其他路径。当应用被卸载后,缓存文件也将被删除。
第二个参数appVersion表示应用版本号,一般设为1即可。当应用版本号发生变化时,系统内关于该应用的缓存将被清除。
第三个参数表示单个节点所对应数据的个数,一般设为1即可。第四个参数表示缓存的总大小,当缓存大小超过这个值后,DiskLruCache会清除一些缓存对象,以不大于这个值。

        File dir = new File("/sdcard/Android/data/com.example.huangzheng.bitmaptest/cache");
        int maxSize = 1024 * 1024 * 50;
        if (!dir.exists()){
            dir.mkdir();
        }
        try {
            mDiskCache = DiskLruCache.open(dir,1,1,maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

2、DiskLruCache的缓存添加
还是一样的,我们以网络获取一张图片,并写入磁盘缓存为例。首先通过下面的代码获取网络图片,并通过OutputStream写入本地。

//网络获取图片,并写入输出流
    private boolean downloadUrlToStream(String urlstring, OutputStream outputstream){
        HttpURLConnection httpconnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlstring);
            httpconnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(httpconnection.getInputStream(),8*1024);
            out = new BufferedOutputStream(outputstream,8*1024);
            int b;
            if ((b = in.read()) != -1){
                out.write(b);
            }
            return true;
        }catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (httpconnection != null){
                httpconnection.disconnect();
            }
            try {
                if (out != null){
                    out.close();
                }
                if (in != null){
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

DiskLruCache的写入操作是通过其Editor 类实现,Editor 不能new,需要调用DiskLruCache的edit方法来创建。

public Editor edit(String key) throws IOException {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }

从edit方法可以看到,需要传入key值,通常情况下图片的url中包括一些特殊字符,比较常规的做法是通过MD5进行编码,保证字符串的唯一性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值