图片加载的轮子有很多了,Universal-Image-Loader, Picasso, Glide, Fresco等。
网上各种分析和对比文章很多,我们这里就不多作介绍了。
古人云:“纸上得来终觉浅,绝知此事要躬行”。
只看分析,不动手实践,终究印象不深。
用当下流行的“神经网络”来说,就是要通过“输出”,形成“反馈”,才能更有效地“训练”。
当然,大千世界,包罗万象,我们不可能任何事情都去经历。
能挑自己感兴趣的方面探究一番,已经幸事。
图片加载是笔者比较感兴趣的,其中有不少知识和技巧值得研究探讨。
话不多说,先来两张图暖一下气氛:
暖场结束,我们开始吧:
命名是比较令人头疼的一件事。
在反复翻了单词表之后,决定用Doodle作为框架的名称。
Picasso是画家毕加索的名字,Fresco翻译过来是“壁画”,比ImageLoader之类的要更有格调;
本来想起Van、Vince之类的,但想想还是不要冒犯这些巨擘了。
Doodle为涂鸦之意,除了单词本身内涵之外,外在也很有趣,很像一个单词:Google。
这样的兼具有趣灵魂和好看皮囊的词,真的不多了。
概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。
流程图如下:
以上简化版的流程(只是众多路径中的一个分支),后面我们将会看到,完善各种细节之后,会比这复杂很多。
但万事皆由简入繁,先简单梳理,后续再慢慢填充,犹如绘画,先绘轮廓,再描细节。
解决复杂问题,思路都是相似的:分而治之。
参考MVC的思路,我们将框架划分三层:
具体划分如下:
外部接口
Doodle: 提供全局参数配置,图片加载入口,以及内存缓存接口。
Config: 全局参数配置。包括缓存路径,缓存大小,图片编码等参数。
Request: 封装请求参数。包括数据源,解码参数,行为参数,以及目标。
执行单元
Dispatcher : 负责请求调度, 以及结果显示。
Worker: 工作线程,异步执行加载,解码,变换,存储等。
Downloader: 负责文件下载。
Source: 解析数据源,提供统一的解码接口。
Decoder: 负责具体的解码工作。
存储组件
MemoryCache: 管理Bitmap缓存。
DiskCache: 图片“结果”的磁盘缓存(原图由OkHttp缓存)。
上一节分析了流程和架构,接下来就是在理解流程,了解架构的前提下,
先分别实现关键功能,然后串联起来,之后就是不断地添加功能和完善细节。
简而言之,就是自顶向下分解,自底向上填充。
众多图片加载框架中,Picasso和Glide的API是比较友好的。
Picasso.with(context)
.load(url)
.placeholder(R.drawable.loading)
.into(imageView);
Glide的API和Picasso类似。
当参数较多时,构造者模式就可以搬上用场了,其链式API能使参数指定更加清晰,而且更加灵活(随意组合参数)。
Doodle也用类似的API,而且为了方便理解,有些方法命名也参照Picasso和 Glide。
object Config {
internal var userAgent: String = ""
internal var diskCachePath: String = ""
internal var diskCacheCapacity: Long = 128L shl 20
internal var diskCacheMaxAge: Long = 30 * 24 * 3600 * 1000L
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888
// ...
fun setUserAgent(userAgent: String): Config {
this.userAgent = userAgent
return this
}
fun setDiskCachePath(path: String): Config {
this.diskCachePath = path
return this
}
// ....
}
object Doodle {
internal lateinit var appContext: Context
fun init(context: Context) : Config {
appContext = context as? Application ?: context.applicationContext
registerActivityLifeCycle(appContext)
return Config
}
}
Doodle.init(context)
.setDiskCacheCapacity(256L shl 20)
.setMemoryCacheCapacity(128L shl 20)
.setDefaultBitmapConfig(Bitmap.Config.ARGB_8888)
虽然也是链式API,但是没有参照Picasso那样的构造者模式的用法(读写分离),因为那种写法有点麻烦,而且不直观。
Doodle在初始化的时候传入context(最好传入Application), 这样后面请求单个图片时,就不用像Picasso和Glide那样用with传context了。
加载图片:
Doodle.load(url)
.placeholder(R.drawable.loading)
.into(topIv)
实现方式和Config是类似的:
object Doodle {
// ....
fun load(path: String): Request {
return Request(path)
}
fun load(resID: Int): Request {
return Request(resID)
}
fun load(uri: Uri): Request {
return Request(uri)
}
}
class Request {
internal val key: Long by lazy {
MHash.hash64(toString()) }
// 图片源
internal var uri: Uri? = null
internal var path: String
private var sourceKey: String? = null
// 图片参数
internal var viewWidth: Int = 0
internal var viewHeight: Int = 0
// ....
// 加载行为
internal var priority = Priority.NORMAL
internal var memoryCacheStrategy= MemoryCacheStrategy.LRU
internal var diskCacheStrategy = DiskCacheStrategy.ALL
// ....
// target
internal var simpleTarget: SimpleTarget? = null
internal var targetReference: WeakReference<ImageView>? = null
internal constructor(path: String) {
if (TextUtils.isEmpty(path)) {
this.path = ""
} else {
this.path = if (path.startsWith("http") || path.contains("://")) path else "file://$path"
}
}
fun sourceKey(sourceKey: String): Request {
this.sourceKey = sourceKey
return this
}
fun into(target: ImageView?) {
if (target == null) {
return
}
targetReference = WeakReference(target)
if (noClip) {
fillSizeAndLoad(0, 0)
} else if (viewWidth > 0 && viewHeight > 0) {
fillSizeAndLoad(viewWidth, viewHeight)