首页 > 代码库 > Caching Bitmaps

Caching Bitmaps

由于要做网络图片的显示,所以先翻译了一下官网的文档,以作备用,


地址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 ListViewGridView 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界面的流畅性。

Use a Memory Cache      用一个内存缓存

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 WeakReferencebitmap 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大小,许多因素应该被予以考虑,例如:

  • How memory intensive is the rest of your activity and/or application?

         你的Acitivity或Application都是很耗内存?

  • How many images will be on-screen at once? How many need to be available ready to come on-screen?

         多少图片立即显示在屏幕上?多少图片需要准备显示在屏幕上?

  • What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy Nexus will need a larger cache to hold the same number of images in memory compared to a device like Nexus S (hdpi).

         设备的屏幕大小和密度是多少?一个额外高度密度屏幕(xhdpi)设备,像Galaxy Nexus将要需要一个更大的缓存来维持内存中相同数量的图片与Nexus S(hdpi)设备。

  • What dimensions and configuration are the bitmaps and therefore how much memory will each take up?

          位图是什么尺寸和配置以及每张图片要占用多少内存?

  • How frequently will the images be accessed? Will some be accessed more frequently than others? If so, perhaps you may want to keep certain items always in memory or even have multiple LruCacheobjects for different groups of bitmaps.

          图片访问频繁吗,比起别的将会被更频繁地访问吗?如果是这样的话,你可能想要在内存中保持一定量的项,甚至对于不同的位图组来说有多个LRUCache对象。

  • Can you balance quality against quantity? Sometimes it can be more useful to store a larger number of lower quality bitmaps, potentially loading a higher quality version in another background task.

         你能在数量和质量之间取得平衡吗?有时对于存储更多的低质量的位图是更有用的,潜在地在其他的后台任务重加载一个更高质量的版本。

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

Use a Disk Cache   用一个硬盘缓存

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线程中,当图片处理完成时,最终位图都将添加到内存和磁盘缓存中已备将来使用。

Handle Configuration Changes   处理配置更改

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时几乎马上从内存加载,你应当注意到几乎没有滞后的感觉.任何图片都是先在内存缓存中找,没有的话再从磁盘缓存中找,如果都没有的话,就会像往常获取图片一样处理.