高效使用Bitmaps(三) 神奇的Cache

应用的场景 html

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

什么是Cache? android

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

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

1/2:Memory Cache内存中的Cache 并发

Memory Cache使用内存来为应用程序提供Cache。因为内存的读写速度很是快,因此咱们应该优先使用它(相对于下面将介绍的Disk Cache来讲)。 ide

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

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

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

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的方法就介绍完了。把这套思路使用在你的项目中,用户体验会立刻大大加强的。


延伸:什么是synchronized?

概念:为了防止多个后台并发线程同时对同一个对象进行写操做时发成错误,java使用synchronized关键字对一个对象“加锁”,以保证同时只有一个线程能够访问该对象。

举个例子:快过年了,咱俩去火车站买回家的火车票,我在1号窗口,你在2号窗口,而且咱俩同时排队到了窗户跟前。巧的是,咱俩买的是同一趟车,而这趟车如今只剩一张票了。而后咱俩都跟售票员说:就这张了,买!因而两个售票员同时点击了电脑上的“出票”按钮。后台系统接到两个请求,两个线程同时进行处理,执行了这么两行代码:

if (tickedCount > 0) { // 若是还有票
    tickedCount -= 1; // 票数减一
    printTicket(); // 出票
}

线程1和线程2几乎同时运行,而且几乎同时执行到第一行代码,线程1一看,哦还有票,行,出票吧!而后执行了第二行代码,票数减一。但它不知道,在他执行第二行代码以前,线程2也执行到了第一行,这线程2也一看,哦还有票,行,出票吧!因而在线程1出票以后,线程2在已经没票的状况下依然把票数减到了-1,而且执行printTicket()方法尝试出票。到了这里,程序究竟是会报错仍是会出两张同样的票已经不重要,重要的是:系统出问题了,它作了不应作的事。

那么怎么解决呢?很简单,加锁:

synchronized(this) {
    if (tickedCount > 0) { // 若是还有票
        tickedCount -= 1; // 票数减一
        printTicket(); // 出票
    }
}
上面这段代码因为加了锁,致使同一时间只有一个线程能够进入这个代码块,当一个线程进入后,其余线程必须等这个线程执行完这段代码后释放了锁,才能进入这个代码块。这样,同时出同一张票的bug就不可能出现了。固然,我只是举例,上面的代码只是一个简化模型。

因为篇幅限制,没法详细地介绍synchronized的更多性质和使用方法,若是有兴趣能够本身查找相关资料。

相关文章
相关标签/搜索