如何实现一个图片加载框架

一、前言

图片加载的轮子有很多了,Universal-Image-Loader, Picasso, Glide, Fresco等。
网上各种分析和对比文章很多,我们这里就不多作介绍了。

古人云:“纸上得来终觉浅,绝知此事要躬行”。
只看分析,不动手实践,终究印象不深。
用当下流行的“神经网络”来说,就是要通过“输出”,形成“反馈”,才能更有效地“训练”。

当然,大千世界,包罗万象,我们不可能任何事情都去经历。
能挑自己感兴趣的方面探究一番,已经幸事。

图片加载是笔者比较感兴趣的,其中有不少知识和技巧值得研究探讨。

话不多说,先来两张图暖一下气氛:

暖场结束,我们开始吧:

二、 框架命名

命名是比较令人头疼的一件事。
在反复翻了单词表之后,决定用Doodle作为框架的名称。

Picasso是画家毕加索的名字,Fresco翻译过来是“壁画”,比ImageLoader之类的要更有格调;
本来想起Van、Vince之类的,但想想还是不要冒犯这些巨擘了。

Doodle为涂鸦之意,除了单词本身内涵之外,外在也很有趣,很像一个单词:Google。
这样的兼具有趣灵魂和好看皮囊的词,真的不多了。

三、流程&架构

3.1 加载流程

概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。
流程图如下:

  • 封装参数:从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;
  • 解析路径:图片的来源有多种,格式也不尽相同,需要规范化;
  • 读取缓存:为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;
  • 查找文件/下载文件:如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;
  • 解码:这一步是整个过程中最复杂的步骤之一,有不少细节;
  • 变换:解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等);
  • 缓存:得到最终bitmap之后,可以缓存起来,一遍下次请求时直接取结果;
  • 显示:显示结果,可能需要做些动画(淡入动画,crossFade等)。

以上简化版的流程(只是众多路径中的一个分支),后面我们将会看到,完善各种细节之后,会比这复杂很多。
但万事皆由简入繁,先简单梳理,后续再慢慢填充,犹如绘画,先绘轮廓,再描细节。

3.2 基本架构

解决复杂问题,思路都是相似的:分而治之。
参考MVC的思路,我们将框架划分三层:

  • Interface: 框架入口和外部接口
  • Processor: 逻辑处理层
  • Storage:存储层,负责各种缓存。

具体划分如下:

  • 外部接口
    Doodle: 提供全局参数配置,图片加载入口,以及内存缓存接口。
    Config: 全局参数配置。包括缓存路径,缓存大小,图片编码等参数。
    Request: 封装请求参数。包括数据源,解码参数,行为参数,以及目标。

  • 执行单元
    Dispatcher : 负责请求调度, 以及结果显示。
    Worker: 工作线程,异步执行加载,解码,变换,存储等。
    Downloader: 负责文件下载。
    Source: 解析数据源,提供统一的解码接口。
    Decoder: 负责具体的解码工作。

  • 存储组件
    MemoryCache: 管理Bitmap缓存。
    DiskCache: 图片“结果”的磁盘缓存(原图由OkHttp缓存)。

四、功能实现

上一节分析了流程和架构,接下来就是在理解流程,了解架构的前提下,
先分别实现关键功能,然后串联起来,之后就是不断地添加功能和完善细节。
简而言之,就是自顶向下分解,自底向上填充。

4.1 API设计

众多图片加载框架中,Picasso和Glide的API是比较友好的。

Picasso.with(context)
		.load(url)
		.placeholder(R.drawable.loading)
		.into(imageView);

Glide的API和Picasso类似。

当参数较多时,构造者模式就可以搬上用场了,其链式API能使参数指定更加清晰,而且更加灵活(随意组合参数)。
Doodle也用类似的API,而且为了方便理解,有些方法命名也参照Picasso和 Glide。

4.1.1 全局参数

  • Config
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
    }
    // ....
}
  • Doodle
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了。

4.1.2 图片请求

加载图片:

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)
    }
}
  • Request
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)
		

你可能感兴趣的:(淘沙,Android,图片加载框架,开源)