上一节我们知道,Bitmap在Android开发中是比较占用内存和耗费资源的。我们不可能每次都从网络去下载图片,每次都从SD卡或者res去读取bitmap,因为这些操作很耗时间和资源的。这个时候,我们就需要用到图片缓存机制。

一、Bitmap图片缓存机制的流程图

我们先来假设,Bitmap即没有内存缓存、也没有SD卡缓存的情况下,怎样将Bitmap加载到ImageView上。

步骤思路:

  1. 网络请求服务器,然后以流InputStream的方式返回到客户端。
  2. 将流读取出byte[]转换成Bitmap。
  3. 在这过程中可以对Bitmap进行压缩,减少内存占用。
  4. 缓存Bitmap到SD卡
  5. 缓存Bitmap到内存
  6. 通过handler更新UI到ImageView

二、从网络获取Bitmap并压缩

思路:

  1. 开启一个AsyncTask,传入ImageView和图片url,其中ImageView使用软引用,更易被GC回收。
  2. 在doInBackground方法中执行后台任务,当联网成功并获取到了inputstream后,开始压缩Bitmap【在这里同时采用了质量和取样压缩法】。
  3. doInBackground返回Bitmap后,在onPostExecute中直接更新ImageView。
private static final String PIC_URL =
 "http://7xijtp.com1.z0.glb.clouddn.com/
 8F055A06-FF58-4A43-
 A846-55CF05B0272C.png";

@Override
protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_test3);

        ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);

        new BitmapWorkerTask(iv_2, PIC_URL).execute();

}

class BitmapWorkerTask extends AsyncTask<Void, Void, Bitmap> {

        private final WeakReference<ImageView> imageViewReference;
        private String url;

        public BitmapWorkerTask(ImageView imageView, String url) {
            imageViewReference = new WeakReference<ImageView>(imageView);
            this.url = url;
        }

        @Override
        protected Bitmap doInBackground(Void... voids) {
            return getBitmapFromServer(url);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {

            if (imageViewReference != null && bitmap != null) {
                final ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
}

/**
 * 通过图片url 从服务器获取Bitmap
 * @param picUrl
 * @return
 */
private Bitmap getBitmapFromServer(String picUrl) {

        URL url = null;
        Bitmap bitmap = null;
        InputStream is = null;

        try {
            url = new URL(picUrl);
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(httpURLConnection.getInputStream());
            if (is != null) {
                // 压缩图片
                bitmap = decodeSampledBitmap(is, 10);
            }
            httpURLConnection.disconnect();
            return bitmap;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return null;
}

/**
 * 图片压缩
 *
 * @param quality
 * @return
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
@NonNull
private Bitmap decodeSampledBitmap(InputStream ins, int quality) {

        BitmapFactory.Options opts = new BitmapFactory.Options();
        Bitmap bm = null;
        ByteArrayOutputStream baos = null;
        try {
            byte[] bytes = readStream(ins);

            opts.inJustDecodeBounds = true;

            bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);

            opts.inJustDecodeBounds = false;

            int picWidth = opts.outWidth;// 得到图片宽度
            int picHeight = opts.outHeight;// 得到图片高度
            Log.e("原图片高度:", picHeight + "");
            Log.e("原图片宽度:", picWidth + "");

            opts.inSampleSize = 2;//设置缩放比例

            bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);

            int picWidth2 = opts.outWidth;// 得到图片宽度
            int picHeight2 = opts.outHeight;// 得到图片高度

            Log.e("压缩后的图片宽度:", picWidth2 + "");
            Log.e("压缩后的图片高度:", picHeight2 + "");
            Log.e("压缩后的图占用内存:", bm.getByteCount() + "");

            // 开始质量压缩
            baos = new ByteArrayOutputStream();
            bm.compress(Bitmap.CompressFormat.PNG, quality, baos);

            byte[] b = baos.toByteArray();
            bm = BitmapFactory.decodeByteArray(b, 0, b.length, opts);

            Log.e("质量压缩后的占用内存:", bm.getByteCount() + "");
            return bm;
        } catch (Exception e) {
            e.printStackTrace();
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e1) {
                    e.printStackTrace();
                }
            }
        }
        return bm;
}

/*
 * 得到图片字节流 数组大小
 * */
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();
}

运行Log的结果:

三、Bitmap缓存到SD本地

/** 
 * 保存Image的方法。
 * 需要注意的地方有:1、判断是否有SD卡;2、判断SD卡存储空间是否够用。
 * @param fileName  
 * @param bitmap    
 * @throws IOException 
 */  
public void savaBitmap(String fileName, Bitmap bitmap){

    FileOutputStream fos = null;
    try {
        if(bitmap == null){
            return;
        }
        String path = getStorageDirectory();
        File folderFile = new File(path);
        if(!folderFile.exists()){
            folderFile.mkdir();
        }
        File file = new File(path + File.separator + fileName);
        file.createNewFile();
        fos = new FileOutputStream(file);
        bitmap.compress(CompressFormat.PNG, 100, fos);

    }catch (IOException E){

    }finally {
        if(fos != null) {
            try {
                fos.flush();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

四、Bitmap缓存到内存

首先这里先引用一段Google文档的说明:

Note: In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash.

简单翻译过来的意思就是:
在以前使用内存缓存的时候一般喜欢使用SoftReference或WeakReference的位图缓存,例如:

HashMap<String, SoftReference<Bitmap>> imageCache

但是Google不建议这样做,原因有两点。

  1. 从Android 2.3开始,垃圾回收器更积极的回收持有软引用或弱引用的对象,导致软引用缓存的数据极易被释放,这使得软引用和弱引用变得没有效果。
  2. 此外,在Android 3.0 之前,图片会存储在native memory内存中,不是以一种可预见的方式释放,可能导致应用程序暂时超过其内存限制而崩溃。

如何理解,在Android 3.0 之前,图片会存储在native memory内存中,不是以一种可预见的方式释放这句话呢?
因为在3.0以前,Bitmap 的位图数据是存储在 native c 堆内存中的,java的单纯GC释放是释放不了这部分内存的,所以Bitmap如果越积越多,可能就会导致应用程序暂时超过其内存限制而崩溃。所以在3.0之前要较为彻底的回收Bitmap占用内存的话,都会调用Bitmap.recycle()方法来释放掉存储在native内存的位图。在3.0以后,Bitmap的位图数据改为存储在Dalvik heap中了,这样便可以直接用java gc的方式来回收Bitmap内存。

所以,在这里Google推荐使用Android自带的API:LruCache 来实现内存缓存

4.1 介绍LruCache

LruCache 底层是把最近使用的对象用强引用存储在LinkedHashMap中,当LruCache的缓存值达到预设定值的容量时,就会把最近最少使用的对象从内存中移除。

Lru算法实现原理:

  • 1、将新数据放到链表头部
  • 2、每当被添加过的数据被访问,就将这个数据又移到链表头部【这样便可以实现,永远把最近经常使用的对象放到链表前面】
  • 3、当超过预设链表容量时,就将链表后面的数据移除掉

LRU图片缓存对象初始化:

// 获取应用的最大可用内存
 int maxMemory = (int) Runtime.getRuntime().maxMemory();
 // 设置LruCache缓存最大值
 int cacheMemory = maxMemory / 8;
 LruCache<String,Bitmap> mLruCache = new LruCache<String, Bitmap>(cacheMemory) {

    @Override
     protected int sizeOf(String key, Bitmap value) {
             return value.getRowBytes() * value.getHeight();
     }

 };

解析:

  • mLruCache 每次添加Bitmap图片缓存的时候(put操作),都会调用sizeOf方法,返回Bitmap。的内存大小给LruCache,然后循环增加这个size。
  • 当这个Size内存大小超过初始化设定的cacheMemory大小时,则遍历map集合,把最近最少使用的元素remove掉

4.2 LruCache缓存思路

1、先去LruCahce里面找有没有Bimap,如果有,直接设置ImageView.setImageBitmap。如果没有,先去SD卡,如果SD卡有,则从SD卡读取获取;如果SD卡也没用,最终联网获取。

public void showImageByAsyncTask(String url, ImageView imageView) {
    // 1.先从内存缓存获取Bitmap
    Bitmap bitmap = getBitmapFromMemoryCache(url);
    if (bitmap == null) {
        // 当内存缓存没有的时候,从SD卡获取
        bitmap = getBitmapFromSD(sdUrl);
        if (bitmap == null) {
            // SD卡也没有的时候,联网获取
            BitmapAsyncTask myAsyncTask = new BitmapAsyncTask(url, imageView);
            myAsyncTask.execute();
        } else {
            if ((url).equals(imageView.getTag()))
                imageView.setImageBitmap(bitmap);
        }
    } else {
        if ((url).equals(imageView.getTag()))
            imageView.setImageBitmap(bitmap);
    }
}

// 通过图片url获取缓存在LruCache中的Bitmap
private Bitmap getBitmapFromMemoryCache(String url) {
    return mLruCaches.get(url);
}

2、联网获取到的Bitmap,如果不为空,就put到缓存。先去判断缓存里面有没有这个对象,如果没有就put,有了则不需要再重复put。

// 把获取到的Bitmap缓存到LruCache
private void addCache(String url, Bitmap bitmap) {
    if (getBitmapFromMemoryCache(url) == null) {
        mLruCaches.put(url, bitmap);
    }
}