如果我们要不止一次去加载很多的图片,像在ListView、GridView、ViewPager等控件中加载图片。本篇将为大家介绍怎样在在内存和磁盘中缓存图片。

内存缓存(Memory Cache)

内存缓存可以让我们快速的访问图片,我们通常使用LruCache类来进行内存缓存,它可以将一个最近应用的对象,保存到一个强引用的LinkedHashMap中,并且当内存不够的时候,及时的去释放掉最近使用的对象。

拓展:在以前的Android应用中,通常用 SoftReference(软引用) 或者WeakReference(弱引用)去实现图片的内存缓存,但是谷歌不鼓励开发者这么去做,因为在Android2.3(api 9)开始,垃圾回收机制更加倾向于去回收SoftReference(软引用) 或者WeakReference(弱引用),这使得他们的效率非常低。

如果为LruCache去设置一个合适大小来缓存图片,需要考虑下面几个因素:

  • 你的应用程序的其他功能(即除了缓存图片以为操作)对你内存的消耗。
  • 你的设备的屏幕尺寸和dpi的大小。如果你的设备的dpi或屏幕尺寸比较大,那么需要更多的缓存。
  • 你要缓存的图片平均占用的内存的大小。
  • 图片被访问的频率。对于访问频率高的图片,要保持其一直在内存中,必要时,创建多个LruCache对象来对不同的访问频率的图片进行分组缓存。
  • 对图片质量和质量作一个最佳的平衡尺度,我们可以去缓存大量低质量的图片,先展示给用户,然后在后台开启任务去加载高质量的图片。

也许,有人会问,有没有一个中和一点的数值作为内存缓存的缓存大小,谷歌说(谷歌说的,不是我说的),没有,你自己分析自己的应用程序后来定, 尺寸小了,导致缓存效率低,尺寸大了,容易导致OOM异常。

下面代码为如何去为bitmap设置一个LruCache:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 获取虚拟机的最大可用内存,注意:如果占用的内存超过这个数值,就会导致OOM异常。
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 这里使用内最大可用内存的八分之一 作为图片的内存缓存大小
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 返回要缓存图片大小
            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);
}

在上面的例子中,内存缓存的大小设置为了可用内存的八分之一。

作了内存缓存后,我们再次去加载图片,这时,首先去LruCache中获取,如果再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;
    }
    ...
}
**小结: **

内存缓存对于获取最近访问的图片有非常高的效率,但是,我们的应用中的图片却不能仅仅依赖于内存缓存。 一些控件比如GridView通常数据量比较大,可以轻易的就把你的内存缓存的空间所占满,获取你的应用可能被其他任务打断,比如来电,此时,你的应用在后台可能会被杀死,或者内存缓存会被销毁,一旦用户再次获得焦点,就会重新去完整的加载图片。

此时,我们可以使用磁盘缓存来缓存图片。从磁盘中获取图片要比内存获取慢,而且需要在后台线程中完成,因为从磁盘中获取数据耗费的时间不可预测。

磁盘缓存(Disk Cache)

下面是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) {
    ...
    // 初始化内存缓存
    ...
    // 在后台线程中初始化磁盘缓存
    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> {
    ...
    // 解析图片.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // 在后台线程中检查磁盘缓存
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 添加图片到缓存中
        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;
}

// 创建一个文件在磁盘中,如果SD卡可用,在SD卡中创建,如果不可用,则在内置磁盘中创建
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);
}


在初始化磁盘的时候需要对磁盘进行操作,这些操作需要在子线程中进行,但是,在初始化磁盘之前,缓存也能被访问到。为了解决这个问题,在上面的代码中,使用锁来确保应用程序在缓存空间被初始化之前,不去读取磁盘。

界面配置发生变化时的处理

当我们程序在运行时,配置发生了改变,诸如屏幕旋转或者语言的切换,导致当前界面被销毁,并且使用新的配置重新启动正在运行的界面,此时,为了很好的用户体验,我们不想自己的图片再次被去加载,无论是从内存或者磁盘,甚至于图片的源头。我们应该怎么做呢。

我们在应用内存缓存的时候,使用了LruCache,这个类在Activity被销毁的时候可以在通过调用**setRetainInstance(true)**来保存这个内存缓存,然后通过一个Fragment将这个值传递给新创建的Activity.

以上过程的示例代码如下:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... //TODO 获取到缓存数据后,把数据恢复到ui界面上
        }
        retainFragment.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();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 在界面销毁时,保存数据
        setRetainInstance(true);
    }
}