前文介绍了Bitmaps的异步加载,将单个Bitmap加载到视图控件是很简单直接的,但是要同时批量加载的话就会变得复杂了.对于ListView或GridView或ViewPager来说,由于他们是可以滑动的,所以有多少图片将要被加载出来就变得不确定.
由于ListView等这种View带有回收机制,因此内存的使用会保持在较低的水平,但同时由于这个回收机制,之前已经加载的Bitmap会被GC释放掉,这种方式有个不好的地方是:你滑到一个位置a,然后滑到一个位置b,然后再滑到a,本来a位置的Bitmap已经加载完了,但是当你滑到b时a位置的Bitmap就被GC回收了,这时a又要重新加载Bitmap.可以想象像ListView这种需要频繁滑动并且带有很多childView的View如果每次都要重新加载的话是多么的耗时耗流量.
因此,需要引入内存和磁盘缓存的帮助,才能让ListView这类型的View快速的加载图片以支持快速滑动.
使用内存缓存
内存缓存是以占用app应用内存为代价来给Bitmap提供快速访问.LruCache这个非常适用于做内存缓存,比如缓存bitmap等,它的内部是用一个LinkedHashMap来保存缓存对象的强引用,并使用LRU(最近最少使用)算法来控制队列的移除以保证不超过设定的内存大小.
注意: 过去LinkedHashMap保存的是引用对象的弱引用或软引用,但是这现在不推荐,因为Android 2.3(API 9)以后GC变得更加的aggressive,弱引用或软引用会被回收,导致引用无效.除此之外,在3.0之前有个问题,就是Bitmap的backing date(也是就是真正的字节数据,Bitmap只是保存一些信息而已)占据的内存不会被正常释放,会潜在的导致应用因为内存溢出而crash.
LruCache使用的时候需要设置一个大小,如何设置一个合理的大小呢?可以参考以下几个因素:
- 除了你的Activity和application之外,其他的地方使用内存情况如何?
- 一次会有多少张图片显示在屏幕?有多少张图片需要被加载出来等待被显示?
- 设备的屏幕大小和密度是多少?高密度屏幕的设备需要更大的cache空间.
- Bitmaps的尺寸和配置参数是什么?它们每个要占多少内存?
- 这些Images放被访问的频率如何?有一些访问比其他频繁的吗?可以根据这个来决定是否将这些对象设置成常驻内存或是使用多个LruCache.
- 你能平衡好数量和质量吗?有时只存储一大推的小图,然后把大图放在使用的时候再加载,这种方式在某些情况很有用.
上述提到了这么多的因素,但是实际上并没有什么具体的最合理的数值或公式,你需要自己去分析然后确定对于你的app最适合的缓存大小.太小的话不仅无用还带来而外的计算,太大又会导致app的可用内存的减少,容易OOM.
下面看个LruCache的设置例子:
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);
}
注意:
- 上面代码中给cache分配的大小为app可用总内存的1/8,假设app总内存为32M,那么cache的大小为4M,如屏幕分辨率为800x480,满屏的话需要图片大小为1.5M左右(8004804 bytes),对于GridView来说,这个cache可以缓存2.5个页面左右(4/1.5).
- 注意计算时的单位要统一,即maxMemory的单位和sizeOf()返回的单位要统一,上述的代码用的是Kbytes.
有了cache之后加载图片的逻辑变成这样:
当收到图片加载请求,先去cache中查找是否有缓存,有则直接返回,没有则返回空接着去请求图片.
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
// 1. 去cache中查找
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
// 2. cache中有缓存
mImageView.setImageBitmap(bitmap);
} else {
// 3. cache中内缓存
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
之前自定义的Task也要更新一下,需要把加载完毕的Bitmap放到cache中
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));
// 将加载完毕的bitmap放到cache中
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
使用磁盘缓存
内存缓存对于最近使用的Bitmap的快速访问非常有用,但是这个还是不够可靠.
- 像GridView这种带大量childView的控件很容易就占满了内存缓存.
- 当你的应用被其他任务打断时,比如来了个电话,你的app就会跑到后台运行,这样你的app的进程可能会被系统给destroy掉,这样内存缓存也就没了,而当用户返回时app又要重新加载这些资源.
这个时候磁盘缓存就可以帮忙了,磁盘缓存可以缓存Bitmaps,这样就可以减少非内存缓存图片的加载速度,当然磁盘的访问速度要比内存的来的慢并且加载的时间也不可预知,因此应该使用异步加载.
注意: 如果要缓存经常访问的图片,可以使用ContentProvider,比如相册类.
下面的代码使用了一个DiskLruCache类的引用,来自Android source,具体如下:
// DiskLruCache的引用,算法一样为LRU
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
// 磁盘缓存初始化标志值,true表示还没初始化
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];
// 获取一个DiskLruCache对象
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) {
// 注意这个方法,与源码的存入方式不一样,这里把这个方法当作是将bitmap存入磁盘缓存的操作即可
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);
}
注意:
- 初始化磁盘缓存要进行磁盘操作,因此需要异步.
- 由于磁盘缓存初始化的异步,从而导致了一种可能的的出现: 用户调用getBitmapFromDiskCache()来访问磁盘缓存,然后磁盘缓存还没创建完毕,因此使用同步锁来防止这种情况的出现.
- 检查内存缓存是否有请求资源是在UI线程中操作的,而检查磁盘缓存是在异步操作的.
- 图片从网络加载完毕之后要放到内存缓存和磁盘缓存,以便后续使用.
- 上述代码中有一句: mDiskLruCache.put(key, bitmap);注意这个方法,源码中没有这个方法,源码的存入方式不一样,这里把这个方法当作是将bitmap存入磁盘缓存的操作即可,具体可以参考官方Demo.
处理运行时配置的变更
Handling Runtime Changes,配置的变更如屏幕旋转等,会导致Activity被destroy掉然后restart,所以如果你想要在配置变更的时候还保存内存缓存的话,有一个方法可以帮助:
将内存缓存引用存到一个设置了setRetainInstance(true)(该方法会改变Fragment的生命周期)的Fragment里面,在Activity重新创建完毕之后重该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);
}
}
要测试的话,可以调用setRetainInstance()方法,分别传入true和false,然后旋转屏幕.
网友评论