Android中BitMap加载和缓存

1.Bitmap的高效加载

1.1 通常如何加载Bitmap

Bitmap在Android指的是一张图,可以是.png/.jpg等其他格式

BitmapFactory提供四类方法:

decodeFile、decodeResource、decodeStream、decodeByteArray

对应从文件系统、资源、输入流、字节数组中加载出一个Bitmap对象

decodeFile、decodeResource又间接调用了decodeStream方法

1.2 如何高效加载Bitmap

核心思想是采用BitmapFactory.Options来加载所需尺寸的图片

比如通过ImageView来显示图片,通常ImageView没有图片原始尺寸这么大,这时就通过BitmapFactory.Options按一定的采样率来加载缩小后的图片,降低内存占用

1.3 BitmapFactory.Options

BitmapFactory.Options缩放图片,主要是用到了 inSampleSize 参数,即采样率
inSampleSize为1时,采样为原始大小
inSampleSize 为2时,图片的宽高为原图的1/2,像素数为1/4,占用内存为1/4
inSampleSize 为4,缩放比例就是1/16,即2的4次方
inSampleSize应该为2的指数倍,2,4,8,16等
inSampleSize小于1,相当于1
inSampleSize不为2 的指数,向下取整为2的指数

1.4 如何获取采样率

●将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片
●设置为true后BitmapFactory只会解析图片的原始宽高信息,并不会加载图片
●根据结果设置合适的采样率
● inJustDecodeBounds设回false然后重新加载图片

举例

public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();//拿到
        options.inJustDecodeBounds = true;//设置
        BitmapFactory.decodeFileDescriptor(fd, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);//计算

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

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

        // Raw height and width of image
        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;

            // Calculate the largest inSampleSize value that is a power of 2 and
            // keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

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

2 Android中的缓存策略

缓存可以避免过多的消耗流量

当用户第一次从网上加载图片后,会把图片缓存在内存中,再缓存到本地储存设备.这样当应用打算从网络请求一张图片的时候会先访问内存,再访问储存设备,都没有后才会去下载

目前常用的缓存算法是LRU,近期最少使用算法

2.1 LruCache

最近最少使用缓存,它用强引用保存需要缓存的对象,内部维护一个队列(实际是LinkedhashMap内部的双向链表,LruCache对其进行了封装,添加了线程安全操作),当其中的一个值被访问时,它被放到队列的尾部,当缓存满了,头部的值会被丢弃,之后可以被垃圾回收。

2.2 LruCache的实现

需要提供缓存的总容量大小并重写sizeOf方法(计算缓存对象的大小)
LruCache还支持删除操作,通过remove方法即可删除一个指定的缓存对象

                //获取最大可用的内存空间
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;//缓存大小为总容量的1/8,单位KB
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {//sizeOf()用来计算缓存对象的大小
                return value.getRowBytes() * value.getHeight() / 1024;//除1024为了将单位转成KB
            }
        };
    }
2.2.1 从Lrucache获取一个缓存对象

mLruCache.get(key)

2.2.2 Lrucache添加一个缓存对象

mLruCache.put(key,bitmap)

2.2.3 删除一个指定的缓存对象

mLruCache.remove(key);

2.3 DiskLruCache

● 用于实现存储设备缓存,磁盘缓存
● 将缓存对象写入文件系统,从而实现缓存效果

2.3.1 DiskLruCache的创建

DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身

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

第一个参数:表示磁盘缓存在文件系统中的存储路径,可以选择SD卡.(如果希望应用卸载后删除缓存文件,那么就选择SD卡上的缓存目录,否则应该选择SD卡上的其他特定目录)
第二个参数:表示应用的版本号,一般为1,版本号改变时会清空之前所有的缓存文件
第三个参数:表示单个节点所对应的数据个数,一般为1
第四个参数:表示缓存的总大小,比如50MB

2.3.2 DiskLruCache的缓存添加

● 此操作是通过Editor来完成的
● 缓存首先要获取图片的url所对应的key(url的md5值)
● 缓存只允许编辑一个缓存对象
● 通过Editor对象得到一个文件输出流,写入到文件系统上
● 最后Editor.commit()
○ 第一步

    private String hashKeyFormUrl(String url){
        String cacheKey;
        try {
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;//获取URL对应的key
    }
 
    private String bytesToHexString(byte[] digest) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < digest.length; i++) {
            String hex = Integer.toHexString(0xFF&digest[i]);
            if(hex.length() == 1){
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();

○ 网络下载图片,通过文件输出流写入到文件系统上。

 private boolean downloadUrlToStream(String urlString,OutputStream outputStream){
        int IO_BUFFER_SIZE = 0;
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
            int b ;
            while ((b = in.read()) != -1){
                out.write(b);
            }
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(urlConnection != null){
                urlConnection.disconnect();
            }
        }
        return false;
}

○ 将图片的url转为key之后,就可以获取Editor对象了,对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输入流,通过Editor的commit完成提交。
int DISK_CACHE_INDEX = 0;//由于前面的open设置了一个节点只能有一个数据,因此DISK_CACHE_SIZE = 0

                String key = hashKeyFormUrl(url);
                try {
                    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if(editor != null){
                        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                     //执行下载
                     if(downloadUrlToStream(url,outputStream)){
                            //提交写入操作
                            editor.commit();
                        }else {
                           //下载异常,执行回退操作
                            editor.abort();
                        }
                         //更新操作
                         mDiskCache.flush();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
2.3.3 DiskLruCache的缓存的查找

● 查找也需要将url转换为key
● 然后通过DiskLruCache的get方法得到一个Snapshot对象
● 再通过Snapshot对象得到缓存的文件输入流
● 记得通过BitmapFactory.Options加载一个缩放后的图片

     Bitmap bitmap = null;
     String keys = hashKeyFormUrl(url);
     try {
       DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keys);
       if(snapshot != null){
 FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap=mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
                        if(bitmap != null){
                            addBitmapToMemoryCache(keys,bitmap);
                        }

2.4 ImageLoader

2.4.1 ImageLoader的实现

一个优秀的ImageLoader应具备如下功能:

● 图片的同步加载(从内存缓存、磁盘缓存、网络中获取的)
● 图片的异步加载(ImageLoader内部需要自己在线程中加载图片并将图片设置给所需的ImageView)
● 图片压缩
● 内存缓存
● 磁盘缓存
● 网络拉取

ImageLoader还需要处理在ListView或者GridView中,快速下拉时图片在item错位的情况

内存缓存和磁盘缓存是ImageLoader的核心,通过这两级缓存极大的提高了程序的效率并降低了流量消耗,只有这两级缓存都不可用时才需要从网络中拉去图片。

2.4.2 优化卡顿现象

● 关键是不要在主线程中做太多耗时的操作
● 不要在getView中执行耗时操作(如加载图片)
● 控制异步任务的执行频率,比如频繁的上下滑动会产生N个异步任务.这时可以·考虑在列表滑动的时候停止加载图片,停下来再加载
● 开启硬件加速:android:hardwareAccelerated=“true”

来自:https://www.yuque.com/mrprf/sgsv7s/hq8gm5#6e7ca743

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容