文档章节

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

扔物线
 扔物线
发布于 2013/12/16 01:00
字数 2574
阅读 7467
收藏 247
点赞 33
评论 12

应用的场景

假设你开发了一个聊天程序,它的好友列表中显示从网络获取的好友头像。可是如果用户发现每次进入好友列表的时候,程序都要重新下载头像才能进行显示,甚至当把列表滑动到底部再重新滑动回顶部的时候,刚才已经加载完成了的头像竟然又变成了空白图片开始重新加载,这将是一种糟糕的用户体验。为了解决这种问题,你需要使用高速缓存技术——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的方法就介绍完了。把这套思路使用在你的项目中,用户体验会马上大大增强的。


延伸:什么是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的更多性质和使用方法,如果有兴趣可以自己查找相关资料。

© 著作权归作者所有

共有 人打赏支持
扔物线
粉丝 82
博文 6
码字总数 9765
作品 1
海淀
高级程序员
加载中

评论(12)

扔物线
扔物线

引用来自“RyanHoo”的评论

写博客把出处标注出来,哪怕是官方文档,何况你这80%的翻译+20%的原创。围着叫好的多半没撸过android trainning docs.

有道理!不过我说了样例代码取自Android开发者站,你的意思是说我应该把地址贴在这句话后面?
乌龟壳
乌龟壳

引用来自“starstroll”的评论

引用来自“郭煜”的评论

这东西为什么要开发应用的程序员来折腾呢?

不折腾一下就只能开发应用了,每个人想走的路都不一样。

我想表达的是这个基本的东西Android没提供吗?
RyanHoo
RyanHoo
写博客把出处标注出来,哪怕是官方文档,何况你这80%的翻译+20%的原创。围着叫好的多半没撸过android trainning docs.
starstroll
starstroll

引用来自“郭煜”的评论

这东西为什么要开发应用的程序员来折腾呢?

不折腾一下就只能开发应用了,每个人想走的路都不一样。
乌龟壳
乌龟壳
这东西为什么要开发应用的程序员来折腾呢?
sharpcx
sharpcx
学很多东西
猪的媳妇
学习机良多
吴晓涵
吴晓涵
谢谢LZ分享,学习啦
zgldh
zgldh
good
-Jacen-
-Jacen-
不明觉厉。
LruMemoryCache和LruCache

LruMemoryCache和LruCache 在UniversalImageLoader中缓存在内存中的数据就使用了LruCache类,叫LruMemoryCache,存在内存中的图片就是放在该类中了,想想早年我们用软引用的方式保存,也是弱...

程序猿骆二胖
2015/12/04
139
0
Glide 3.5.0 发布,Android 图片加载和缓存库

Glide 3.5.0 发布,此版本是增量版本,包括一些新特性和重要的 bug 修复。 新特性 添加 GlideModules,更简单的延迟配置 (#311). 支持原始大小 (#274): // You can override a view's size ...

oschina
2015/01/26
7.5K
6
Android Bitmaps缓存

Android 开发中,bitmap是引起内存泄漏的罪魁祸首,关于bitmap的加载,缓存策略,官方已经给了很详细的方法: 缓存之Memory Cache: 缓存的策略,是利用应用程序的分配的内存拿出适当的一部分...

wei-spring
2015/10/27
0
0
Glide 4.2.0 发布,Android 图片加载和缓存库

Glide 4.2.0 已发布,本次更新主要包括新功能的添加和 Bug 修复,例如增加了替代 Glide 默认编码器的支持,以及在具有相同的缓存键时,添加了一种更高级的方法来控制如何/何时重启请求。 具体...

局长
2017/10/04
912
0
详细解释强力的图片加载框架 Glide的配置(顺便补充下CollapsingToolbarLayout的一些功能)

转载请注明出处:王亟亟的大牛之路 折腾了一天,单位里的网终于好了真是蛋疼,然后今天讲Glide(本来是准备昨天写的,唉) 理论性的介绍就直接从网上扣点来了,从头码字没啥意义,废话不多,...

ddwhan0123
2016/03/23
0
0
深入理解Ehcache系列(一)

EHcache是 Java最广泛使用的一种Cache. 它能高效的减轻数据库的负载,同时有很好的扩展,支持集群, 是解决C10K问题的一把重要利器. 它使用简单,高速,实现线程安全,同时提供了用内存,磁盘文件...

ZooKeeper
2013/12/16
0
3
Disruptor 全解析(6):为什么它这么快 (二) - 神奇的 cacheline 补齐

原文地址:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-why-its-so-fast_22.html, 作者是 Trisha Gee, LMAX 公司的一位女工程师。 我们多次提到了 Mechanical Sympathy ......

长源
2012/06/29
0
0
快速开发CSS的框架--CSScaffold

CSScaffold是一款帮助CSS开发者快速进行开发的框架,使用PHP编写而成- Simple, but powerful ! 不同于许多CSS框架,它必须依靠PHP与Apache的mod_rewrite来执行,但也因为需要这两种东西,让...

匿名
2009/12/29
9.1K
1
关于游戏服务器中缓存的设计方案的讨论

(只针对游戏服务器中的热数据)游戏服务器的缓存设计总体大概有三种类型:进程内缓存--如java的ehcahe、 进程内缓存--使用会话session Cache,通过语言的基础类型和基础的集合框架来定制 、...

石头哥哥
2014/10/31
0
0
guava源码阅读

Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解...

browser123
2017/05/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JAVA知识点随心记

1.Switch case具体的支持类型? Q:支持byte、short、char、int基本类型,枚举类型和String类型(JDK7以上支持),四种基本类型的包装类型也支持,但是原因在于触发了自动拆箱,将包装类型拆成了基本...

勤奋的蚂蚁
12分钟前
0
0
NoSQL

一、NoSQL介绍 NoSQL属于非关系型数据,mysql属于关系型数据库。 对于关系型数据库来说,是需要把数据存储到库、表、行、字段里,查询的时候根据条件一行一行地去匹配,当数据量非常大的时候...

人在艹木中
17分钟前
0
0
第17章MySQL主从配置

mysql安装总结 mysql主从准备工作: 准备两台机器,每台机器安装msyql服务,并启动mysql服务 mysql详细安装 1.首先下载二进制免编译的包,下载到/usr/local/src/目录下 2.解压压缩包 3.解压完...

Linux学习笔记
21分钟前
0
0
Redis高可用及分片集群

一、主从复制 使用异步复制 一个服务器可以有多个从服务器 从服务器也可以有自己的从服务器 复制功能不会阻塞主服务器 可以通过服务功能来上主服务器免于持久化操作,由从服务器去执行持久化...

Java大蜗牛
25分钟前
0
0
前端面试题汇总

最近在复习,准备找工作了,特此总结一下前端的相关知识。 1.获取浏览器URL中查询字符的参数: function getQuery(name){    var reg = new RegExp("(^|&)"+name+"=([^&]*)"(&|$));...

凛冬来袭
59分钟前
0
0
可持续发展的学习道路

与其要求别人,不如提升自己 内心渴望进步 经常做出改变现有模式,不断学习 寻找资源,整合资源,不断熟练这种模式 渠道很重要 先打开新世界的航路

狮子狗
今天
0
0
apollox-lua开源项目 示例codepen2

今天在示例上增加了几个功能, 首先添加js array的标准库。 所有js array的方法目前都支持了。 添加查看code模式。 点击查看code可以看到生成的lua代码。默认web模式需要把标准库连接进来, ...

钟元OSS
今天
0
0
javascript性能优化之避免重复工作

javascript最重要也最根本的性能优化标准之一是避免工作,避免工作又包括两点,第一,不做不必要的工作,第二,不做重复的已经完成的工作。第一部分可以通过代码重构完成,第二部分不做重复的...

老韭菜
今天
0
0
缓存穿透、并发和雪崩那些事

0 题记 缓存穿透、缓存并发和缓存雪崩是常见的由于并发量大而导致的缓存问题,本文讲解其产生原因和解决方案。 缓存穿透通常是由恶意攻击或者无意造成的;缓存并发是由设计不足造成的;缓存雪...

Java填坑之路
今天
1
0
项目jar包管理构建工具---Maven

一、what is Maven? 我们来寻找一下官网,里面介绍了maven到底是什么?下面一句话就有讲解到:Apache Maven is a software project management and comprehension tool. Based on the conc...

一看就喷亏的小猿
今天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部