设计简单的Android图片加载框架
目前android 发展至今优秀的图片加载框架太多,例如: volley ,picasso,imageloader,glide等等。但是作为程序猿,懂得其中的实现原理还是相当重要的,只有懂得才能更好地使用。于是乎,今天我就简单设计一个网络加载图片框架。主要就是熟悉图片的网络加载机制。
一般来说,一个优秀的 图片加载框架(imageloader) 应该具备如下功能:
图片压缩
内存缓存
磁盘缓存
图片的同步加载
图片的异步加载
网络拉取
那我们就从以上几个方面进行介绍:
1.图片压缩(有效的降低oom的发生概率)
图片压缩功能我在bitmap 的高效加载中已经做了介绍这里不多说直接上代码。这里直接抽象一个类用于完成图片压缩功能。
public class imageresizer { private static final string tag = "imageresizer"; public imageresizer() { super(); // todo auto-generated constructor stub } public bitmap decodesampledbitmapfromresource(resources res, int resid, int reqwidth, int reqheight) { final bitmapfactory.options options = new bitmapfactory.options(); options.injustdecodebounds = true; bitmapfactory.decoderesource(res, resid, options); options.insamplesize = calculateinsamplesize(options, reqwidth, reqheight); options.injustdecodebounds = false; return bitmapfactory.decoderesource(res, resid, options); } public bitmap decodesampledbitmapfrombitmapfiledescriptor(filedescriptor fd, int reqwidth,int reqheight){ final bitmapfactory.options options = new bitmapfactory.options(); options.injustdecodebounds = true; bitmapfactory.decodefiledescriptor(fd, null, options); options.insamplesize = calculateinsamplesize(options, reqwidth, reqheight); options.injustdecodebounds = false; return bitmapfactory.decodefiledescriptor(fd, null, options); }
public int calculateinsamplesize(bitmapfactory.options options, int reqwidth, int reqheight) { final int width = options.outwidth; final int height = options.outheight; int insamplesize = 1; if (height > reqheight || width > reqwidth) { final int halfheight = height / 2; final int halfwidth = width / 2; while ((halfheight / insamplesize) > reqheight && (halfwidth / insamplesize) > halfwidth) { insamplesize *= 2; } } return insamplesize; } }
2.内存缓存和磁盘缓存
缓存直接选择 lrucache 和 disklrucache 来完成内存缓存和磁盘缓存工作。
首先对其初始化:
private lrucache<string, bitmap> mmemorycache; private disklrucache mdisklrucache; public imageloader(context context) { mcontext = context.getapplicationcontext(); //分配内存缓存为当前进程的1/8,磁盘缓存容量为50m int maxmemory = (int) (runtime.getruntime().maxmemory() * 1024); int cachesize = maxmemory / 8; mmemorycache = new lrucache<string, bitmap>(cachesize) { @override protected int sizeof(string key, bitmap value) { return value.getrowbytes() * value.getheight() / 1024; } }; file diskcachedir = getdiskchahedir(mcontext, "bitmap"); if (!diskcachedir.exists()) { diskcachedir.mkdirs(); } if (getusablespace(diskcachedir) > disk_cache_size) { try { mdisklrucache = disklrucache.open(diskcachedir, 1, 1, disk_cache_size); misdisklrucachecreated = true; } catch (ioexception e) { e.printstacktrace(); } } }
创建完毕后,接下来则需要提供方法来视线添加以及获取的功能。首先来看内存缓存。
private void addbitmaptomemorycache(string key, bitmap bitmap) { if (getbitmapfrommemcache(key) == null) { mmemorycache.put(key, bitmap); } } private bitmap getbitmapfrommemcache(string key) { return mmemorycache.get(key); }
相对来说内存缓存比较简单,而磁盘缓存则复杂的多。磁盘缓存(lrudiskcache)并没有直接提供方法来实现,而是要通过editor以及snapshot 来实现对于文件系统的添加以及读取的操作。
首先看一下,editor,它提供了commit 和 abort 方法来提交和撤销对文件系统的写操作。
//将下载的图片写入文件系统,实现磁盘缓存 private bitmap loadbitmapfromhttp(string url, int reqwidth, int reqheight) throws ioexception { if (looper.mylooper() == looper.getmainlooper()) { throw new runtimeexception("can not visit network from ui thread."); } if (mdisklrucache == null) return null; string key = hashkeyformurl(url); disklrucache.editor editor = mdisklrucache.edit(key); if (editor != null) { outputstream outputstream = editor .newoutputstream(disk_cache_index); if (downloadurltostream(url, outputstream)) { editor.commit(); } else { editor.abort(); } } mdisklrucache.flush(); return loadbitmapfordiskcache(url, reqwidth, reqheight); }
snapshot, 通过它可以获取磁盘缓存对象对应的 fileinputstream,但是fileinputstream 无法便捷的进行压缩,所以通过filedescriptor 来加载压缩后的图片,最后将加载后的bitmap添加到内存缓存中。
public bitmap loadbitmapfordiskcache(string url, int reqwidth, int reqheight) throws ioexception { if (looper.mylooper() == looper.getmainlooper()) { log.w(tag, "load bitmap from ui thread , it's not recommended"); } if (mdisklrucache == null) return null; 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(); bitmap = mimageresizer.decodesampledbitmapfrombitmapfiledescriptor( filedescriptor, reqwidth, reqheight); if (bitmap != null) { addbitmaptomemorycache(key, bitmap); } } return bitmap; }
3.同步加载
同步加载的方法需要外部在子线程中调用。
//同步加载 public bitmap loadbitmap(string uri, int reqwidth, int reqheight) { bitmap bitmap = loadbitmpafrommemcache(uri); if (bitmap != null) { return bitmap; } try { bitmap = loadbitmapfordiskcache(uri, reqwidth, reqheight); if (bitmap != null) { return bitmap; } bitmap = loadbitmapfromhttp(uri, reqwidth, reqheight); } catch (ioexception e) { e.printstacktrace(); } if (bitmap == null && !misdisklrucachecreated) { bitmap = downloadbitmapfromurl(uri); } return bitmap; }
从方法中可以看出工作过程遵循如下几步:
首先尝试从内存缓存中读取图片,接着尝试从磁盘缓存中读取图片,最后才会从网络中拉取。此方法不能再主线程中执行,执行环境的检测是在loadbitmapfromhttp中实现的。
if (looper.mylooper() == looper.getmainlooper()) { throw new runtimeexception("can not visit network from ui thread."); }
4.异步加载
//异步加载 public void bindbitmap(final string uri, final imageview imageview, final int reqwidth, final int reqheight) { imageview.settag(tag_key_uri, uri); bitmap bitmap = loadbitmpafrommemcache(uri); if (bitmap != null) { imageview.setimagebitmap(bitmap); return; } runnable loadbitmaptask = new runnable() { @override public void run() { bitmap bitmap = loadbitmap(uri, reqwidth, reqheight); if (bitmap != null) { loaderresult result = new loaderresult(imageview, uri, bitmap); mmainhandler.obtainmessage(message_post_result, result) .sendtotarget(); } } }; thread_pool_executor.execute(loadbitmaptask); }
从bindbitmap的实现来看,bindbitmap 方法会尝试从内存缓存中读取图片,如果读取成功就直接返回结果,否则会在线程池中去调用loadbitmap方法,当图片加载成功后再将图片、图片的地址以及需要绑定的imageview封装成一个loaderresult对象,然后再通过mmainhandler向主线程发送一个消息,这样就可以在主线程中给imageview设置图片了。
下面来看一下,bindbitmap这个方法中用到的线程池和handler,首先看一下线程池 thread_pool_executor 的实现。
private static final int cpu_count = runtime.getruntime() .availableprocessors(); private static final int core_pool_size = cpu_count + 1; private static final int maximum_pool_size = cpu_count * 2 + 1; private static final long keep_alive = 10l; private static final threadfactory sthreadfactory = new threadfactory() { private final atomicinteger mcount = new atomicinteger(); @override public thread newthread(runnable r) { // todo auto-generated method stub return new thread(r, "imageloader#" + mcount.getandincrement()); } }; public static final executor thread_pool_executor = new threadpoolexecutor( core_pool_size, maximum_pool_size, keep_alive, timeunit.seconds, new linkedblockingdeque<runnable>(), sthreadfactory);
1.使用线程池和handler的原因。
首先不能用普通线程去实现,如果采用普通线程去加载图片,随着列表的滑动可能会产生大量的线程,这样不利于效率的提升。 handler 的实现 ,直接采用了 主线程的looper来构造handler 对象,这就使得 imageloader 可以在非主线程构造。另外为了解决由于view复用所导致的列表错位这一问题再给imageview 设置图片之前会检查他的url有没有发生改变,如果发生改变就不再给它设置图片,这样就解决了列表错位问题。
private handler mmainhandler = new handler(looper.getmainlooper()) { @override public void handlemessage(message msg) { loaderresult result = (loaderresult) msg.obj; imageview imageview = result.imageview; imageview.setimagebitmap(result.bitmap); string uri = (string) imageview.gettag(tag_key_uri); if (uri.equals(result.uri)) { imageview.setimagebitmap(result.bitmap); } else { log.w(tag, "set image bitmap,but url has changed , ignored!"); } } };
总结:
图片加载的问题 ,尤其是大量图片的加载,对于android 开发者来说一直是比较困扰的问题。本文只是提到了最基础的一种解决方法,用于学习还是不错的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。