转载声明:Ryan的博客文章欢迎您的转载,但在转载的同时,请注明文章的来源出处,不胜感激! :-) html
http://my.oschina.net/ryanhoo/blog/88443
java
译者:Ryan Hoo android
来源:https://developer.android.com/develop/index.html
缓存
译者按: 在Google最新的文档中,提供了一系列含金量至关高的教程。由于种种缘由而不为人知,真是惋惜!Ryan将会细心整理,将之翻译成中文,但愿对开发者有所帮助。 app
本系列是Google关于展现大Bitmap(位图)的官方演示,能够有效的解决内存限制,更加有效的加载并显示图片,同时避免让人头疼的OOM(Out Of Memory)。
ide
------------------------------------------------------------------------------------- 测试
译文: ui
加载一个Bitmap(位图)到你的UI界面是很是简单的,可是若是你要一次加载一大批,事情就变得复杂多了。在大多数的状况下(如ListView、GridView或者ViewPager这样的组件),屏幕上的图片以及立刻要在滚动到屏幕上显示的图片的总量,在本质上是不受限制的。 this
像这样的组件在子视图移出屏幕后会进行视图回收,内存使用仍被保留。但假设你不保留任何长期存活的引用,垃圾回收器也会释放你所加载的Bitmap。这天然再好不过了,可是为了保持流畅且快速加载的UI,你要避免继续在图片回到屏幕上的时候从新处理。使用内存和硬盘缓存一般能解决这个问题,使用缓存容许组件快速加载并处理图片。
spa
这节课将带你使用内存和硬盘缓存Bitmap,以在加载多个Bitmap的时候提高UI的响应性和流畅性。
使用内存缓存
以牺牲宝贵的应用内存为代价,内存缓存提供了快速的Bitmap访问方式。LruCache类(能够在Support Library中获取并支持到API Level 4以上,即1.6版本以上)是很是适合用做缓存Bitmap任务的,它将最近被引用到的对象存储在一个强引用的LinkedHashMap中,而且在缓存超过了指定大小以后将最近不常使用的对象释放掉。
注意:之前有一个很是流行的内存缓存实现是SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,然而如今已经不推荐使用了。自Android2.3版本(API Level 9)开始,垃圾回收器更着重于对软/弱引用的回收,这使得上述的方案至关无效。此外,Android 3.0(API Level 11)以前的版本中,Bitmap的备份数据直接存储在本地内存中并以一种不可预测的方式从内存中释放,极可能短暂性的引发程序超出内存限制而崩溃。
为了给LruCache选择一个合适的大小,要考虑到不少缘由,例如:
对于设置缓存大小,并无适用于全部应用的规范,它取决于你在内存使用分析后给出的合适的解决方案。缓存空间过小并没有益处,反而会引发额外的开销,而太大了又可能再次引发java.lang.OutOfMemory异常或只留下很小的空间给应用的其余程序运行。
这里有一个设置Bitmap的LruCache示例:
private LruCache<String, Bitmap> 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<String, Bitmap>(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); }
注意:在这个例子中,1/8的应用内存被分配给缓存。在一个普通的/hdpi设备上最低也在4M左右(32/8)。一个分辨率为800*480的设备上,全屏的填满图片的GridView占用的内存约1.5M(800*480*4字节),所以这个大小的内存能够缓存2.5页左右的图片。
当加载一个Bitmap到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; } ... }
使用硬盘缓存
一个内存缓存对加速访问最近浏览过的Bitmap很是有帮助,可是你不能局限于内存中的可用图片。GridView这样有着更大的数据集的组件能够很轻易消耗掉内存缓存。你的应用有可能在执行其余任务(如打电话)的时候被打断,而且在后台的任务有可能被杀死或者缓存被释放。一旦用户从新聚焦(resume)到你的应用,你得再次处理每一张图片。
在这种状况下,硬盘缓存能够用来存储Bitmap并在图片被内存缓存释放后减少图片加载的时间(次数)。固然,从硬盘加载图片比内存要慢,而且应该在后台线程进行,由于硬盘读取的时间是不可预知的。
注意:若是访问图片的次数很是频繁,那么ContentProvider可能更适合用来存储缓存图片,例如Image Gallery这样的应用程序。
这个类中的示例代码使用DiskLruCache(来自Android源码)实现。在示例代码中,除了已有的内存缓存,还添加了硬盘缓存。
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); }
注意:即使是硬盘缓存初始化也须要硬盘操做,所以不该该在主线程执行。可是,这意味着硬盘缓存在初始化前就能被访问到。为了解决这个问题,在上面的实现中添加了一个锁对象(lock object),以确保在缓存被初始化以前应用没法访问硬盘缓存。
在UI线程中检查内存缓存,相应的硬盘缓存检查应在后台线程中进行。硬盘操做永远不要在UI线程中发生。当图片处理完成后,最终的Bitmap要被添加到内存缓存和硬盘缓存中,以便后续的使用。
处理配置更改
运行时的配置会发生变化,例如屏幕方向的改变,会致使Android销毁并以新的配置从新启动Activity(关于此问题的更多信息,请参阅Handling Runtime Changes)。为了让用户有着流畅而快速的体验,你须要在配置发生改变的时候避免再次处理全部的图片。
幸运的是,你在“使用内存缓存”一节中为Bitmap构造了很好的内存缓存。这些内存能够经过使用Fragment传递到信的Activity(活动)实例,这个Fragment能够调用setRetainInstance(true)方法保留下来。在Activity(活动)被从新建立后,你能够在上面的Fragment中访问到已经存在的缓存对象,使得图片能快加载并从新填充到ImageView对象中。
下面是一个使用Fragment将LruCache对象保留在配置更改中的示例:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache = RetainFragment.mRetainedCache; if (mMemoryCache == null) { mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } mRetainFragment.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(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }为了测试这个,能够在不适用Fragment的状况下旋转设备屏幕。在保留缓存的状况下,你应该能发现填充图片到Activity中几乎是瞬间从内存中取出而没有任何延迟的感受。任何图片优先从内存缓存获取,没有的话再到硬盘缓存中找,若是都没有,那就以普通方式加载图片。