Bitmap的加载和Cache

由于Bitmap的特殊性以及Android对单个应用所施加的内存限制,比如16M,这导致加载Bitmap的时候很容易出现内存溢出。比如以下场景:

1
java.lang.OutofMemoryError:bitmap size exceeds VM budget

Android中常用的缓存策略也是很有意思,缓存策略一个通用的思想,可以用到很多场景中,比如在实际开发中经常需要用到Bitmap做缓存。通过缓存策略,我们不需要每次都从网络上请求图片或者从存储设备中加载图片,这样就极大地提高了图片的加载效率以及产品的用户体验。目前比较常用的缓存策略是LruCache和DiskLruCache,其中LruCache常被用做内存缓存,而DiskLruCache用做存储缓存。Lru是Least Recently Used的缩写,即最近最少使用算法,这种算法的核心思想:当缓存快满时,会淘汰近期最少使用的缓存目标,很显然Lru算法的思想是很容易被接受的。

Bitmap的高效加载

Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。

如何高效地加载Bitmap呢,其实核心思想也简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。主要是用到它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如为2,那么采样后的图片其宽/宽均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。从最新官方文档中指出,inSampleSize的取值应该是2的指数,比如1、2、4、8、16等等。

通过采样率即可有效地加载图片,那么到底如何获取采样率呢,获取采样率也很简单,循序如下流程:

  • 将BitmapFactory.Options的inJustDecodeBounds参数设为True并加载图片
  • 从BitmapFactory.Options中取出图片的原始宽高信息,他们对应于outWidth和outHeight参数
  • 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
  • 将BitmapFactory.Options的inJustDecodeBounds参数设为False,然后重新加载图片。

经过上面4个步骤,加载出的图片就是最终缩放后的图片,当然也有可能不需要缩放。代码如下:

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
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}

// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;

// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}

Android中的缓存策略

缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略变得更为重要。有一个场景就是批量下载网络图片,在PC上是可以把所有的图片下载到本地再显示即可,但是放到移动设备上就不一样了。不管是Android还是IOS设备,流量对于用户来说都是一种宝贵的资源。

如何避免过多的流量消耗呢,那就是缓存。当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用从网络上获取了,这样就为用户节省了流量。很多时候为了提高用户的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。

目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader。

LruCache

LruCache是Android 3.1提供的一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。它是一个泛型类,它内部采用一个LinkedHashMap,当强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

LruCache是线程安全的,因为用到了LinkedHashMap。从Android 3.1开始,LruCache就已经是Android源码的一部分。

DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘存储,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache得到了Android官方文档的推荐,但它不属于Android SDK的一部分。

ImageLoader的实现

一般来说,一个优秀的ImageLoader应该具备如下功能:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

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

图片的异步加载是一个很有用的功能,很多时候调用者不想再单独的线程中以同步的方式来获取图片,这个时候ImageLoader内部需要自己在线程中加载图片并将图片设置所需的ImageView。图片压缩的作用更需要了,这是降低OOM概率的有效手段,ImageLoader必须合适地处理图片的压缩问题。

内存缓存和磁盘缓存是ImageLoader的核心,也是ImageLoader的意义所在,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户所造成的流量消耗,只有当这两级缓存都不可用时才需要从网络中拉取图片。

一个实现ImageLoader的例子:

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
public class ImageLoader {

private static final String TAG = "ImageLoader";

public static final int MESSAGE_POST_RESULT = 1;

private static final int CPU_COUNT = Runtime.getRuntime()
.availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;

private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};

public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);

private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
};
};

private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;

private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* build a new instance of ImageLoader
* @param context
* @return a new instance of ImageLoader
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}

private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}

private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}

/**
* load bitmap from memory cache or disk cache or network async, then bind imageView and bitmap.
* NOTE THAT: should run in UI Thread
* @param uri http url
* @param imageView bitmap's bind object
*/
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}

public void bindBitmap(final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}

Runnable loadBitmapTask = new Runnable() {

@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}

/**
* load bitmap from memory cache or disk cache or network.
* @param uri http url
* @param reqWidth the width ImageView desired
* @param reqHeight the height ImageView desired
* @return bitmap, maybe null.
*/
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
return bitmap;
}

try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}

if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}

return bitmap;
}

private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}

private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}

String key = hashKeyFormUrl(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();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}

private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}

Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}

return bitmap;
}

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;
}

private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;

try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
}

private String hashKeyFormUrl(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;
}

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();
}

public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment
.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}

return new File(cachePath + File.separator + uniqueName);
}

@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}

private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;

public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}

优化列表的卡顿现象

在一般ListView或者GridView中,使用照片墙的时候,容易出现滑动卡顿,如何优化呢,有三点建议:

  • 不要在getView中执行耗时操作。比如加载图片,肯定会导致卡顿,因为加载图片是一个耗时的操作,这种操作必须通过异步的方式来处理。
  • 控制异步任务的执行频率。比如在异步加载图片时,用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。那该如何解决呢,可以考虑在列表滑动的时候,停止加载图片,尽管这个过程是异步的,等列表停下来以后在加载图片仍然可以获得良好的用户体验。
  • 开启硬件加速可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated = “true”即可为Activity开启硬件加速。
,