Android图片缓存机制(Caching Bitmaps)

来源:互联网 发布:mac打开exe文件的软件 编辑:程序博客网 时间:2024/06/05 04:17

Android加载一张图片到用户界面是很简单的,但是当一次加载多张图片时,情况就变得复杂起来。很多情况下(像ListViewGridViewViewPager等组件),屏幕上已显示的图片和即将滑动到当前屏幕上的图片数量基本上是没有限制的。

这些组件通过重用已经移除屏幕的子视图来将降低内存的使用,垃圾回收器也会及时释放那些已经不再使用的已下载的图片,这些都是很好的方法,但是为了保持一个流畅的、快速加载的用户界面,就应该避免当再次回到某个页面时而重新处理图片。内存缓存和磁盘缓存可以帮我们做到这些,它们允许组件快速地重新加载已处理好的图片。 

使用内存缓存

内存缓存允许快速地访问图片,但它以占用App宝贵的内存为代价。LruCache类(API Level 4Support Library也支持)特别适合来做图片缓存,它使用一个强引用的LinkedHashMap来保存最近使用的对象,并且会在缓存数量超出预设的大小之前移除最近最少使用的对象。

说明:以前流行的内存缓存方案是使用软引用或弱引用来缓存图片,然而现在不推荐这样做了,因为从Android 2.3(API Level 9)起,垃圾收集器更倾向于先回收软引用或弱引用,这样就使它们变得低效。另外在Android 3.0(API Level 11)之前,图片的像素数据是存储在本地内存(native memory)中的,它以一种不可预测的方式释放,因此可能会导致App超过内存限制甚至崩溃。

为了给LruCache设置一个合适的大小,以下是应该考虑的一些因素:

1.你的ActivityApp的可用内存是多少?

2.一次展示到屏幕上的图片是多少?有多少图片需要预先准备好以便随时加载到屏幕?

3.设备的屏幕尺寸和密度是多少?像Galaxy Nexus这样的高分辨率(xhdpi)设备比Nexus S这样分辨率(hdpi)的设备在缓存相同数量的图片时需要更大的缓存空间。

4.图片的尺寸和配置是怎样的?每张图片会占用多少内存?

5.图片的访问频率如何?是否有一些图片比另一些访问更加频繁?如果这样的话,或许可以将某些图片一直保存在内存里或者针对不同的图片分组设置不同的LruCache对象。

6.你能否平衡图片质量和数量之间的关系?有时候存储更多低质量的图片更加有用,当在需要的时候,再通过后台任务下载高质量的图片。

这里没有一个具体的大小和计算公式适用于所有的App,你需要分析你的使用情况并得到一个合适的方案。当一个缓存太小时会导致无益的额外的开销,而缓存太大时也可能会引起java.lang.OutOfMemory异常,另外缓存越大,留给App其他部分的内存相应就越小。

这里是一个为图片设置LruCache的示例:

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

说明:在上述例子中,我们分配了应用内存的1/8作为缓存大小,在一个normal/hdpi的设备上最少也有4MB(32/8)的大小。一个800*480分辨率的屏幕上的一个填满图片的GridView大概占用1.5MB800*480*4byte)的内存,因此该Cache至少可以缓存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);    }}

上述线程中,在解码图片之后,也需要把它添加到内存缓存中:

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这样展示大量数据的组件可以很轻易地就占满内存缓存。你的App也可能被类似电话这样的任务打断,当App被切换到后台后也可能被杀死,内存缓存也可能被销毁,一旦用户回到之前的界面,你的App依然要重新处理每个图片。

磁盘缓存可以用来辅助存储处理过的图片,当内存缓存中图片不可用时,可以从磁盘缓存中查找,从而减少加载次数。当然,从磁盘读取图片要比从内存读取慢并且读取时间是不可预期的,因此需要使用后台线程来读取。

说明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);}

说明:初始化磁盘缓存需要磁盘操作因此它不应在主线程进行,然而这意味着有可能在磁盘缓存尚未初始化之前就有访问操作发生,为了解决这个问题,在上面的实现中,使用一个锁对象来确保只有在磁盘缓存初始化之后才会从磁盘缓存读取数据。

内存缓存可以直接在UI线程读取,然而磁盘缓存必须在后台线程检查,磁盘操作不应该在UI线程发生。当图片处理完毕后,务必将最终的图片添加到内存缓存和磁盘缓存以备后续使用。


原文地址:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html ,转载请注明出处。





0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 牛仔裙老是卷边怎么办 毛边牛仔裙卷边怎么办 牛仔裤短裤卷边怎么办 牛仔裙太宽了怎么办 牛仔裙腰围小了怎么办 牛仔a字裙大了怎么办 牛仔a字裙易变形怎么办 红色皮衣染色了怎么办 羽绒服干了结块怎么办 羽绒服手洗后鸭毛堆在一起怎么办 白色羽绒服发霉了怎么办 早晨起床双脚冷怎么办 黑色棉衣粘毛怎么办 貂皮大衣旧了怎么办 pu皮床头掉皮怎么办 背心领子大怎么办鸡心 羊毛裙子缩水了怎么办 背带裙背带老掉怎么办 背带裙背带坏了怎么办 黑色衣服发红了怎么办 公司拖欠离职员工工资怎么办 windows系统坏了怎么办 背带裙肩带总掉怎么办 四个月宝宝干咳怎么办 4个多月宝宝干咳怎么办 小孩吃饭不嚼怎么办 四个月宝宝积食怎么办 新生儿睡枕头了怎么办 宝宝半夜练翻身怎么办 宝宝牙齿长歪怎么办 婴儿睡觉头歪怎么办 婴儿放在床就哭怎么办 宝宝米粉没吃完怎么办 宝宝吃米粉上火怎么办 幼儿发烧怎么办 两岁 七个月婴儿厌食怎么办 打卤时肉总是沾在一起怎么办 宝宝大便头干硬怎么办 婴儿辅食机的水怎么办 三岁宝宝牙痛怎么办 5岁宝宝牙痛怎么办