展会信息港展会大全

构建Android缓存模块 Memory Cache & File Cache
来源:互联网   发布日期:2016-01-19 12:27:44   浏览:2645次  

导读:在我翻译的Google官方系列教程中,Bitmap系列由浅入深地介绍了如何正确的解码Bitmap,异步线程操作以及使用Fragments重用等技术,并且在最后给出了非常强大的独家秘笈:BitmapFun,让猿媛们得以一窥究竟Google的 ...

在我翻译的Google官方系列教程中,Bitmap系列由浅入深地介绍了如何正确的解码Bitmap,异步线程操作以及使用Fragments重用等技术,并且在最后给出了非常强大的独家秘笈:BitmapFun,让猿媛们得以一窥究竟Google的攻城师们是如何高屋建瓴地秒杀OOM的。

前言

在下载到BitmapFun.rar这个神圣的压缩包以后,我是双手颤抖,似乎是打开上古秘藏一般,心情激动导致久久不能自已。我还记得那天上海下着小雨,我当时霍然起身,伫立在23楼的窗台,仰着头向江水对岸的东方明珠望去,似乎这样我郁积已久的眼泪就不能掉下来。说到这里,Ryan又暗自抹了一把眼泪。短暂地忘记了过去的黑暗时光,那一个漫长的被OOM的淫威所折磨的盛夏。。。

最后在Boss诧异的目光中,我回到办公桌,按捺着内心汹涌的情绪波动,然后小心翼翼的打开BitmapFun.rar。当那些在洪荒时代就活跃在Android平台的大师们书写的篇章呈现在我眼前时,我的表情与阿宝从师父手里得到Dragon Scroll时一般,永久的定格在了极度天真的期待与眼角一抽一抽的状态。

那些泛黄的代码在我看去,通篇只有一句话:老子看不懂!

自力更生,构建自己的缓存模块

Google的这个demo堪称详尽,考虑极其周详,自然是极好的。但是当原理被层层的 特殊情况 包装起来,原本简单的例子变得异常复杂,几个类之间的关系错综复杂,堪比吸血鬼日记几个帅哥美女之间的关系。要理解清楚每一句代码的含义,你一定要有理解Matt那人老珠黄的老娘和他和失落的好朋友Taylor搞在一起的觉悟。

好了,吐槽一下就收,千万不要怀疑Google,人家已经仁至义尽了。BitmapFun中在下载后将Bitmap缓存起来,缓存做了两份:LruCache和DiskLruCache,分别是内存缓存和硬盘缓存。此外两个至关重要的类是:

1

BitmapWorkerTask(ImageView imageView)

2

3

AsyncDrawable extends BitmapDrawable

4

AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask)

BitmapWorkerTask持有一个WeakReference<ImageView> imageViewReference,弱引用ImageView,用作异步处理加载图片的任务。

AsyncDrawable巧妙的引用持有弱引用WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference,是BitmapDrawable的子类,这样就可以setImageBitmap(AsyncDrawable)

关系:AsyncDrawable中弱引用BitmapWorkerTask。其实是图片引用ImageView的关系,而ImageView.getDrawable又可以获得图片。这种高妙的思想不是正值得我们学习么?

当然,这节课并不是讲解官方Demo的,在讲解它之前,我们先来学习一个更加简单的缓存实现方案,使用最简单的方式快速构建自己应用的缓存模块,有效避免OOM异常。它的难度非常小也很方便理解,可以在这个缓存实现的基础上,我们再去理解更加高妙的BitmapFun的缓存实现方案。

后面将要介绍的缓存方案已经应用在一个项目中(该项目将于13年1月20开源,使用Github托管,纪念我22岁的生日),效果相当不错,下载并显示上百张Bitmap也异常流畅,甚至没有半点的停顿,全程使用Emulator测试也没有出现过OOM异常,内存处于可控状态。

如何解决OOM

Bitmap之所以容易引起OOM异常,原因已经在Bitmap系列教程中说的明明白白。但是我们至少清楚一点:一个手机屏幕再大,合理尺寸的Bitmap也不至于耗空所有内存,那要怎么做才能避免OOM呢?

加载合理尺寸的Bitmap

避免反复解码、重复加载Bitmap

控制Bitmap的生命周期,合理回收

此外网上也有不少歪门邪道,我个人认为是不可取的,使用这些简单粗暴的方法,后期会为你带来更大的麻烦:

减损图片质量(使用过高的inSampleSize值)

使用decodeStream(绕过Java层,直接调用JNI)

强制增加heap size

其他

控制Bitmap的生命周期才是正解,BitmapFun使用的LruCache是将它将最近被引用到的对象存储在一个强引用的 LinkedHashMap中,并且在缓存超过了指定大小之后将最近不常使用的对象释放掉。

Memory Cache的Size是受限的,因此加入DiskLruCache,虽然在访问速度上逊于Memory Cache,但是速度也是相当可观的。

借鉴Google的做法,我也将缓存做了两份,一份是Memory Cache,使用弱引用的WeakHashMap来控制Bitmap的生命周期,后面会有详细解释。另一份严格来说不能算是缓存,直接将文件存储在SDCard上,避免重复下载。

佛说引用,既非引用,是名引用。

关于引用,或许对于小菜鸟们不是很好理解(我碰到过太多Java都没学好来做Android的,基础很重要!)。我使用金刚经的著名三段论来解释它:佛说XX,既非XX,是名XX。

这句话什么意思呢?比如佛说大米,既可以说它不是大米,只是名字叫做大米罢了。不会因为你为它改名叫做大麦而改变它的本质,你叫它做水,吃到嘴里的还是原来的味道。

关于引用,跟这个有着非常相似的共性。引用就相当于实际对象的名字,比如下面的例子:

1

Person p1 = new Person();

2

Person p2 = null;

3

p2 = p1;

4

p1 = null;

new Person()这个对象的名是p1,而后你将名字改成了p2,对象还是那个对象,不会因为你将p1的大名盖在null的头上而改变它的本质。以上的p1 和p2都是引用,它们都不过是名。

在了解到引用的含义后,虚拟机会告诉你,被引用的对象处于可获得(reachable)状态,它是你的好管家,既然你要用它,它就不会回收它。(你想想如果你正在吃一只烤鸭,人家突然一把抢了过去扔垃圾桶了你什么感觉。)

如果在上面的那段程序后面加上p2 = null,Person这个对象就没有任何引用指向它了,垃圾回收器会在不确定的时间进行回收。(你都把东西扔了,总不能不让人家收破烂吧?)

如果你想继续持有这个对象的引用,希望可以继续访问,但是也允许垃圾回收器进行回收,该怎么办呢?(你想减肥,告诉你的好朋友说,如果察觉到你太胖了,就将你嘴里的烤鸭抢去扔了。如果你很饿,身材也不错,你要继续吃。)

这个时候,我们需要借助Java提供的软/弱/虚引用。我们平时使用的如p1和p2这样的叫做强引用(Strong Reference)。要使垃圾回收器能在内存不够的时候,主动抢下你嘴里的烤鸭,进行回收,需要使用这些:

软引用:SoftReference

弱引用:WeakReference

虚引用:PhantomReference

它们按照由强到弱的引用关系排列,虚引用相当于几乎没有引用。文艺青年常说的若即若离用来形容它再恰当不过了。

关于这三个引用的具体学习,详见我提供的参考资料。这里只是向你解释为什么使用弱引用可以起到防止Bitmap过多而导致内存紧张的作用。

在这里,由于我需要使用Bitmap和名字的key-value对应关系,我使用Java提供的WeakHashMap(String key, Bitmap value),顾名思义,它用来保存WeakReference,并且确保每个key只对应一个值,在内存不够的时候,垃圾回收器会进行回收。当key值索引不到Bitmap,再进行其他的操作。

原理示意图

我将原理画成图,以便大家的理解。主体有三个,分别是UI,缓存模块和数据源。它们之间的关系如下:

① UI:请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。

② 内存缓存:缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。否则执行第三步。

③ 硬盘存储:使用唯一Key值对应的文件名,检索SDCard上的文件。

④ 如果有对应文件,使用BitmapFactory.decode*方法,解码Bitmap并返回数据,同时将数据写入缓存。如果没有对应文件,执行第五步。

⑤ 下载图片:启动异步线程,从数据源下载数据(Web)。

⑥ 若下载成功,将数据同时写入硬盘和缓存,并将Bitmap显示在UI中。

总结:这节课除了吐槽,主要的还是原理分析。如果你有更好的缓存方案,欢迎提出。下节课将讲解具体的 Memory Cache和FileCache如何实现。

【构建Android缓存模块】(二)Memory Cache & File Cache

上节课我们讲到普通应用缓存Bitmap的实现分析,根据MVC的实现原理,我将这个简单的缓存实现单独写成一个模块,这样可以方便以后的使用,对于任意的需求,都属于一个可插拔式的功能。

之前提到,这个缓存模块主要有两个子部件:

Memory Cache:内存缓存的存取速度非常惊人,远远快于文件读取,如果没有内存限制,当然首选这种方式。遗憾的是我们有着16M的限制(当然大多数设备限制要高于Android官方说的这个数字),这也正是大Bitmap容易引起OOM的原因。Memory Cache将使用WeakHashMap作为缓存的中枢,当程序内存告急时,它会主动清理部分弱引用(因此,当引用指向为null,我们必须转向硬盘缓存读取数据,如果硬盘也没有,那还是重新下载吧)。

能力越大,责任越大?人家只是跑得快了点儿,总得让人家休息,我们一定不希望让内存成为第一位跑完马拉松的Pheidippides,一次以后就挂了吧?作为精打细算的猿媛,我们只能将有限的内存分配给Memory Cache,将更繁重的任务托付给任劳任怨的SDCard。

File Cache:硬盘读取速度当然不如内存,但是为了珍惜宝贵的流量,不让你的用户在月底没有流量时嚎叫着要删掉你开发的 流量杀手 ,最好是避免重复下载。在第一次下载以后,将数据保存在本地即可。

文件读写的技术并不是很新颖的技术,Java Core那点儿就够你用了。不过要记得我们可是将Bitmap写入文件啊,怎么写入呢?不用着急,Android的Bitmap本身就具备将数据写入OutputStream的能力。我将这些额外的方法写在一个帮助类中:BitmapHelper

public static boolean saveBitmap(File file, Bitmap bitmap){

if(file == null || bitmap == null)

return false;

try {

BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));

return bitmap.compress(CompressFormat.JPEG, 100, out);

} catch (FileNotFoundException e) {

e.printStackTrace();

return false;

}

}

最后附上Memory Cache和File Cache的具体代码,非常简单。

public class MemoryCache {

private static final String TAG = "MemoryCache";

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

//WeakReference Map: key=string, value=Bitmap

private WeakHashMap<String, Bitmap> cache = new WeakHashMap<String, Bitmap>();

/**

* Search the memory cache by a unique key.

* @param key Should be unique.

* @return The Bitmap object in memory cache corresponding to specific key.

* */

public Bitmap get(String key){

if(key != null)

return cache.get(key);

return null;

}

/**

* Put a bitmap into cache with a unique key.

* @param key Should be unique.

* @param value A bitmap.

* */

public void put(String key, Bitmap value){

if(key != null && !"".equals(key) && value != null){

cache.put(key, value);

//Log.i(TAG, "cache bitmap: "key);

Log.d(TAG, "size of memory cache: "cache.size());

}

}

/**

* clear the memory cache.

* */

public void clear() {

cache.clear();

}

}

没什么难的地方,直接贴代码。下节课我讲讲解如何使用异步任务下载数据,以及使用Controller操作Model,控制View的显示。

【构建Android缓存模块】(三)Controller & 异步图片加载

转载声明:Ryan的博客文章欢迎您的转载,但在转载的同时,请注明文章的来源出处,不胜感激! :-)

http://my.oschina.net/ryanhoo/blog/93432

1

上节课我们学习了缓存模块的实现, 缓存分做两份:Memory Cache和File Cache。方法也很简单,分别是:

存储文件

按唯一key值索引文件

清空缓存

区别在于内存缓存读取优先,因为它读写的速度更快。但是考虑到内存限制,退而选用文件存储,分担内存缓存的压力。

原理非常简单,在第一课中已经详细分析了。那么要怎么才能将这个缓存模块与UI模块的显示关联起来呢?在这里我们需要一个控制器,掌管数据流向和读写,同时控制UI的显示。

那么这个控制器需要以下的元素:

内存缓存

硬盘缓存

异步任务处理

控制UI显示

//caches

private MemoryCache memoryCache;

private FileCache fileCache;

//Asynchronous task

private static AsyncImageLoader imageLoader;

Memory Cache和File Cache在上一课中有具体的实现,这里有一个异步的任务处理器AsyncImageDownloader,它用来在后台下载数据,完成下载后存储数据到缓存中,并更新UI的显示 。让我们来看看它是如何实现的:

class AsyncImageDownloader extends AsyncTask{

private ImageView imageView;

private String fileName;

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

public AsyncImageDownloader(ImageView imageView, String fileName){

this.imageView = imageView;

this.fileName = fileName;

}

@Override

protected void onPreExecute() {

super.onPreExecute();

imageView.setImageResource(R.drawable.placeholder);

}

@Override

protected Bitmap doInBackground(Void... arg0) {

String url = Utils.getRealUrlOfPicture(fileName);

HttpResponse response = new HttpRetriever().requestGet(url, null);

Log.i(TAG, "url: "url);

Log.i(TAG, "respone: "response);

InputStream in = null;

try {

if(response != null && response.getEntity() != null)

in = response.getEntity().getContent();

} catch (IllegalStateException e) {

e.printStackTrace();

return null;

} catch (IOException e) {

e.printStackTrace();

return null;

}

//TODO to be optimized: adjust the size of bitmap

return BitmapFactory.decodeStream(in);

}

@Override

protected void onPostExecute(Bitmap result) {

super.onPostExecute(result);

if(result != null && imageView != null)

imageView.setImageBitmap(result);

//TODO cache the bitmap both in sdcard & memory

memoryCache.put(fileName, result);// key is a unique token, value is the bitmap

fileCache.put(fileName, result);

}

}

可以看到这个类的构造函数需要两个参数,分别是文件名和对应要显示的ImageView,那么在任务开始的时候,可以为该ImageView设置未下载状 态的图片,然后下载完成后更新UI。

注:需要提醒的是,这里的唯一key值,我使用的是文件名,因为我接收到的文件名是唯一的。猿媛们也可以根据自己的需求,设计自己的唯一key值算法。

接下来,我们需要读用key值索引相应的Bitmap:

public Bitmap getBitmap(String key){

Bitmap bitmap = null;

//1. search memory

bitmap = memoryCache.get(key);

1

2

3

4

5

6

7

8

//2. search sdcard

if(bitmap == null){

File file = fileCache.getFile(key);

if(file != null)

bitmap = BitmapHelper.decodeFile(file, null);

}

return bitmap;

}

读取到Bitmap后进行显示:

public void displayBitmap(ImageView imageView, String fileName){

//no pic for this item

if(fileName == null || "".equals(fileName))

return;

1

2

3

4

5

6

7

8

9

Bitmap bitmap = getBitmap(fileName);

//search in cache, if there is no such bitmap, launch downloads

if(bitmap != null){

imageView.setImageBitmap(bitmap);

}

else{

Log.w(TAG, "Can't find the file you required.");

new AsyncImageDownloader(imageView, fileName).execute();

}

}

到这里,一个简单的缓存框架就搭建成功了。它简洁有效,但是非常单薄,似乎不够强大,需要你们根据自己的需求进行修改。另外它本来的目的就是用于演示,理 解这个以后,我们再来看Google的BitmapFun。

不过,我将它应用在一个小项目中,性能还不错。对于小项目的需求,应该是够的。

1

最后,附上使用方法,以及整个类的源码。

AsyncImageLoader imageLoader = AsyncImageLoader.getInstance(this);、

imageLoader.displayBitmap(imageView, fileName);

源码:

public class AsyncImageLoader {

private static final String TAG = "AsyncImageLoader";

//caches

private MemoryCache memoryCache;

private FileCache fileCache;

//Asynchronous task

private static AsyncImageLoader imageLoader;

class AsyncImageDownloader extends AsyncTask<Void, Void, Bitmap>{

private ImageView imageView;

private String fileName;

public AsyncImageDownloader(ImageView imageView, String fileName){

this.imageView = imageView;

this.fileName = fileName;

}

@Override

protected void onPreExecute() {

super.onPreExecute();

imageView.setImageResource(R.drawable.placeholder);

}

@Override

protected Bitmap doInBackground(Void... arg0) {

String url = Utils.getRealUrlOfPicture(fileName);

HttpResponse response = new HttpRetriever().requestGet(url, null);

Log.i(TAG, "url: "url);

Log.i(TAG, "respone: "response);

InputStream in = null;

try {

if(response != null && response.getEntity() != null)

in = response.getEntity().getContent();

} catch (IllegalStateException e) {

e.printStackTrace();

return null;

} catch (IOException e) {

e.printStackTrace();

return null;

}

//TODO to be optimized: adjust the size of bitmap

return BitmapFactory.decodeStream(in);

}

@Override

protected void onPostExecute(Bitmap result) {

super.onPostExecute(result);

if(result != null && imageView != null)

imageView.setImageBitmap(result);

//TODO cache the bitmap both in sdcard & memory

memoryCache.put(fileName, result);// key is a unique token, value is the bitmap

fileCache.put(fileName, result);

}

}

private AsyncImageLoader(Context context){

this.memoryCache=new MemoryCache();

this.fileCache=new FileCache(context);

}

public static AsyncImageLoader getInstance(Context context){

if(imageLoader == null)

imageLoader = new AsyncImageLoader(context);

return imageLoader;

}

public void displayBitmap(ImageView imageView, String fileName){

//no pic for this item

if(fileName == null || "".equals(fileName))

return;

Bitmap bitmap = getBitmap(fileName);

//search in cache, if there is no such bitmap, launch downloads

if(bitmap != null){

imageView.setImageBitmap(bitmap);

}

else{

Log.w(TAG, "Can't find the file you required.");

new AsyncImageDownloader(imageView, fileName).execute();

}

}

public Bitmap getBitmap(String key){

Bitmap bitmap = null;

//1. search memory

bitmap = memoryCache.get(key);

//2. search sdcard

if(bitmap == null){

File file = fileCache.getFile(key);

if(file != null)

bitmap = BitmapHelper.decodeFile(file, null);

}

return bitmap;

}

public void clearCache(){

if(memoryCache != null)

memoryCache.clear();

if(fileCache != null)

fileCache.clear();

}

}

赞助本站

人工智能实验室

相关热词: 缓存 模块 Android

相关内容
AiLab云推荐
展开

热门栏目HotCates

Copyright © 2010-2024 AiLab Team. 人工智能实验室 版权所有    关于我们 | 联系我们 | 广告服务 | 公司动态 | 免责声明 | 隐私条款 | 工作机会 | 展会港