转载:http://my.oschina.net/rengwuxian/blog/184650

应用的场景

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
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进行缓存:

1
2
3
4
5
6
7
8
9
10
11
12
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进行图片缓存的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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号窗口,并且咱俩同时排队到了窗户跟前。巧的是,咱俩买的是同一趟车,而这趟车现在只剩一张票了。然后咱俩都跟售票员说:就这张了,买!于是两个售票员同时点击了电脑上的“出票”按钮。后台系统接到两个请求,两个线程同时进行处理,执行了这么两行代码:

1
2
3
4
if (tickedCount > 0) { // 如果还有票
    tickedCount -= 1; // 票数减一
    printTicket(); // 出票
}

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

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

1
2
3
4
5
6
synchronized(this) {
    if (tickedCount > 0) { // 如果还有票
        tickedCount -= 1; // 票数减一
        printTicket(); // 出票
    }
}

上面这段代码由于加了锁,导致同一时间只有一个线程可以进入这个代码块,当一个线程进入后,其他线程必须等这个线程执行完这段代码后释放了锁,才能进入这个代码块。这样,同时出同一张票的bug就不可能出现了。当然,我只是举例,上面的代码只是一个简化模型。

由于篇幅限制,无法详细地介绍synchronized的更多性质和使用方法,如果有兴趣可以自己查找相关资料。

高效使用Bitmaps(三) 神奇的Cache的更多相关文章

  1. 高效使用Bitmaps(一) 大Bitmap的加载

    转载:http://my.oschina.net/rengwuxian/blog/182885 高效使用Bitmaps有什么好处? 我们常常提到的“Android程序优化”,通常指的是性能和内存的优化 ...

  2. java并发编程(三)cpu cache & 缓存一致性

    一 cpu cache 1. cache的意义    为什么需要CPU cache?因为CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源.所以cache的出 ...

  3. EXT.NET高效开发(三)——使用Chrome浏览器的开发人员工具

    这篇帖子老少皆宜,不分男女,不分种族,不分职业.俗话说:“磨刀不误砍柴工”.掌握一些开发工具的使用,对自己帮助是很大的(无论是用于分析问题,还是提高生产力).本篇就讲述如何利用Chrome浏览器(这里 ...

  4. 高效使用Bitmaps(二) 后台加载Bitmap

    转载:http://my.oschina.net/rengwuxian/blog/183802 为什么要在后台加载Bitmap? 有没有过这种体验:你在Android手机上打开了一个带有含图片的Lis ...

  5. buffer cache 深度解析

    本文首先详细介绍了oracle中buffer cache的概念以及所包含的内存结构.然后结合各个后台进程(包括DBWRn.CKPT.LGWR等)深入介绍了oracle对于buffer cache的管理 ...

  6. 多媒体文件格式(三):M3U8 格式

    一.M3U8 格式标准介绍 M3U8文件是指UTF-8编码格式的M3U文件.M3U文件是记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播 ...

  7. springboot整合redis-sentinel支持Cache注解

    一.前提 已经存在一个redis-sentinel集群,两个哨兵分别如下: /home/redis-sentinel-cluster/sentinel-1.conf port 26379 dir &q ...

  8. guava cache学习

    Guava Cache与ConcurrentMap很相似,但也不完全一样.最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除.相对地,Guava Cache为了限制内存占 ...

  9. Redis持久化存储(三)

    redis高级特性-发布订阅消息服务功能 Pub/Sub 订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者).而是发布的消 ...

随机推荐

  1. SPRING IN ACTION 第4版笔记-第九章Securing web applications-001-SpringSecurity简介(DelegatingFilterProxy、AbstractSecurityWebApplicationInitializer、WebSecurityConfigurerAdapter、@EnableWebSecurity、@EnableWebMvcS)

    一.SpringSecurity的模块 At the least, you’ll want to include the Core and Configuration modules in your ...

  2. *IntelliJ idea创建创建Maven管理的Java Web项目

    配置IntelliJ在IntelliJ的设置中,可以设置maven的安装目录,settings.xml文件的位置,和本地仓库的位置等信息.

  3. UnsupportedClassVersionError: Bad version number in...

    在使用eclipse开发servlet可能会出现一个很麻烦事情,版本不一致错误. java.lang.UnsupportedClassVersionError: Bad version number ...

  4. 188. Best Time to Buy and Sell Stock IV

    题目: 链接: 题解: 测试: Reference:

  5. James搭建邮件服务器

    来源:http://chenfengcn.iteye.com/blog/356874   1 关于James与Javamail Apache James(Java Apache Mail Enterp ...

  6. knowledge about apache

    http://wenku.baidu.com/link?url=6O51BQJdtFRFWDGszKfN3aK7IY92QTCpuc7miBhRLazXvxL5gXb18B_TqIdi3EruX1o_ ...

  7. JavaScript DOM高级程序设计 2.4-try{}catch{}--我要坚持到底!

    先看一段有异常的语句 var sound = 'Roar!'; function myOrneryBeast() { this.style.color='green';//window没有style属 ...

  8. NPOI的测试代码

    NPOI\testcases\main\testcases vs10.csproj 需要注意,重新引用一下NPOI类库 需要注意的是,测试项目,使用了NUnit 找到测试项目下的SS文件夹,再定位到U ...

  9. C#中保留2位小数

    public static void Method() { double a = 1.991; a = Math.Round(a); Console.WriteLine("a = {0}&q ...

  10. git设置忽略某些文件或文件夹

    在git中如果想忽略掉某个文件,不让这个文件提交到版本库中,可以使用修改 .gitignore 文件的方法.如果没有 .gitignore 文件,就自己创建一个,手动创建会提示你输入文件名称,因此,你 ...