上一节我们知道,Bitmap在Android开发中是比较占用内存和耗费资源的。我们不可能每次都从网络去下载图片,每次都从SD卡或者res去读取bitmap,因为这些操作很耗时间和资源的。这个时候,我们就需要用到图片缓存机制。
一、Bitmap图片缓存机制的流程图
我们先来假设,Bitmap即没有内存缓存、也没有SD卡缓存的情况下,怎样将Bitmap加载到ImageView上。
步骤思路:
网络请求服务器,然后以流InputStream的方式返回到客户端。
将流读取出byte[]转换成Bitmap。
在这过程中可以对Bitmap进行压缩,减少内存占用。
缓存Bitmap到SD卡
缓存Bitmap到内存
通过handler更新UI到ImageView
二、从网络获取Bitmap并压缩 思路:
开启一个AsyncTask,传入ImageView和图片url,其中ImageView使用软引用,更易被GC回收。
在doInBackground方法中执行后台任务,当联网成功并获取到了inputstream后,开始压缩Bitmap【在这里同时采用了质量和取样压缩法】。
doInBackground返回Bitmap后,在onPostExecute中直接更新ImageView。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 @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); } } } } 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 ; } 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本地 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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> imageCache
但是Google不建议这样做,原因有两点。
从Android 2.3开始,垃圾回收器更积极的回收持有软引用或弱引用的对象,导致软引用缓存的数据极易被释放,这使得软引用和弱引用变得没有效果。
此外,在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内存。
来源:http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/cache-bitmap.html
所以,在这里Google推荐使用Android自带的API:LruCache 来实现内存缓存
4.1 介绍LruCache LruCache 底层是把最近使用的对象用强引用存储在LinkedHashMap中,当LruCache的缓存值达到预设定值的容量时,就会把最近最少使用的对象 从内存中移除。
Lru算法实现原理:
1、将新数据放到链表头部
2、每当被添加过的数据被访问,就将这个数据又移到链表头部 【这样便可以实现,永远把最近经常使用的对象放到链表前面】
3、当超过预设链表容量时,就将链表后面的数据移除掉
LRU图片缓存对象初始化:
1 2 3 4 5 6 7 8 9 10 int maxMemory = (int ) Runtime.getRuntime().maxMemory(); 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卡也没用,最终联网获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void showImageByAsyncTask (String url, ImageView imageView) { Bitmap bitmap = getBitmapFromMemoryCache(url); if (bitmap == null ) { bitmap = getBitmapFromSD(sdUrl); if (bitmap == null ) { 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); } } private Bitmap getBitmapFromMemoryCache (String url) { return mLruCaches.get(url); }
2、联网获取到的Bitmap,如果不为空,就put到缓存。先去判断缓存里面有没有这个对象,如果没有就put,有了则不需要再重复put。
1 2 3 4 5 6 private void addCache (String url, Bitmap bitmap) { if (getBitmapFromMemoryCache(url) == null ) { mLruCaches.put(url, bitmap); } }