上一节我们知道,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。
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();
}
三、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不建议这样做,原因有两点。
- 从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内存。
所以,在这里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);
}
}