图片的高效加载(三)缓存

转自:https://my.oschina.net/rengwuxian/blog/184650

摘要: 假设你开发了一个聊天程序,它的好友列表中显示从网络获取的好友头像。可是如果用户发现每次进入好友列表的时候,程序都要重新下载头像才能进行显示,甚至当把列表滑动到底部再重新滑动回顶部的时候,刚才已经加载完成了的头像竟然又变成了空白图片开始重新加载,这将是一种糟糕的用户体验。为了解决这种问题,你需要使用高速缓存技术——Cache。

应用的场景

假设你开发了一个聊天程序,它的好友列表中显示从网络获取的好友头像。可是如果用户发现每次进入好友列表的时候,程序都要重新下载头像才能进行显示,甚至当把列表滑动到底部再重新滑动回顶部的时候,刚才已经加载完成了的头像竟然又变成了空白图片开始重新加载,这将是一种糟糕的用户体验。为了解决这种问题,你需要使用高速缓存技术——Cache。

什么是Cache?

Cache,高速缓存,原意是指计算机中一块比内存更高速容量更小的存储器。更广义地说,Cache指对于最近使用过的信息的可高速读取的存储块。而本文要讲的Cache技术,指的就是将最近使用过的Bitmap缓存在手机的内存与磁盘中,来实现再次使用Bitmap时的瞬时加载,以节省用户的时间和手机流量。

下面将针对Android中的两种Cache类型Memory Cache和Disk Cache分别进行介绍。样例代码取自Android开发者站

1/2:Memory Cache内存中的Cache

Memory Cache使用内存来为应用程序提供Cache。由于内存的读写速度非常快,所以我们应该优先使用它(相对于下面将介绍的Disk Cache来说)。

Android中提供了LruCache类来进行Memory Cache的管理(该类是在Android 3.1时推出的,但我们可以使用android -support-v4.jar的兼容包来对低版本的手机提供支持)。

提示:有人习惯使用SoftReference和WeakReference来做Memory Cache,但谷歌官方不建议这么做。因为自从Android2.3之后,Android中的GC变得更加积极,导致这种做法中缓存的Bitmaps非常容易被回收掉;另外,在Android3.0之前,Bitmap的数据是直接分配在native memory中,它的释放是不受dalvik控制的,因此更容易导致内存的溢出。如果你喜欢简单粗暴的总结,那就是:反正不要用这种方法来管理Memory Cache。

下面我们看一段为Bitmap设置LruCache的代码

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 获取虚拟机可用内存(内存占用超过该值的时候,将报OOM异常导致程序崩溃)。最后除以1024是为了以kb为单位
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 使用可用内存的1/8来作为Memory Cache
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 重写sizeOf()方法,使用Bitmap占用内存的kb数作为LruCache的size
            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来提供给Memory Cache,我们简单分析一下这个值。一个普通屏幕尺寸、hdpi的手机的可用内存为32M,那么他的Memory Cache为32M/8=4M。通常hdpi的手机为480*800像素,它一个全屏Bitmap占用内存为480*800*4B=1536400B≈1.5M。那么4M的内存为大约2.5个屏幕大小的bitmap提供缓存。同理,一个普通尺寸、xhdpi大小的720*1280的手机可以为大约2.2个屏幕大小的bitmap提供缓存。

当一个ImageView需要设置一个bitmap的时候,LruCache会进行检查,如果它已经缓存了相应的bitmap,它就直接取出来并设置给这个ImageView;否则,他将启动一个后台线程加载这个Bitmap

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在加载完成后,通过前面的addBitmapToMemoryCache()方法把这个bitmap进行缓存:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 后台加载Bitmap
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

2/2:Disk Cache(磁盘中的Cache)

前面已经提到,Memory Cache的优点是读写非常快。但它的缺点就是容量太小了,而且不能持久化,所以在用户在滑动GridView时它很快会被用完,而且切换多个界面时或者是关闭程序重新打开后,再次进入原来的界面,Memory Cache是无能为力的。这个时候,我们就要用到Disk Cache了。

Disk Cache将缓存的数据放在磁盘中,因此不论用户是频繁切换界面,还是关闭程序,Disk Cache是不会消失的。

实际上,Android SDK中并没有一个类来实现Disk Cache这样的功能。但google其实已经提供了实现代码:DiskLruCache。我们只要把它搬到自己的项目中就可以了。

下面请看一段使用DiskLruCache来配合Memory Cache进行图片缓存的代码

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) {
    ...
    // 初始化memory cache
    ...
    // 开启后台线程初始化disk cache
    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; // 初始化完成
            mDiskCacheLock.notifyAll(); // 唤醒被hold住的线程
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在后台加载图片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // 通过后台线程检查disk cache
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // 如果没有在disk cache中发现这个bitmap
            // 加载这个bitmap
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 把这个bitmap加入cache
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // 把bitmap加入memory cache
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // 同样,也加入disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // 等待disk cache初始化完毕
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// 在自带的cache目录下建立一个独立的子目录。优先使用外置存储。但如果外置存储不存在,使用内置存储。
public static File getDiskCacheDir(Context context, String uniqueName) {
    // 如果MEDIA目录已经挂载或者外置存储是手机自带的(Nexus设备都这么干),使用外置存储;否则使用内置存储
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

 提示:由于disk cache的初始化是耗时操作,所以这个过程被放在了后台进程。而由此导致的结果是,主线程有可能在它初始化完成之前就尝试读取disk cache,这会导致程序出错。因此以上代码中使用了synchronized关键字和一个lock对象来确保在初始化完成之前disk cache不会被访问。(什么是synchronized?文章最后会有介绍)

上面这段代码看起来比较多,但大致读一下就会发现,它的思路非常简单:1.读取cache的时候,优先读取memory cache,读不到的时候再读取disk cache;2.把bitmap保存到cache中的时候,memory cache和disk cache都要保存。

至此,使用Cache来缓存Bitmap的方法就介绍完了。把这套思路使用在你的项目中,用户体验会马上大大增强的。

 

 

posted @ 2016-11-24 10:04  Lammy  阅读(2502)  评论(0编辑  收藏  举报