地址http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html。
Loading a single bitmap into your user interface (UI) is straightforward, however things get more complicated if you need to load a larger set of images at once. In many cases (such as with components like ListView
, GridView
orViewPager
), the total number of images on-screen combined with images that might soon scroll onto the screen are essentially unlimited.
加载单个位图到你的UI界面是很简单的,然而你需要马上加载一组更大的图片的话就复杂的多,在许多情况下(有些组件像ListView,GridView以及ViewPager),出现在屏幕上的图片总量,其中包括可能马上要滚动出屏幕上的那些图片,实际上是无限的。
Memory usage is kept down with components like this by recycling the child views as they move off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI you want to avoid continually processing these images each time they come back on-screen. A memory and disk cache can often help here, allowing components to quickly reload processed images.
那些通过回收即将移除屏幕的子视图组件,来使内存保留,如果你不长期保持你对象的引用的话,垃圾回收器也会释放你加载的位图内存。但为了保持流畅,快速加载,并且你想避免它们每次出现在屏幕上时重复加载处理这些图片的UI的话,这最好不过了。一个内存和磁盘的缓存通常能够解决这个问题,允许组件快速地重新处理图片。
This lesson walks you through using a memory and disk bitmap cache to improve the responsiveness and fluidity of your UI when loading multiple bitmaps.
这课告诉你当你加载多个位图时通过使用一个内存和磁盘的位图缓存来提高相应速度以及提升整个UI界面的流畅性。
A memory cache offers fast access to bitmaps at the cost of taking up valuable application memory. The LruCache
class (also available in the Support Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced LinkedHashMap
and evicting the least recently used member before the cache exceeds its designated size.
在占用宝贵的应用内存情况下,内存缓存提供了可以快速访问位图。LruCache类(也可以使用V4库)特别适合于缓存位图的任务,保持最近引用的对象在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存。
Note: In the past, a popular memory cache implementation was a SoftReference
or WeakReference
bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash.
注意:在过去,一个常用的内存缓存实现是一个SoftReference或WeakReference的位图缓存,然而现在不推荐使用,从Android2.3(API级别9)开始,垃圾回收器更加注重于软/弱引用,这使得使用以上引用很大程度上无效,此外,之前的Android3.0(API级别11),位图的备份被存储本地那些在一种可预测的情况下没有被释放的内存中,很有可能导致应用程序内存溢出和崩溃。
In order to choose a suitable size for a LruCache
, a number of factors should be taken into consideration, for example:
为了选择一个合适的LruCache大小,许多因素应该被予以考虑,例如:
你的Acitivity或Application都是很耗内存?
多少图片立即显示在屏幕上?多少图片需要准备显示在屏幕上?
设备的屏幕大小和密度是多少?一个额外高度密度屏幕(xhdpi)设备,像Galaxy Nexus将要需要一个更大的缓存来维持内存中相同数量的图片与Nexus S(hdpi)设备。
位图是什么尺寸和配置以及每张图片要占用多少内存?
LruCache
objects for different groups of bitmaps. 图片访问频繁吗,比起别的将会被更频繁地访问吗?如果是这样的话,你可能想要在内存中保持一定量的项,甚至对于不同的位图组来说有多个LRUCache对象。
你能在数量和质量之间取得平衡吗?有时对于存储更多的低质量的位图是更有用的,潜在地在其他的后台任务重加载一个更高质量的版本。
There is no specific size or formula that suits all applications, it's up to you to analyze your usage and come up with a suitable solution. A cache that is too small causes additional overhead with no benefit, a cache that is too large can once again cause java.lang.OutOfMemory
exceptions and leave the rest of your app little memory to work with.
没有具体的大小及合适所有应用的公式,它是由你来分析你的情况,并拿出一个合适的解决方案。缓存太小会导致额外的没有益处的开销,缓存太大导致内存溢出异常,并且只保留下你的应用程序其余相当少的内存来运行你的应用程序。
Here’s an example of setting up a LruCache
for bitmaps:
这个例子是针对位图来设置一个LruCache:
<span style="font-size:14px;"><span style="font-size:14px;">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. //bitmap.getByteCount()需要API为12,但是查看源码可以看到bitmap.getByteCount() = bitmap.getRowBytes() * bitmap.getHeight(),而bitmap.getRowBytes() * bitmap.getHeight()在API8就可以使用,所以对于兼容低版本的时候我们可以使用bitmap.getRowBytes() * bitmap.getHeight()替换bitmap.getByteCount() 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); }</span></span>
Note: In this example, one eighth of the application memory is allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full screen GridView
filled with images on a device with 800x480 resolution would use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in memory.
注意:在这个例子中,应用程序中八分之一的内存被用作缓存,在一个noraml/hdpi设备中最低有4MB。在一个800*480分辨率的设备上全屏显示一个充满图片的GridView控件视图将使用1.5MB左右的缓存,所以这将在内存中缓冲至少四分之一的图片。
When loading a bitmap into an ImageView
, the LruCache
is checked first. If an entry is found, it is used immediately to update the ImageView
, otherwise a background thread is spawned to process the image:
当将一张位图加载进一个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); } }
The BitmapWorkerTask
also needs to be updated to add entries to the memory cache:
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;
}
...
}
A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot rely on images being available in this cache. Components like GridView
with larger datasets can easily fill up a memory cache. Your application could be interrupted by another task like a phone call, and while in the background it might be killed and the memory cache destroyed. Once the user resumes, your application has to process each image again.
一个内存缓存对于加快访问最近浏览过的位图是很有用的,然而你不能局限图片在缓存中可用,像GridView这种具有更大的数据集的组件很容易填满内存,你的应用程序会被其他的任务像打电话等打断,并且当运行在后台时可能被进程杀死以及内存缓存被回收,一旦用户重新打开,你的应用程序必须重新处理每一张图片。
A disk cache can be used in these cases to persist processed bitmaps and help decrease loading times where images are no longer available in a memory cache. Of course, fetching images from disk is slower than loading from memory and should be done in a background thread, as disk read times can be unpredictable.
在这种情况下使用磁盘缓存来持续处理位图,并且有助于在图片在内存缓存中不再可以用时缩短加载时间,当然,从磁盘获取图片比从内存加载更慢并且应当在后台线程中处理,因为磁盘读取的时间是不可预知的。
Note: A ContentProvider
might be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.
注意:如果它们被更频繁地访问,那么一个ContentProvider可能是一个更合适来存储缓存中的图片,例如在一个图片库应用程序中。
The sample code of this class uses a DiskLruCache
implementation that is pulled from the Android source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:
包括在这一类的实例代码是一个基本DiskLruCache实现,然而,一个更强大和推荐的DiskLruCache解决方案包括在Android4.0源代码(libcore/luni/src/main/java/libcore/io/DiskLruCache.java).这里是个更新的示例代码,包括在这一类示例应用程序中使用简单的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);
}
Note: Even initializing the disk cache requires disk operations and therefore should not take place on the main thread. However, this does mean there's a chance the cache is accessed before initialization. To address this, in the above implementation, a lock object ensures that the app does not read from the disk cache until the cache has been initialized.
注意:初始化磁盘缓存需要磁盘操作,因此不应该发生在主线程。但是这并不意味着有缓存被初始化之前访问的机会。为了解决这个问题,在上述实施方式中,锁定对象可以确保应用程序不从磁盘上读取缓存,直到缓存已经初始化。
While the memory cache is checked in the UI thread, the disk cache is checked in the background thread. Disk operations should never take place on the UI thread. When image processing is complete, the final bitmap is added to both the memory and disk cache for future use.
当内存缓存在UI线程中被检查时,磁盘缓存在后台线程中被检测,磁盘操作应当永远不会发生在UI线程中,当图片处理完成时,最终位图都将添加到内存和磁盘缓存中已备将来使用。
Runtime configuration changes, such as a screen orientation change, cause Android to destroy and restart the running activity with the new configuration (For more information about this behavior, see Handling Runtime Changes). You want to avoid having to process all your images again so the user has a smooth and fast experience when a configuration change occurs.
程序运行时配置改变,例如屏幕的方向改变,导致系统销毁活动并且采用新的配置重新运行活动(有关此问题的更多信息,请参考Handling Runtime Changes),你想要避免不得不再次处理所有的图片以使用户在配置发生改变时有一个平稳和快速的体验。
Luckily, you have a nice memory cache of bitmaps that you built in the Use a Memory Cache section. This cache can be passed through to the new activity instance using a Fragment
which is preserved by calling setRetainInstance(true)
). After the activity has been recreated, this retainedFragment
is reattached and you gain access to the existing cache object, allowing images to be quickly fetched and re-populated into the ImageView
objects.
幸运地是,在使用一个内存缓存一节,你有一个不错的自己所构造的位图内存缓冲.缓存能通过新的活动实例来使用一个Fragment,这个Fragment是通过调用setRetainInstance(true)方法被保留的.活动被重新构造后,保留的片段重新连接,并且你获得现有的高速缓存对象的访问权限,使得图片能快速的加载并重新填充到ImageView对象中.
Here’s an example of retaining a LruCache
object across configuration changes using a Fragment
:
接下来是一个使用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);
}
}
To test this out, try rotating a device both with and without retaining the Fragment
. You should notice little to no lag as the images populate the activity almost instantly from memory when you retain the cache. Any images not found in the memory cache are hopefully available in the disk cache, if not, they are processed as usual.
为了测试这一点,尝试在不使用Fragment的情况下旋转设备和不旋转设备.在你保留缓存的情况下,由于图片填充到activity时几乎马上从内存加载,你应当注意到几乎没有滞后的感觉.任何图片都是先在内存缓存中找,没有的话再从磁盘缓存中找,如果都没有的话,就会像往常获取图片一样处理.