浅析Cube SDK里的ImageLoader组件是如何加载图片的(一)
为什么使用轻量级的库?
通常我会选择一些成熟的、功能丰富的框架,而这些框架通常是一些重量级的库,也许很多功能你根本用不到,而这些需求之外的功能直接导致了代码体积的增长。也许使用Proguard能移除没有使用的代码,但是类之间如果存在复杂的依赖耦合关系,Proguard还能起到多大作用,Who knows. 有人担心以后可能需要用到那些功能,现在的库没法满足需求. OK, 在使用这个库的时候了解是如何设计和实现的,优秀的库都是对扩展开放的,面向接口编程,那么需要新功能的话就扩展它吧,学习其他人造轮子也会提高自己的编程能力。
曾经一个朋友拿着他们公司大约200kb的类似小米游戏下载软件给我看,我简直无法相信,现在一个功能非常简单的apk动不动就是几兆,能做到这个程度,几乎让人怀疑这个软件的是否专业,然而它在低端机器上表现得都非常流畅。我们费劲心思琢磨如何给apk瘦身,结果增加几个大的jar包就打回了原形,我们要增加第三方统计,友盟的百度的都加上,再加上第三方登录,推送,这些sdk再引用了大量的库比如http组件,json解析,安装包一下子膨胀了几兆,难受但是无奈。
一个图片加载框架的加载流程
1. CubeImageView会检查图片是否已经加载过,图片已经加载完成并已经在显示的重复请求直接忽略。
2. 检查图片是否在内存缓存中,如果再内存中,显示内存中的图片。否则,创建一个ImageTask, 传递给 ImageLoader, ImageTaskExecutor会处理这个任务。
3. ImageTaskExecutor 会用后台线程处理这个ImageTask.
4. ImageProvider负责获取图片,如果图片在本地有文件缓存,那么直接从本地文件加载;否则从网络下载,存文件缓存。
5. 获取到图片之后,存内存缓存,通知加载完成。收到加载完成通知后,ImageView就可以显示图片了。
根据这个加载流程,从核心类入手,逐个分析实现这样一个图片加载框架,我们关注的一些细节问题。
核心类功能介绍
CubeImageView
CubeImageView
有多个重载的loadImage
方法,将加载的信息封装到ImageRequest
, 客户端可以指定要加载图片最终显示的宽高。再看下面的代码
1 | private void tryLoadImage() { |
这段代码注释非常清晰,那么有几个问题。
如何判断ImageView是被复用的,如何判断重复请求?
每个CubeImageView
都绑定了一个ImageTask
,ImageTask
对请求进行了封装,内部类ImageViewHolder
使用弱引用持有ImageView. 类似android消息处理中的一个类Message
, 内部实现了一个池,但是正式发布的代码没有启用,作者说是因为不太稳定就没用。如果不使用对象池的话,更简单的做法是使用ImageView的setTag方法绑定请求或者加载地址,来达到防止重复加载或者图片错位的目的。ImageTask
这个成员变量如果为null, 表示是第一次加载图片,并且创建ImageTask
,查询内存缓存,内存缓存未击中,ze 否则的话这个ImageView就是复用的,接着根据url判断重复请求,如果是新的请求,移除原来的绑定。并将真正的加载任务LoadImageTask
从任务队列中移除,取消正在执行的且不是预加载的图片请求。
SimpleTask
SimpleTask
继承Runnable, 是一个抽象类,定义了任务的4种状态,STATE_NEW
, STATE_RUNNING
, STATE_FINISH
, STATE_CANCELLED
, 代表任务不同的执行状态,使用主线程Looper创建静态Handler与主线程通信,通知任务完成或者取消。如果任务执行过程中出错,设置ImageTask
的标记位mFlag, 在回调过程中检查加载是否成功,调用DefaultImageLoadHandler
进行view更新,显示loading图片,下载失败的图片或者正确加载的图片。SimpleTask
有一个mCurrentThread成员,保存当前正在执行的线程,可以在需要的时候方便调用中断退出任务。
LoadImageTask
LoadImageTask
继承SimpleTask
, 实现doInBackground方法,从Disk缓存或者网络取数据。重写onFinish, onCancel方法,onFinish是在主线程中执行的,调用ImageLoadHandler
对图片进行处理和显示,删除任务队列中已经完成或者取消的任务。
1 |
|
1)为什么要使用ConcurrentHashMap保存待执行的任务?
配置线程池DefaultImageTaskExecutor
管理线程,线程池与工作队列BlockingQueue密切相关,其中在工作队列中保存了所有等待执行的任务。所有提交的任务都放入了这个队列,Executor从队列中取出任务执行,用户往队列中添加任务,形成了一个生产者-消费者模型。既然在创建ThreadPoolExecutor时就已经传入了一个BlockingQueue, 为何还需要使用ConcurrentHashMap保存所有的任务呢?原因是ImageLoader启用了生命周期管理,随着Activity或者Fragment生命周期的变迁,在UI从部分可见变为可见时调用pauseWork暂停工作,UI变为可见时调用resumeWork恢复工作,UI变为完全不可见时调用stopWork,将标记位mExitTaskEarly设置为true, 标记位mPauseWork设置为true, 正在执行的任务进入阻塞状态或者退出,已经执行完毕的任务可能在未更新界面的情况下退出,但是这些任务并没有从map里移除,UI从完全不可见变为可见时调用recoverWork, 重新提交map里剩余未执行完的任务。
1 |
|
2)如何退出正在执行的任务,暂停任务和恢复任务?
1 | synchronized (mImageLoader.mPauseWorkLock) { |
调用wait使任务挂起。当条件改变时,调用notifyAll,唤醒所有在mPauseWorkLock上等待的任务。
1 | private void setPause(boolean pause) { |
3)如何配置线程池?
使用一个线程工厂ThreadFactory来创建我们自定义名称的线程,根据cpu的核心数指定ThreadPoolExecutor的corePoolSize和maximumPoolSize,使用一个无界双向队列LinkedBlockingDeque存放任务,设置任务的优先级(FIFO或者LIFO),当队列中没有任务时,设置Timeout退出core线程,防止空闲的线程继续占用系统资源。如何在移动端配置线程池,如何在性能和内存折中,需要去实践对比一下不同的方案。比如Google官方的ThreadSample,对任务进行更细粒度的划分,将图片下载和decode分为两个不同的任务,分别放在下载任务线程池和decode任务线程池中并行执行,这样是能获得性能提升还是会导致性能下降?比如Volley使用一个数组保存启动的工作线程,线程的数目是固定的。
图片复用
比如用户头像,120*120图像下载到本地后,如有80*80的需求,无需再次下载,直接复用120*120图片。那么是如何实现的呢?
假如我需要加载一个地址为http://7xsj0t.com2.z0.glb.qiniucdn.com/IMG_20150613_161908.jpeg?imageView2/2/w/320, 需要先定义一个identity字符串数组,比如[“2/w/120”, “2/w/320”, “2/w/640”], 模式2代表等比缩放, w/120表示宽最多120个像素
请求加载图片时指定图片复用信息,如
1 | mImageView.loadImage(mImageLoader, itemData, sImageReuseInfoManger.create("2/w/120")); |
ImageReuseInfoManger
创建了一个ImageReuseInfo
, 保存可以替换的identity字符串数组。假如当前要加载的图片identity是”2/w/320”, 那么就可以复用disk缓存中identity为”2/w/640”的图片。
在加载完图片保存到缓存中的时候,会将该identity和url拼接成一个关键字作为缓存文件的名称。注意到这个url地址是原始地址去掉identity。
1 | // read from file cache |
最后在取disk缓存时,会检查复用信息,比如想要加载地址是“…/2/w/120”的图片,尝试读取key为“…/2/w/320”的缓存图片,没有的话再尝试下一个key为“…/2/w/640”的图片,查询不到复用的缓存就只能从网络下载。大致就是这样
Executors.newCachedThreadPool有毒 - Executors.newCachedThreadPool() considered harmful
ThreadSample - Google官方最佳实践系列之使用多线程
ImageLoader文档 - Cube SDK官方文档
七牛文档 - 图片基本处理