使用内存缓存
使用内存缓存可以快速的拿到bitmap,但是是以占用应用可用内存为代价的。LruCache类就是为了实现bitmap的缓存的,它把最近被引用的对象存储在了一个强引用类型的LinkedHashMap中,在缓存大小超过它设定的值的时候就会赶出尽量少的最近使用的对象。
注意:在以前,一个比较受欢迎的内存缓存实现是SoftReference或者WeakReference bitmap缓存,然而并不建议这么做。从Android2.3(API 9)开始,android虚拟机的垃圾收集器变得更加的具有侵略性,极有可能会回收掉soft/weak的引用,这可能就会使使用soft/weak的缓存策略失效。而且,在android3.0(API
11)之前,bitmap的数据时存储在本地内存中的,这种内存的释放是不可预期的不可控的,这样就有可能造成内存达到阀值从而崩溃。
为了选择LruCache的合适大小,下面是几个需要考虑的因素:
-有多少图片会一次性的展示在屏幕上?有多少需要在稍后被还原到屏幕上?
-设备的屏幕大小的密度。xhdpi的密度比hdpi的设备需要更多的缓存空间大小。
-图片的尺寸和配置信息,每张图片会占用多少内存
-图片被使用的频率。是否有其中一些图片比其他的图片使用频率更高?如果这样的话,你可能要把这些图片始终放在内存中,甚至为不同的图片组使用多个LruCache对象。
-你能平衡好数量和质量么?有的时候可能存储大量的低质量的图片更加有用,而在后台默默的加载对应图片的高质量版本。
没有适合于所有应用的指定大小或者计算公式。全都依靠你自己的应用的内存使用情况,并自己给出合适的方案。太小不会有明显的效果,太大可能会导致OutOfMemory异常。
下面是一个例子:
private LruCache<String, Bitmap> mMemoryCache; @Overrideprotected 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.这里除以1024是因为maxMemory()方法返回的是字节数,而LruCache的构造函数是以KB作为最大值的单位
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);}
注意:在这个例子中,八分之一的应用内存分配给了我们的缓存。在hdpi的设备上,这大概是4MB左右的大小。一个480*800设备上的一个全屏的铺满图片的GridView大概会使用1.5(480*800*4 bytes)MB,所以这个能够缓存大概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这样的拥有大量数据的组件可能很容易就会填满内存缓存。你的应用也有可能被来电打断,或者在后台运行的时候被kill掉,这个时候如果用户重新进入到应用中,你的应用还要重新处理每一个图片。
磁盘缓存这时候就有用了,它可以帮助减少图片加载的次数(先到内存中找有没有有缓存,没有的话再到磁盘中找,再没有的话则请求网络数据)。当然,从磁盘中加载数据比从内存中加载要慢,并且要在后台线程中进行,因为磁盘的读取时间是不可预期的。
注意:如果缓存的图片频繁的被访问,ContentProvider可能是一个储存缓存图片的更好的选择,例如图片浏览器(Image Gallery)应用。
下面的实例代码是从android源码中抽取的一段DiskLruCache的代码。这里除了已经存在的内存缓存之外,还添加了一个磁盘缓存:
private DiskLruCache mDiskLruCache;private final Object mDiskCacheLock = new Object();private boolean mDiskCacheStarting = true;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MBprivate static final String DISK_CACHE_SUBDIR = "thumbnails"; @Overrideprotected 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);}