原文链接:http://docs.eoeandroid.com/training/displaying-bitmaps/cache-bitmap.html
负责人:hepengcheng.cn
完成时间:9月13日
目录[隐藏]
|
加载一副位图到你的用户界面是很简单的,然而如果你需要马上加载一组更大的图片的话就会复杂的多.在许多情况下(例如有些组件像ListView,GridView以及ViewPager等),出现在屏幕上的图片总量,其中包括可能马上要滚动显示在屏幕上的那些图片,实际上是无限的.
那些通过回收即将移除屏幕的子视图的组件,内存使用得以保留.如果你不长期保持你对象的引用的话,垃圾收集器也会释放你所加载的位图内存.但为了保持一个流畅,快速加载,并且你想避免它们每次出现在屏幕上时重复加载处理这些图片的UI的话,这最好不过了.一个内存和磁盘的缓存通常能解决这个问题,允许组件快速地重新处理图片.
这个要点教你当加载多个位图时使用一个内存和磁盘的位图缓存来提高响应速度以及提升整个UI界面的流畅性.
在占用宝贵的应用程序内存情况下,内存缓冲提供了可以快速访问位图.LruCache类(也可以使用API级别4的Support Library)特别适合用于缓存位图的任务,最近被引用的对象保存在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存.
注意:在过去,一个常用的内存缓存实现是一个SoftReference或WeakReference的位图缓存,然而现在不推荐使用.从android2.3(API 级别9)开始,垃圾回收器更加注重于回收软/弱引用,这使得使用以上引用很大程度上无效.此外,之前的android3.0(API级别11),位图的备份数据存储在本地那些在一种可预测的情况下没有被释放的内存中,很有可能会导致应用程序内存溢出和崩溃.
为了选择一个合适的的LruCache大小,许多因素应当被予以考虑,例如:
没有具体的大小以及适合所有应用程序的公式,它是由你来分析您的使用情况,并拿出一个合适的解决方案.缓存太小会导致额外的没有益处的开销,缓存过大会再次导致java.lang.OutOfMemory异常,并且只保留下你的应用程序其余相当少的内存来运行你的应用程序.这个例子是针对位图来设置一个LruCache:
private LruCache mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get memory class of this device, exceeding this amount will throw an // OutOfMemory exception. final int memClass = ((ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE)).getMemoryClass(); // Use 1/8th of the available memory for this memory cache. final int cacheSize = 1024 * 1024 * memClass / 8; mMemoryCache = new LruCache(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in bytes rather than number of items. return bitmap.getByteCount(); } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
当将一张位图加载进一个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 { ... // 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实现.然而,一个更强大和推荐的DiskLruCache解决方案包括在Android4.0源代码(libcore/luni/src/main/java/libcore/io/DiskLruCache.java). 返回移植之前的Android版本上使用这个类的,应该是相当简单(快速搜索已经实现这个解决方案).这里是个更新的示例代码,包括在这一类示例应用程序中使用简单的DiskLruCache:
private DiskLruCache mDiskCache; 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 ... File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); ... } class BitmapWorkerTask extends AsyncTask { ... // 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(String.valueOf(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 if (!mDiskCache.containsKey(key)) { mDiskCache.put(key, bitmap); } } public Bitmap getBitmapFromDiskCache(String key) { return mDiskCache.get(key); } // 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 getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Environment.isExternalStorageRemovable() ? context.getExternalCacheDir().getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
当内存缓存在UI线程中被检查时,磁盘缓存在后台线程中被检查.磁盘操作应当永远不会发生在UI线程中.当图片处理完成时,最终的位图都将被添加到内存和磁盘缓存中已被将来时候.
程序运行时配置改变,例如屏幕的方向改变,导致系统销毁活动并且采用新的配置重新运行活动(有关此问题的更多信息,请参见Handling Runtime Changes).你想要避免不得不再次处理所有的图片以使用户在配置发生改变时有一个平稳和快速的体验.
幸运地是,在使用一个内存缓存一节,你有一个不错的自己所构造的位图内存缓冲.缓存能通过新的活动实例来使用一个Fragment,这个Fragment是通过调用setRetainInstance(true)方法被保留的.活动被重新构造后,保留的片段重新连接,并且你获得现有的高速缓存对象的访问权限,使得图片能快速的加载并重新填充到ImageView对象中.
接下来是一个使用Fragment将一个LruCache对象保留在配置更改中的范例:
private LruCache mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache = RetainFragment.mRetainedCache; if (mMemoryCache == null) { mMemoryCache = new LruCache(cacheSize) { ... // Initialize cache here as usual } mRetainFragment.mRetainedCache = mMemoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache mRetainedCache; public RetainFragment() {} public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) { fragment = new RetainFragment(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
为了测试这一点,尝试在不使用Fragment的情况下旋转设备和不旋转设备.在你保留缓存的情况下,由于图片填充到activity时几乎马上从内存加载,你应当注意到几乎没有滞后的感觉.任何图片都是先在内存缓存中找,没有的话再从磁盘缓存中找,如果都没有的话,就会像往常获取图片一样处理.