Android中的三级缓存解析与实战

来源:互联网 发布:淘宝网怎么加好友呢 编辑:程序博客网 时间:2024/06/07 00:57

凡永恒伟大的爱,都要绝望一次,消失一次,一度死,才会重获爱,重新知道生命的价值。——《文学回忆录》

1、概述

由于Bitmap的特殊性以及Android对单个进程应用只分配16M的内存,这导致加载Bitmap的时候很容易出现内存泄漏。为了解决这个问题,引入了缓存策略。

缓存策略是一个通用的思想,可以用在很多场景中,在实际开发中,经常用Bitmap做缓存。通过缓存策略,我们不需要每次从网络上请求图片或者从存储设备中加载图片,这样就极大的提高了图片的加载效率以及产品的用户体验。

目前常用的缓存策略,LruCache常被用作内存缓存,DiskLruCache常被用作存储缓存。Lru(Least Recently Used),即最近最少使用算法。核心思想:当缓存快满时,会淘汰近期最少使用的缓存目标。

2、三级缓存

三级缓存策略,通过网络、本地、内存三级缓存图片,来减少不必要的网络交互,避免浪费流量。

  • 网络加载,不优先加载,速度慢,浪费流量
  • 本地缓存,次优先加载,速度快
  • 内存缓存,优先加载,速度最快

三级缓存原理

  • 首次加载 Android App 时,肯定要通过网络交互来获取图片,之后我们可以将图片保存至本地SD卡和内存中
  • 之后运行 App 时,优先访问内存中的图片缓存,若内存中没有,则加载本地SD卡中的图片
  • 总之,只在初次访问新内容时,才通过网络获取图片资源

3、Bitmap的高效加载

缓存实现最终目的是为了图片的高效显示,所以先介绍Bitmap的高效加载。

3.1、Bitmap加载

BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于从文件系统、资源、输入流以及字节数组中加载一个Bitmap对象,其中decodeFile和decodeResource间接调用了decodeStream方法。

3.2、高效加载Bitmap

采用BitmapFactory.Options来加载所需尺寸的图片。这里假设通过ImageView来显示图片,很多时候,ImageView的尺寸并没有图片的原始尺寸那么大,这个时候把整个图片加载进来再设给ImageView,这显然是没有必要的,因为ImageView并没有办法显示原始图片。

通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,就可以降低内存的使用,在一定程度上避免OOM,提高Bitmap加载时的性能。

BitmapFactory.Options中的inJustDecodeBounds参数:
当此参数为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会真正加载图片,这个操作是轻量级的。这里需要注意的是,Bitmap获取的图片宽/高信息和图片的位置以及程序运行的设备有关,比如同一张图片放在不同的drawable目录下或运行在不同屏幕密度的设备上,这都可能导致BitmapFactory获取不同的结果。

BitmapFactory.Options中的inSampleSize参数:
通过BitmapFactory.Option来缩放图片,主要用到它的inSampleSize参数即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize为2时,采样后的图片大小宽/长为图片的原始大小的1/2,而像素数为原来的1/4,内存也为原来的1/4。

获取采样率流程:
(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;
(2)从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;
(3)根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;
(4)将BitmapFactory.Options的inJustDecodeBounds参数设置为false,然后重新加载图片。

代码实现:

package com.chunsoft.bitmapcache;import android.content.res.Resources;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.util.Log;import java.io.FileDescriptor;/** * Developer:chunsoft on 2017/3/27 15:22 * Email:chun_soft@qq.com * Content:图片压缩,降低OOM概率 */public class ImageResizer {    private static final String TAG = "ImageResizer";    public ImageResizer() {    }    public Bitmap decodeSampledBitmapFromResource(Resources res,                                                  int resId, int reqWidth, int reqheight) {        // 1.将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片        BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeResource(res, resId,options);        // 2.从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数        // 3.根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqheight);        // 4.将BitmapFactory.Options的inJustDecodeBounds参数设置为false,然后重新加载图片        options.inJustDecodeBounds = false;        return BitmapFactory.decodeResource(res,resId, options);    }    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd,                                                        int reqWidth,int reqHeight) {        // 1.将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片        BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeFileDescriptor(fd, null, options);        // 2.从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数        // 3.根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);        // 4.将BitmapFactory.Options的inJustDecodeBounds参数设置为false,然后重新加载图片        options.inJustDecodeBounds = false;        return BitmapFactory.decodeFileDescriptor(fd, null, options);    }    private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqheight) {        if (reqheight == 0 || reqWidth == 0) {            return 1;        }        // 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数        final int height = options.outHeight;        final int width = options.outWidth;        Log.e(TAG,"origin, w=" + width +" h=" + height);        int inSampleSize = 1;        if (height > reqheight || width > reqWidth) {            final int halfHeight = height / 2;            final int halfWidth = width / 2;            while ((halfHeight / inSampleSize) >= reqheight                    && (halfWidth / inSampleSize) >= reqWidth) {                inSampleSize *= 2;            }        }        Log.e(TAG, "sampleSize:" + inSampleSize);        return inSampleSize;    }}

4、LruCache

LruCache是一个泛型类,它内部采用了LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。LruCache是线程安全的。

代码实现:

//求最大内存大小,换成KB单位int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);//定义缓存为内存的1/8int cacheSize = maxMemory / 8;//内存缓存mMemoryCache = new LruCache<String, Bitmap>(cacheSize){    //Bitmap大小的计算    @Override    protected int sizeOf(String key, Bitmap bitmap) {                return bitmap.getRowBytes()*bitmap.getHeight() / 1024;            }        };     //添加图片到内存缓存     private void addBitmapToMemoryCache(String key,Bitmap bitmap) {        if (getBitmapFromMemCache(key) == null) {            mMemoryCache.put(key, bitmap);        }    }    //从内存缓存中获取图片    private Bitmap getBitmapFromMemCache(String key) {        return mMemoryCache.get(key);    }

5、DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统,从而实现缓存的效果。它不属于Android SDK的一部分,源码链接。

5.1、DiskLruCache的创建

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

/*** 参数1:磁盘缓存文件在文件系统中的存储路径   * 参数2:应用版本号,一般设置为1   * 参数3:单个节点对应的数据个数,一般设置为1   * 参数4:缓存的总大小   */public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

5.2、DiskLruCache的缓存添加

DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。首先需要获取图片url所对应的key,然后根据ke y就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,edit()会返回null,即DiskLruCache不允许同时编辑一个缓存对象,之所以要把url转换成key,是因为图片中的url中可能存在特殊字符,这将影响url在Android中直接使用,一般采用url的md5值作为key。

//将URL转化为md5值作为key    //图片中的url很有可能有特殊字符,这将影响url在Android直接使用,一般采用url的md5值作为key    private String hashKeyFromUrl(String url) {        String cacheKey;        try {            final MessageDigest mDigest = MessageDigest.getInstance("MD5");            mDigest.update(url.getBytes());            cacheKey = bytesToHexString(mDigest.digest());        } catch (NoSuchAlgorithmException e) {            cacheKey = String.valueOf(url.hashCode());        }        return cacheKey;    }    //转化为16进制    private String bytesToHexString(byte[] bytes) {        StringBuilder sb = new StringBuilder();        for (int i = 0;i < bytes.length;i ++) {            String hex = Integer.toHexString(0xFF & bytes[i]);            if (hex.length() == 1) {                sb.append('0');            }            sb.append(hex);        }        return sb.toString();    }

将图片的url转换成key之后,就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。

String key = hashKeyFromUrl(url);        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();            }            mDiskLruCache.flush();        }

有了文件输出流,当从网络下载图片时,图片就可以通过这个文件输出流写入到文件系统上。

public boolean downloadUrlToStream(String urlString,                                       OutputStream outputStream) {        HttpURLConnection urlConnection = null;        BufferedOutputStream out = null;        BufferedInputStream in = null;        try {            final 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 (IOException e) {            Log.e(TAG, "downloadBitmap failed." + e);        } finally {            if (urlConnection != null) {                urlConnection.disconnect();            }            MyUtils.close(out);            MyUtils.close(in);        }        return false;    }

经过上面的步骤,其实并没有真正地将图片写入文件系统,还必须通过Editor()的commit()来提交写入操作。如果下载过程发生异常,那么可以通过Editor的abort()来回退下载操作。

 OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);            if (downloadUrlToStream(url, outputStream)) {                editor.commit();            } else {                editor.abort();            }            mDiskLruCache.flush();

5.3、DiskLruCache的缓存查找

和缓存的添加过程类似,缓存查找过程也需要将url转化为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可获得缓存的文件输入流,有了文件输入流就可以得到Bitmap对象。通过文件流来得到文件描述符,再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片:

 Bitmap bitmap = null;        String key = hashKeyFromUrl(url);        //磁盘缓存的读取需要通过Snapshot来完成        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);        if (snapShot != null) {            //通过Snapshot可以得到磁盘缓存对象的FileInputStream            FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);            //FileInputStream无法便捷地进行压缩,所以通过FileDescriptor来加载压缩后的图片,            //最后将加载后的Bitmap添加到内存缓存中            FileDescriptor fileDescriptor = fileInputStream.getFD();            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,                    reqWidth, reqHeight);            if (bitmap != null) {                addBitmapToMemoryCache(key, bitmap);            }

那么这样就实现了Android的三级缓存,具体实现代码在文末会给出。

6、图片三级缓存加载实现

(1)图片的同步加载
图片的同步加载是指能够以同步的方式向调用者提供加载的图片,这个图片可能是从缓存中读取的,也可能是从磁盘缓存中读取的,还可能是从网络拉取的。

(2)图片的异步加载
很多时候调用者不想在单独的线程中以同步的方式来获取图片,这时候需要在线程中加载图片,并将图片设置给需要的ImageView。

(3)图片压缩
降低OOM概率的有效手段

(4)内存缓存

(5)磁盘缓存

(6)网络拉取

具体代码在文末。

7、优化列表的卡顿现象

主要解决方案就一条,不要在主线程中做太耗时的操作。主要三个方面。

(1)不要在getView中执行耗时操作。如果直接在getView中加载图片,肯定会导致卡顿,因为加载图片是一个耗时操作,因此采用异步操作。

(2)控制异步任务的执行频率。以照片墙来说,在getView方法中会通过异步方法来异步加载图片,但是如果用户刻意地频繁上下滑动,这在一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。

可以考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载图片仍然可以获得良好的用户体验。具体实现时,可以考虑给ListView或GridView设置setOnScrollListener,并在OnScrollListener的onScrollStatechanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片:

if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {            mIsGridViewIdle = true;            mImageAdapter.notifyDataSetChanged();        } else {            mIsGridViewIdle = false;        }

然后在getView方法中,仅当列表停止时才能加载图片。

if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {                imageView.setTag(uri);                mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);            }

(3)硬件加速。一般来说,经过上面两个步骤,列表都不会有卡顿现象,在绝大数情况下,硬件加速可以解决莫名的卡顿现象,还可以设置android:hardwareAcclerated=”true”即可为Activity开启硬件加速。

8、总结

本文介绍了图片加载、三级缓存策略、列表的滑动流畅性,通过文末的demo,可以更好的理解本文内容。

=====================================================

源码链接。

0 0