前言
在Android开发中,ImageLoader应该算得上是最重要的开源库之一,由于项目原因(不能使用开源库),前段时间自己也是需要实现一个简单的ImageLoader,因此诞生了这个库,我们暂且叫它为SimpleImageLoader。就目前而言,你上网查ImageLoader资料的时候,基本上能够找到很简单的实现,基本上一个类就把所有的工作给做了,这就显得很不专业了嘛,很多时候我们不只是需要实现功能,而是希望能够在实现功能的同时在设计层面有所提升。
SimpleImageLoader分享出来的主要目的并不是说替代那些著名开源库,而是提供一个简单的、又有一定参考价值的ImageLoader实现让一些需要帮助的人学习,在深入了解实现的同时学到知识,也能够体会到在设计一个开源库时应该要做哪些考虑、做哪些取舍、有什么模式,当然在了解了ImageLoader的实现之后再去使用专业的开源库也会更加的得心应手,出现问题的时候自己也能够不太费力地去究其原因。在提升自己的同时也能够了解一些开源库的设计基本原则,这也是我的博客中一直主张的观点。
当然限于本人水平有限,有bug在所难免,但是我们这里是以学习为目的,了解ImageLoader的设计与实现才是我们最重要的目的,一些细节不必在意,可以在你深入的学习过程中修改你认为不合理或者错误的地方。如果你有好的实现方法或者有好的建议,也都请指教一二;如果你认为我写的东西烂得不值一提,那你就深深地埋藏在心里吧。
基本架构
一般来说,ImageLoader的实现都是基于线程池,在第一版的我也是使用线程池来加载图片,但是后面的版本却换成了跟SimpleNet类似的架构模式,原因是觉得线程池刚启动的时候会稍微慢一些,我感觉不太爽就换了线程模型。当然我也会把线程池的版本在另外的分支上给出,这样给一些需要的朋友参考。
SimpleImageLoader的基本结构如图1所示。
图1
看到这幅图看过SimpleNet网络框架的朋友应该是会熟悉一些,基本结构与SimpleNet很相似,其实主要也是我们比较喜欢把架构图画成这种分层样式,感觉比较好理解,
SimpleImageLoader类是用户的入口,用户在通过配置类初始化SimpleImageLoader之后就可以向SimpleImageLoader提交加载图片的请求了。SimpleImageLoader内部维护了一个请求队列,用户提交的加载图片的请求会在内部被封装成BitmapRequest对象,然后将这些对象放到请求队列中。在创建队列时会创建用户指定数量(默认为CPU个数 + 1)的线程来加载图片,这些线程在内部命名为RequestDispatcher,它们在run函数中不断地获取队列中的加载请求,然后交给对应的Loader加载图片。
为了方便用户的扩展,我们引入了Loader这个抽象,因为在SimpleImageLoader中只支持两种图片uri的加载,即网络图片uri和本地文件的uri。网络图片一般以"http://"或者"https://"开头,而本地图片的uri格式却是"file://"开头,SimpleImageLoader内部通过图片uri的格式的不同使用不同的Loader来加载图片,这样后续用户就可以注册Loader来实现其他格式的加载,例如"drawable:// + 图片名"来加载res/drawable中的图片等。这样保证了SimpleImageLoader可加载图片uri格式的可扩展性。Loader会通过LoaderManager来进行管理,如果需要注册自己的Loader实现,则调用LoaderManager的register函数即可。如果你传递进去的图片uri是无效,例如格式错误,那么LoaderManager会返回一个默认的Loader,这个默认的Loader名为NullLoader,它其实什么也不做,只是为了防止在外部进行判空而已,这种模式成为Null Object设计模式。当然,在加载图片之前我们会从缓存中读取,如果有缓存我们则不加载。
Loader加载完图片之后会先更新UI,即将图片显示到对应的ImageView上,在构造BitmapRequest时内部已经将图片的uri设置为ImageView的tag了。图片加载完成后判断ImageView的tag和uri是否相等,如果相等则将图片显示到ImageView上,否则不更新ImageView。这一步很重要,很多朋友在使用ImageLoader时出现问题基本上就是由于没有设置ImageView的tag。
加载图片的先后顺序是由加载策略决定的,策略相关的内容没有在架构图中给出。加载策略决定了请求在队列中的排序,在将请求添加到队列中时会给每个请求设置一个序号,队列将根据这个序号对请求进行排序。这样我们就可以知道哪个请求是先添加进来的,也可以很方便的实现自己的策略类来定制自己的加载策略,比如最后加载到队列中的请求最先加载。比如我们在ListView滚动时,最后添加到队列中的图片请求应该是我们最急需显示的,我们它们就在手机的当前屏幕,而前面的请求对应的ImageView已经被复用,即使它们加载完成它们也不会被显示,因为ImageView的tag已经变化了。因此,策略的灵活性依然很重要。
在加载完图片并且更新UI之后,我们会将图片缓存起来。内置的缓存类型有四种,无缓存、内存缓存、sd卡缓存、内存和sd卡的双缓存,这四种缓存都实现了Cache接口,如果你这四种缓存类型还不能满足你的需求,那么你可以实现Cache接口,然后实现自己的缓存逻辑,然后在配置ImageLoader时设置需要的缓存类型(具体配置后续说明),如果不配置则默认使用的是内存缓存。这里我们又看到了一个面向接口编程的例子,即SimpleImageLoader只依赖于Cache接口的抽象,而不是说依赖于某个具体的缓存类,这样用户就可以很方面的实现自己的缓存逻辑,并且将缓存实现注入到sdk中。当然,上述的Loader、加载策略实现也是基于同样的理论基础,就是说过很多遍的“面向接口编程”。
恩,是时候捋一捋这个执行流程了。
用户调用displayImage请求加载图片,SimpleImageLoader将这个加载图片请求封装成一个Request,然后加入到队列中。几个色眯眯的调度子线程不断地从队列中获取请求,然后根据uri的格式获取到对应的Loader来加载图片。在加载图片之前首先会查看缓存中是否含有目标图片(具体细节在后续的博客再细说),如果有缓存则使用缓存,否则加载目标图片。获取到图片之后,我们会将图片投递给ImageView进行更新,如果该ImageView的tag与图片的uri是一样的,那么则更新ImageView,否则不处理。使用ImageView的tag与图片的uri进行对比是为了防止图片错位显示的问题,这在ImageLoader中是很重要的一步。如果目标图片没有缓存,第一次从uri中加载后会加入缓存中,当然从sdcard中加载的图片我们只会缓存到内存中,而不会再缓存一份到sd卡的另一个目录中。这样,整个加载过程也就完成了。
SimpleImageLoader工程结构图
效果图
上述效果中有四张图片是显示不出来的,因为在图片uri列表中有三张是我手机中的图片,模拟器中没有,因此显示加载失败。还有一张是无效的uri,也是加载失败。