Android Bitmap的加载优化与Cache相关介绍
一 . 高效加载 bitmap
bitmapfactory 提供了四类方法: decodefile,decoderesource,decodestream 和 decodebytearray 分别用于从文件系统,资源,输入流以及字节数组中加载出一个 bitmap 对象。
高效加载 bitmap 很简单,即采用 bitmapfactory.options
来加载所需要尺寸图片。bitmapfactory.options
就可以按照一定的采样率来加载缩小后的图片,将缩小后的图片置于 imageview 中显示。
通过采样率即可高效的加载图片,遵循如下方式获取采样率:
- 将
bitmapfactory.options
的 injustdecodebounds 参数设置为 true 并加载图片 - 从
bitmapfactory.options
中取出图片的原始宽高信息,即对应于 outwidth 和 outheight 参数 - 根据采样率的规则并结合目标 view 的所需大小计算出采样率 insamplesize
- 将
bitmapfactory.options
的 injustdecodebounds 参数设置为 false,然后重新加载图片
过上述四个步骤,加载出的图片就是最终缩放后的图片,当然也有可能没有缩放。
代码实现如下:
public bitmap decodesampledbitmapfromresource(resources res, int resid, int reqwidth, int reqheight) { // first decode with injustdecodebounds=true to check dimensions final bitmapfactory.options options = new bitmapfactory.options(); options.injustdecodebounds = true; bitmapfactory.decoderesource(res, resid, options); // calculate insamplesize options.insamplesize = calculateinsamplesize(options, reqwidth, reqheight); // decode bitmap with insamplesize set options.injustdecodebounds = false; return bitmapfactory.decoderesource(res, resid, options); } public int calculateinsamplesize(bitmapfactory.options options, int reqwidth, int reqheight) { if (reqwidth == 0 || reqheight == 0) { return 1; } // raw height and width of image final int height = options.outheight; final int width = options.outwidth; log.d(tag, "origin, w= " + width + " h=" + height); int insamplesize = 1; if (height > reqheight || width > reqwidth) { final int halfheight = height / 2; final int halfwidth = width / 2; // calculate the largest insamplesize value that is a power of 2 and // keeps both height and width larger than the requested height and width. while ((halfheight / insamplesize) >= reqheight && (halfwidth / insamplesize) >= reqwidth) { insamplesize *= 2; } } log.d(tag, "samplesize:" + insamplesize); return insamplesize; }
实际使用就可以像下面这样了,如加载 100*100 的图片大小,就可以像下面这样高效的加载图片了:
mimageview.setimagebitmap( decodesampledbitmapfromresource(getresource(),r.id.myimage,100,100));
二 . android 中的缓存策略
目前常用的算法是 lru,即近期最少使用算法,当缓存存满时,会优先淘汰近期最少使用的缓存对象
2.1 lrucache
lrucache 是一个泛型类,其内部实现机制是 linkedhashmap 以强引用的方式存储外部的缓存对象,提供了 get()
和 put()
来完成缓存对象的存取。当缓存满了,移除较早的缓存对象,再添加新的。lrucache 是线程安全的。
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用时,系统内存不足时,会被 gc 回收
- 弱引用:当一个对象只有弱引用时,随时会被回收
2.2 disklricache
disklrucache 用于实现存储设备缓存,即磁盘缓存。
2.2.1 disklrucache 的创建
由于它不属于 android sdk的一部分,所以不能通过构造方法来创建,提供了 open()
方法用于自身的创建
public static disklrucache open(file directory,int appversion,int valuecount,long maxsize);
典型的 disklrucache 的创建过程
private static final disk_cache_size = 1024*1024*50;//50m file diskcachedir = getdiskcachedir(mcontext,"bitmap"); if(!diskcachedir.exists()){ diskcachedir.mkdirs(); } mdisklrucache = disklrucache.open(diskcachedir,1,1,disk_cache_size);
第三个参数表示单个节点所对应的数据,一般设置为1即可。
2.2.2 disklrucache 的缓存添加 缓存的添加操作是通过 editor 完成的, editor 表示一个缓存对象的编辑对象。disklrucache 不允许同时编辑一个缓存对象。
2.2.3 disklrucache 的缓存查找
缓存查找过程也需要将 url 转换为 key,通过 disklrucache 的 get()
得到一个 snapshot 对象,然后通过该对象即可得到缓存的文件输入流,得到文件输入流即可得到 bitmap 对象了。为了避免加载过程中 oom,一般不会直接加载原始图片。在前面介绍通过 bitmapfactory.options
来加载一张缩放后的图片,但是那种方法对 fileinputstream 的缩放存在问题,原因是 fileinputstream 是一种有序的文件流,而两次 decodestream 调用影响了文件流的位置属性,导致了第二次 decodestream 时得到的是 null。为了解决这个问题,可以通过文件流得到其对应的文件描述符,然后通过 bitmapfactory.decodefiledescriptor
方法来加载一张缩放过后的图片。
bitmap bitmap = null; string key = hashkeyformurl(url); disklrucache.snapshot snapshot = mdisklrucache.get(key); if (snapshot != null) { fileinputstream fileinputstream = (fileinputstream)snapshot.getinputstream(disk_cache_index); // 获取文件描述符 filedescriptor filedescriptor = fileinputstream.getfd(); // 通过 bitmapfactory.decodefiledescriptor 来加载一张缩放后的图片 bitmap = mimageresizer.decodesampledbitmapfromfiledescriptor(filedescriptor, reqwidth, reqheight); if (bitmap != null) { addbitmaptomemorycache(key, bitmap); } } return bitmap; }
三 . imageloader 的实现
具备的功能,即图片的同步加载,异步加载,图片的压缩,内存缓存,磁盘缓存以及网络拉取。
3.1 图片压缩功能
如前面所述。
3.2 内存缓存和磁盘缓存的实现
选择 lrucache 和 disklrucache 来分别完成内存缓存和磁盘缓存的工作
3.3 同步加载和异步加载的接口设计
关于同步加载:从 loadbitmap 的实现可以看出,其工作过程遵循如下几个步骤:先试着从内存缓存中读取图片,接着从磁盘缓存中读取图片,最后试着从网络拉取图片。另外该方法不能在主线程中调用,否则就会抛出异常。因为加载图片是一个耗时的操作。
关于异步加载:从 bindbitmap 中可以看出,binfbitmap 会先试着从内存缓存中读取结果,如果成功就直接返回,否则会从线程池中去调用 loadbitmap()
,当加载成功后,再讲图片,图片地址以及需要绑定的 imageview 封装成一个 loaderresult 对象,通过 mmainhandler 向主线程发送一个消息,这样就可以在主线程中给 imageview 设置图片了。图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式来加载图片,并将图片设置给需要的 imageview, 从而imageloader 内部需要自己需要在内部线程中加载图片,并且将图片设置给所需要的 imageview。
imageloader源码可以点击这里:下载 查看imageloader的实现
四 . imageloader 的使用
核心是 imageadapter , 其中的 getview()
的核心方法如下:
@override public view getview(int position, view convertview, viewgroup parent) { viewholder holder = null; if (convertview == null) { convertview = minflater.inflate(r.layout.image_list_item,parent, false); holder = new viewholder(); holder.imageview = (imageview) convertview.findviewbyid(r.id.image); convertview.settag(holder); } else { holder = (viewholder) convertview.gettag(); } imageview imageview = holder.imageview; final string tag = (string)imageview.gettag(); final string uri = getitem(position); if (!uri.equals(tag)) { imageview.setimagedrawable(mdefaultbitmapdrawable); } if (misgridviewidle && mcangetbitmapfromnetwork) { imageview.settag(uri); // 这句话将图片的复杂加载过程交给 imageloader 了 mimageloader.bindbitmap(uri, imageview, mimagewidth, mimagewidth); } return convertview; }
对于上述代码 imageadapter 来说, imageloader 的加载图片的复杂过程,更不需要知道。
优化列表卡顿现象:
- 不要在 getview() 中做加载图片的操作,那样肯定会耗时,像这个例子中一样,交给 imageloaer 来实现。
- 控制异步加载频率, 如果用户刻意的频繁的上下滑动,可能在一瞬间加载几百个异步任务,这样会给线程池造成拥堵。解决的办法是考虑在用户滑动列表时,停止加载图片。等到列表停下来时,在进行异步加载任务。
- 开启硬件加速:给activity添加配置android:hardwareaccelerated=”true”
总结
以上就是这篇文章的全部内容了,希望本文的内容对给我android开发者们能带来一定的帮助,如果有疑问大家可以留言交流。