Android Bitmap 优化(2) - 图片缓存

上一节我们知道,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。
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);
}
}
}
}

/**
* 通过图片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;
}

/**
* 图片压缩
*/
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
/** 
* 保存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> 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内存。

来源: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缓存最大值
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) {
// 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。

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

Android Bitmap图片缓存