缓存图片

原创
2014/11/26 17:28
阅读数 21

原文地址:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

缓存图片

        加载一个图片到界面中是很简单的,但是如果一次性要加载一堆大的图片就变的复杂多了。在许多情况下(比如使用ListView、GridView或是ViewPager组件)界面上能显示的图片随着滚动会无限制的增加。

        缓存的使用需要被控制当组件在不断的重复使用的时候。如果你不长时间引用加载来的图片的话,垃圾搜集器也会清除他。为了流畅快速的加载界面,不希望组件每次显示的时候都重新加载一次图片。利用缓存和磁盘缓存就能够快速加载图片。这一课就是介绍如果通过缓存和磁盘缓存快速加载多张图片。

使用内存缓存

内存缓存通过占用应用固定的内存达到快速读取缓存。LruCache类能很好的解决图片缓存占用问题,保证最近使用的资源强引用在LinkedHashMap中,删除最近最少使用的资源。

过去一种非常流行的缓存实现是通过SoftReference或是WeakReference存储图片缓存,然而这种方法不再推荐。从2.3开始垃圾收集器会更加积极的搜集soft/weak的应用这将使得他们无效了。在3.0以后,返回的图片数据存储在一个私有的内存中,以一种不可预见的方式释放,可能导致应用暂时超过其内存限制而奔溃。

为了给LruCache选中一个适合的大小,有一些因素需要考虑:

1、应用剩下多少的内存

2、屏幕一次要展示多少图片,需要多少图片准备用于屏幕展示

3、设备的屏幕大小和分辨率是多少,对于高分辨率设别(xhdpi)像Galaxy Nexus比hdpi的设备需要更大的缓存来存储图片

4、图片的分辨率和配置和其占用的缓存

5、图片存储是否频繁,是否有其他更频繁的,如果有要确保其内存,甚至可以使用多个LruCache对象处理不同组的图片

6、能否平衡质量和数量,大多数情况下存储更多低质量的图片比存储高质量的图片来的重要。

对于所有应用来说没有专门的大小或公式。取决于你使用的解决方案。缓存太小会导致而外开销,缓存太大会导致内存溢出。

下面是一个设置图片LruCache缓存的例子:

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.
            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的内存分配给缓存,一个普通的hdpi设备的最小缓存在4MB左右(32/8),一个全屏的GridView在800x480的设备中大概占用1.5MB(800x480x4bytes),因此能缓存的最少页面的2.5个页面的缓存。当加载图片在ImageView中时先从LruCache中获取

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

使用磁盘缓存

内存的使用对于控件快速加载图片来说是非常有用的,然而不能指望所有图片都存储在内存中,像GridView这样的组件加载大量的数据能够很轻松的占满缓存。应用会被其他任务所打断比如电话,当其在后台时,有可能被杀死,缓存被销毁。一旦返回应用时,又必须重新加载。

这种情况下使用磁盘缓存加载不再内存中的图片能减少加载时间。当然从磁盘获取图片是缓慢的不确定的,需要在后台线程中执行。

ContentProvider可能是一个更合适的地方来存储缓存图片如果他们更频繁地访问,例如在一个图片库应用程序。

下面是使用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);
}

即使是初始化磁盘缓存的任务都不应该占用UI线程。但并不意味着读取缓存会在初始化之前,通过锁来控制其他任务需要在初始化完成后才能执行。

内存的获取在UI线程,磁盘的获取在后台线程,磁盘的操作不能占用UI线程,当图片加载完成后,图片会存储在内存和磁盘中供以后使用。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部