在使用ListView,GridView,ViewPager一类的组件时,图片的总数包括了当前显示的和将会滑动出来的,事实上可以是无限的。
为什么内存使用不会增加呢,因为此类组件会回收移除屏幕的child view,垃圾回收也会回收掉你不再持有引用的Bitmap。为了避免每次展示图片都重新加载一遍,让滑动的时候更流畅,内存缓存和硬盘缓存此时就可以派上用场了。
内存缓存以内存为代价换取Bitmap的快速访问,LruCache类可以很好的缓存Bitmap,将最近引用过的对象放到LinkedHashMap里面,并将最近最少使用的对象给替换掉。
在过去,流行的内存缓存方式是使用软引用或者弱引用,然而这不是推荐做法。从Android2.3开始,垃圾回收器会更加积极地回收软引用或者弱引用对象,使得它们没有什么用处。另外,在Android3.0之前,Bitmap数据保存在本地内存,而且内存释放的方式不可预测,有可能会导致app短暂的超过内存极限然后崩溃掉。
为了选择一个合适的LruCache的大小,需要考虑很多因素,比如说:
1. 你的activity或者应用剩余部分的内存使用是否紧张。
2. 一屏需要显示多少图片,需要准备多少未来将要显示的图片。
3. 你的设备尺寸和分辨率是怎样的,一个xhdpi的设备缓存同样数量的图片会比一个hdpi的设备需要更多的内存。(举个例子,ImageView的大小是100dp*100dp,那么hdpi需要100px*100px=100*100*4(ARGB) byte;而xhdpi需要200px*200px=200*200*4(ARGB) byte)。
4. Bitmap的尺寸和格式,因此每个Bitmap会占用多少内存。
5. 图片被访问的频率?是不是有一些会比其它的访问得更加频繁,如果如此,也许你希望它们始终存在于内存之中,甚至针对不同组的Bitmap拥有多个LruCache对象。
6. 你可以平衡数量和质量么?有时候存储大量的低质量图片更有用,然后再另外一个线程中加载高质量图片。
并没有一个特定的大小对所有应用都合适,这取决于开发者如何去分析内存的使用然后来得出一个合适的解决方案。一个过小的缓存会造成额外的开销,并没有什么益处。一个太大的缓存也会造成OutOfMemory异常,给你的app的剩余部分只留下非常小的内存空间。
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
在这个例子中,使用了应用可用内存的1/8作为缓存。在一个hdpi的设备中,最少有32/8=4MB,一个全屏的GridView在一个800*480的设备上会使用约800*480*4bytes = 1.5MB,所以这个缓存大概可以缓存2.5页图片在内存中。
当加载Bitmap到ImageView时,先去LruCache中找有没有被缓存过,没有再去开线程加载图片
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
BitmapWorkerTask中加载图片成功后,将其加入内存缓存
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
你不能完全依赖于内存缓存,因为碰到大数据集内存缓存很容易满,而且你的app很可能被一个电话给打断,此时后台有可能清理掉你的内存缓存。当用户回来的时候,你就只能重新加载图片了。
在这种情况下,硬盘缓存有助于持久的保存已加载的图片,减少图片的加载次数。当然,从硬盘读取图片要比内存慢,应该开线程去做这件事。
注意:ContentProvider可能是一个更加合适的放硬盘缓存的地方,如果图片被访问得更加频繁的话。比如一个image gallery应用。
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
上面这段代码之前貌似看过郭神的解析,现在一看关键牛逼在线程的同步处理吧。首先mDiskCacheLock这个对象来保证DiskCache的线程安全,然后用mDiskCacheStarting这个对象来保证DiskCache初始化完成之后再来开始尝试从DiskCache中取图片。
这个地方有个小问题,就是容易去想,尝试从DiskCache中存储图片时需不需要同样的处理,但是再一考虑,显然是先尝试取图片,然后线程阻塞,然后等待DiskCache加载完成,然后没找到去加载图片,然后再添加到硬盘缓存这个流程。所以这里这么写是安全的。
在主线程这种检查内存缓存时,在子线程中检查硬盘缓存。硬盘操作不应该发生在UI线程中。当图片加载完成后,Bitmap会向硬盘缓存和内存缓存中添加。
当用户旋转屏幕时,Activity会Destroy然后restart当前Activity,这种时候你肯定不希望重新去加载所有图片一遍。你此时有一个内存缓存,这个缓存可以被传递给一个新的Activity实例,通过Fragment调用setRetainInstance(true)来实现。在Activity recreated之后,这个保留的Fragment会重新关联上来,使得你有机会去重新访问缓存对象。
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
这里给我们介绍了一种使用Fragment做对象传递的方式,虽然我觉得直接在Application Context中做全局的内存缓存就够了,然而也提供了一种Fragment的新用法。