Bitmap(二) 内存管理#
1.使用内存缓存保证流畅性
这种使用方式在ListView等这种滚动条的展示方式中使用最为广泛,
使用内存缓存
内存缓存位图可以提供最快的展示。但代价就是占用一定的内存空间。这个工作最适合LruCache.java去做。LruCache具有一块内存区域,他可以用来持有value值得强引用。每次一个value进来,就会放到队列的头,一旦队列满了。队尾的value就会被清除,这样垃圾收集器就可以回收掉踢出队列的value.
entryRemoved(boolean, K, V, V)方法可以显示的踢出指定的value.
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}//这是常见的分配一个4M的内存作为缓存空间的方法。
这个类是线程安全的。多个缓存操作可以在同步代码块中原子级别的完成。
synchronized (cache) {
if (cache.get(key) == null) {
cache.put(key, value);
}
}
用这个类就能保证缓存下最近用过的位图,最近没用的位图被踢出缓存中。
使用软引用,弱引用缓存
这种方式不建议使用。因为从API9(Android9)开始垃圾收集器更容易收集软引用和弱引用。这就导致这样的方式变得效果不好。除此之外,API11(Android3.0)之前bitmap缓存在native层内存中,这种存储可能导致不可预见性。潜在可能造成应用内存溢出,程序崩溃。
使用LruCache就要选择一个合适的内存块大小。下列因素是需要考虑的方面:
1.应用剩余内存多少
2.一屏加载多少图,其中有多少图是要缓存准备加载的。
3.设备屏幕大小和像素密度。高像素密度的设备比低像素密度的设备需要更大的内存缓存空间来持有相同大小的图
4.位图的配置和尺寸,每个位图占多大内存
5.图片调用的频繁度,其中有些图片是否比其他图片调用频繁度高。如果是这种情况就需要把某些图片一直放在内存中,或者使用多个LruCache区分存储。
6.数量和质量要有一个平衡。有时候缓存大量的小图比较有用。这时候在后台去加载更大质量更好的图。
LruCache的内存占用大小是一个经验值。这个值取决于用途和方案。如果太小可能会连一张图也存不下。如果太大可能就会造成OOM或者导致剩余给应用使用的内存会变小。
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;//用1/8内存空间做存储
//在使用normal/hdpi的手机上,1/8大概是4M内存。一个在800*480的设备上,充满图片的全屏GridView大概使用1.5M内存。(800*400*4)(这里默认使用8888格式图片)也就是4M的LruCache能缓存两屏半的图。
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);
}
//这里用资源ID做key,那如果加载过了,那bitmap就不是null的。可以直接set进ImageView。如果null,那就去开线程拿图。再做缓存
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);
}
}
拿到图以后需要缓存进LruCache
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;
}
...
}
2.使用硬盘缓存保证流畅性
内存缓存可以有效加快Bitmap的加载,然而不能依赖内存缓存去处理正在被加载的图片。
像需要加载大量数据的GridView等视图可以很快用光内存缓存空间。比如一个电话打进来可能就会打断APP的流程。APP进了后台以后就可能被杀死或者被回收掉内存。一旦用户返回应用,那APP可能还是需要加载一边之前的图。
当内存中没有图的时候,这种情况下就需要硬盘缓存去持久化位图,减少加载的时间。
ContenetProvider可能更适合持久化使用频繁的图片。比如系统图片库应用。
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);//这里去执行拿图的策略
...
}
这个InitDiskCacheTask类用于开一个硬盘缓存
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;
}
}
BitmapWorkerTask类用于获取位图,先从硬盘缓存取,如果没有再按正常的方式取。取到以后加到内存缓存和硬盘缓存
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);
}
3.处理运行时配置变换
比如屏幕旋转,造成界面重启。这时候肯定是不希望去重新加载一边的。所以最好的办法就是复用原来保存在界面中的图。
可以使用设置了 setRetainInstance(true)的Fragment做这个工作。界面重启以后,这个Fragment可以带着之前的缓存重新attach到界面中。
这样就可以迅速加载图片了。
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;
}
...
}
这是相应的Fragment
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);
}
}
这样就能保证无论怎样反转,都不会影响到图片的加载了。