位图管理、图片下载缓存、管理图片内存 (四) 缓存位图

时间:2023-01-11 22:15:45
        缓存位图
        下载单个位图对象到UI组件中是很直接的,然而 ,如果你需要同时加载一系列的图片,则会显得比较复杂。许多情况下(如 ListiView,GridView,ViewPager中), 屏幕上的图片总数可能由于组件滚动的看似无限量的。
        当图片被滑出屏幕时,为了节省内存,这类组件会循环使用子视图。假如你没有长时间地持有这些引用,垃圾回收器也会释放你下载的位图。 这些都是好的,不过,为了保持UI流畅而且快速,你可能想要避免他们每次回到屏幕时都持续处理这些图片。 可以通过内存缓存和硬盘文件缓存快速重复加载这些图片,很好的达到这个目的。

        这一节将学习如何在下载多个位图时通过磁盘文件缓存和内存缓存提高UI的响应速度和流畅度。


       使用内存缓存

       内存缓存提供了一种快速获取位图的方法,这种方法以消耗应用内存为代价。LruCache类是非常合适的,他可以缓存位图,通过强类型引用LinkedHashMap保持最近引用的对象,并在内存缓存容量达到极限时清除最长时间没有引用的对象。
注意:在此之前,通过实现SoftReference or WeakReference缓存位图是很受欢迎的方式,不过不建议使用。从android2.3开始,垃圾回收机制对导致引用无响应的强弱类型引用更具侵略性。并且,在android3.0之前,位图的后台数据被保存在本地内存中,通过可预知的方式释放,很可能暂时性地导致应用耗光内存是程序崩溃。

       为了选择合适的LruCache容量,下列因素应该被考虑在内。
       1. 应用或activity的剩余内存是如何加强的?
       2. 屏幕同时可能存在多少图片,有多少图片应该准备在屏幕上显示?
       3. 设备的屏幕尺寸和分辨率是多少?高分辨率的的设备需要更大的缓存容量用于持有相同数量的图片。
       4. 图片会被多么频繁地被访问?其中一些比另外一些会被更频繁的访问么?如果是,那么你可能想要持有比较大数量的对象在内存中,甚至可以用多个LruCache对象用于存储不同群组的位图。
       5. 你能平衡数量与质量的问题么?有时候,存储数量较大质量较低的位图,而在后台任务中加载另外一个高质量的版本,可能更加有利。

      

       没有适合所有应用的容量值和计算公式,这些都依赖于你怎样分析你的使用,并找出一种合适的解决方案。缓存太小会导致额外的不利开销,缓存过大可能导致内存泄露。

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

注意:在这个例子中,八分之一的内存会被分配为缓存,在一台常用的高分辨率设备上,这个最小值大约是4M, 在800*400分辨率的设备上,通过GridView全屏显示图片大约需要1.5M内存(800*400*4byte),所以,这可以在内存中缓存大约2.5页图片。


         当把位图加载到ImageView中时,LruCache会首先被检查。如果找到入口,则立即更新ImageView,否则,开启后台线程处理图片。

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

        使用硬盘缓存
        要获取最近浏览的位图,使用内存缓存方式是非常有利的,不过你不能依靠这种内存方式获取图片。像GridView这样具有大数据集的组件很容易填满内存缓存。此外,应用可能被另一个任务(如电话)打断,如果运行在后台,那么这个任务可能被杀死内存缓存会被销毁。
        在内存缓存中图片无法再获得的情况下,硬盘缓存可以用于辅助处理位图,降低加载时间。当然,从硬盘加载图片比从内存加载图片稍慢,而且加载过程应该在后台线程进行,因为硬盘的读取时间是不可预知的。

注: 如果图片被很频繁地访问,那么ContentProvider对象可能更适合放置缓存的图片,比如说在图片浏览器应用中。
        

        下列样例代码使用DiskLruCache实现,下列代码在内存缓存的基础上添加了硬盘缓存。

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);
}
注: 因为初始化硬盘缓存要求对硬盘进行操作,所以这个过程不应该在主线程中发生。不过,这就意味着,有机会在初始化之前获得缓存。为了解决这个问题,在上述实现中,锁对象可以确保应用在初始化之前不从硬盘缓存中读取数据。
        因为内存缓存是在主线程中检查的,而硬盘缓存是在后台线程中检查的,所以银盘操作不应该在UI线程中发生。当图片处理完成时,最终的位图会被添加到内存缓存和硬盘缓存中已备使用。


        处理配置改变
        运行时配置改变,如屏幕方向改变,会导致应用正在运行的activity销毁并根据新的配置信息重新启动(要获取更多相关信息,可以查看Handling Runtime Changes部分)。你可能想在配置改变时避免持续重复处理图片,从而获得更平稳和更快捷的用户体验。
        幸运的是,在“使用内存缓存”中,你创建了很好的位图内存缓存。 这个缓存会在调用setRetainInstance(true)时通过Fragment传递到新的Activity实例中。当新的activity重建完成后,这个保留的fragment会被附着到该activity中,并且可通过现存的缓存对象获得, 从而使图片可以被快速获取并被重新注入到ImageView对象中。

        下面是一个在配置放生改变时通过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和获得Fragment的情况下旋转设备。你会注意到当你通过内存缓存实例化图片并将图片附着到activity上时,几乎没有延迟。任何在内存缓存中找不到的图像都有望在硬盘缓存中获得,否则将做一般处理,既重新下载再缓存。