如何實(shí)現(xiàn)一個圖片加載框架

一、前言

圖片加載的輪子有很多了,Universal-Image-Loader, Picasso, Glide, Fresco等。
網(wǎng)上各種分析和對比文章很多,我們這里就不多作介紹了。

古人云:“紙上得來終覺淺,絕知此事要躬行”。
只看分析,不動手實(shí)踐,終究印象不深。
所以,我們通過手撕一個圖片加載框架,一窺其中奧秘。

話不多說,先來兩張圖暖一下氣氛:

二、 框架命名

命名是比較令人頭疼的一件事。
在反復(fù)翻了單詞表之后,決定用Doodle作為框架的名稱。

Picasso是畫家畢加索的名字,F(xiàn)resco翻譯過來是“壁畫”,比ImageLoader之類的要更有格調(diào);
本來想起Van、Vince之類的,但想想還是不要冒犯這些巨擘了。

Doodle為涂鴉之意,除了單詞本身內(nèi)涵之外,外在也很有趣,很像一個單詞:Google。
這樣的兼具有趣靈魂和好看皮囊的詞,真的不多了。

三、流程&架構(gòu)

3.1 加載流程

概括來說,圖片加載包含封裝,解析,下載,解碼,變換,緩存,顯示等操作。
流程圖如下:


  • 封裝參數(shù):從指定來源,到輸出結(jié)果,中間可能經(jīng)歷很多流程,所以第一件事就是封裝參數(shù),這些參數(shù)會貫穿整個過程;
  • 解析路徑:圖片的來源有多種,格式也不盡相同,需要規(guī)范化;
  • 讀取緩存:為了減少計算,通常都會做緩存;同樣的請求,從緩存中取圖片(Bitmap)即可;
  • 查找文件/下載文件:如果是本地的文件,直接解碼即可;如果是網(wǎng)絡(luò)圖片,需要先下載;
  • 解碼:這一步是整個過程中最復(fù)雜的步驟之一,有不少細(xì)節(jié);
  • 變換:解碼出Bitmap之后,可能還需要做一些變換處理(圓角,濾鏡等);
  • 緩存:得到最終bitmap之后,可以緩存起來,以便下次請求時直接取結(jié)果;
  • 顯示:顯示結(jié)果,可能需要做些動畫(淡入動畫,crossFade等)。

以上簡化版的流程(只是眾多路徑中的一個分支),后面我們將會看到,完善各種細(xì)節(jié)之后,會比這復(fù)雜很多。
但萬事皆由簡入繁,先簡單梳理,后續(xù)再慢慢填充,猶如繪畫,先繪輪廓,再描細(xì)節(jié)。

3.2 基本架構(gòu)

解決復(fù)雜問題,思路都是相似的:分而治之。
參考MVC的思路,我們將框架劃分三層:

  • Interface: 框架入口和外部接口
  • Processor: 邏輯處理層
  • Storage:存儲層,負(fù)責(zé)各種緩存。

具體劃分如下:

  • 外部接口
    Doodle: 提供全局參數(shù)配置,圖片加載入口,以及內(nèi)存緩存接口。
    Config: 全局參數(shù)配置。包括緩存路徑,緩存大小,圖片編碼等參數(shù)。
    Request: 封裝請求參數(shù)。包括數(shù)據(jù)源,解碼參數(shù),行為參數(shù),以及目標(biāo)。

  • 執(zhí)行單元
    Dispatcher : 負(fù)責(zé)請求調(diào)度, 以及結(jié)果顯示。
    Worker: 工作線程,異步執(zhí)行加載,解碼,變換,存儲等。
    Downloader: 負(fù)責(zé)文件下載。
    Source: 解析數(shù)據(jù)源,提供統(tǒng)一的解碼接口。
    Decoder: 負(fù)責(zé)具體的解碼工作。

  • 存儲組件
    MemoryCache: 管理Bitmap緩存。
    DiskCache: 圖片“結(jié)果”的磁盤緩存(原圖由OkHttp緩存)。

四、功能實(shí)現(xiàn)

上一節(jié)分析了流程和架構(gòu),接下來就是在理解流程,了解架構(gòu)的前提下,
先分別實(shí)現(xiàn)關(guān)鍵功能,然后串聯(lián)起來,之后就是不斷地添加功能和完善細(xì)節(jié)。
簡而言之,就是自頂向下分解,自底向上填充。

4.1 API設(shè)計

眾多圖片加載框架中,Picasso和Glide的API是比較友好的。

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

Glide的API和Picasso類似。

當(dāng)參數(shù)較多時,構(gòu)造者模式就可以搬上用場了,其鏈?zhǔn)紸PI能使參數(shù)指定更加清晰,而且更加靈活(隨意組合參數(shù))。
Doodle也用類似的API,而且為了方便理解,有些方法命名也參照Picasso和 Glide。

4.1.1 全局參數(shù)

  • 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
    internal var gifDecoder: GifDecoder? = null
    // ...
    fun setUserAgent(userAgent: String): Config {
        this.userAgent = userAgent
        return this
    }

    fun setDiskCachePath(path: String): Config {
        this.diskCachePath = path
        return this
    }
    // ....
}
  • Doodle
object Doodle {
    fun config() : Config {
        return Config
    }
}
  • 框架初始化
Doodle.config()
        .setDiskCacheCapacity(256L shl 20)
        .setGifDecoder(gifDecoder)

雖然也是鏈?zhǔn)紸PI,但是沒有參照Picasso那樣的構(gòu)造者模式的用法(讀寫分離),因為那種寫法有點(diǎn)麻煩,而且不直觀。
Config是一個單例,除了GifDecoder之外,其他參數(shù)都有默認(rèn)值。

4.1.2 圖片請求

加載圖片:

Doodle.load(url)
        .placeholder(R.drawable.loading)
        .into(topIv)

實(shí)現(xiàn)方式和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

    // 圖片參數(shù)
    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)
        } 
        // ...
   }

    private fun fillSizeAndLoad(targetWidth: Int, targetHeight: Int) {
        viewWidth = targetWidth
        viewHeight = targetHeight
        // ...
        Dispatcher.start(this)
    }
    
    override fun toString(): String {
        val builder = StringBuilder()
        if (!TextUtils.isEmpty(sourceKey)) {
            builder.append("source:").append(sourceKey)
        } else {
            builder.append("path:").append(path)
        }
        // ....
        return builder.toString()
    }
}

Request主要職能是封裝請求參數(shù),參數(shù)可以大約劃分為4類:

  • 1、圖片源;
  • 2、解碼參數(shù):寬高,scaleType,圖片配置(ARGB_8888, RGB_565)等;
  • 3、加載行為:加載優(yōu)先級,緩存策略,占位圖,動畫等;
  • 4、目標(biāo),ImageView或者回調(diào)等。

其中,圖片源和解碼參數(shù)決定了最終的bitmap, 所以,我們拼接這些參數(shù)作為請求的key,這個key會用于緩存的索引和任務(wù)的去重。
拼接參數(shù)后字符串很長,所以需要壓縮成摘要,由于終端上的圖片數(shù)量不會太多,64bit的摘要即可(原理參考《漫談散列函數(shù)》)。

圖片文件的來源,通常有網(wǎng)絡(luò)圖片,drawable/raw資源, assets文件,本地文件等。
當(dāng)然,嚴(yán)格來說,除了網(wǎng)絡(luò)圖片之外,其他都是本地文件,只是有各種形式而已。
Doodle支持三種參數(shù), id(Int), path(String), 和Uri(常見于調(diào)用相機(jī)或者相冊時)。

對于有的圖片源,路徑可能會變化,比如url, 里面可能有一些動態(tài)的參數(shù):

val url = "http://www.xxx.com/a.jpg?t=1521551707"

請求服務(wù)端的時候,其實(shí)返回的是同一張圖片。
但是如果用整個url作為請求的key的一部分,因為動態(tài)參數(shù)的原因,每次請求key都不一樣,會導(dǎo)致緩存失效。
為此,可以將url不變的部分作為制定為圖片源的key:

    val url = "http://www.xxx.com/a.jpg"
    Skate.load(url + "?t=" + System.currentTimeMillis())
            .sourceKey(url)
            .into(testIv);

有點(diǎn)類似Glide的StringSignature。

請求的target最常見的應(yīng)該是ImageView,
此外,有時候需要單純獲取Bitmap,
或者同時獲取Bitmap和ImageView,
抑或是在當(dāng)前線程獲取Bitmap ……
總之,有各種獲取結(jié)果的需求,這些都是設(shè)計API時需要考慮的。

4.2 緩存設(shè)計

幾大圖片加載框架都實(shí)現(xiàn)了緩存,各種文章中,有說二級緩存,有說三級緩存。
其實(shí)從存儲來說,可簡單地分為內(nèi)存緩存和磁盤緩存;
只是同樣是內(nèi)存/磁盤緩存,也有多種形式,例如Glide的“磁盤緩存”就分為“原圖緩存”和“結(jié)果緩存”。

4.2.1 內(nèi)存緩存

為了復(fù)用計算結(jié)果,提高用戶體驗,通常會做bitmap的緩存;
而由于要限制緩存的大小,需要淘汰機(jī)制(通常是LRU策略)。
Android SDK提供了LruCache類,查看源碼,其核心是LinkedHashMap。
為了更好地定制,這里我們不用SDK提供的LruCache,直接用LinkedHashMap,封裝自己的LruCache。

internal class BitmapWrapper(var bitmap: Bitmap) {
    var bytesCount: Int = 0
    init {
        this.bytesCount = Utils.getBytesCount(bitmap)
    }
}
internal object LruCache {
    private val cache = LinkedHashMap<Long, BitmapWrapper>(16, 0.75f, true)
    private var sum: Long = 0
    private val minSize: Long = Runtime.getRuntime().maxMemory() / 32

    @Synchronized
    operator fun get(key: Long?): Bitmap? {
        val wrapper = cache[key]
        return wrapper?.bitmap
    }

    @Synchronized
    fun put(key: Long, bitmap: Bitmap?) {
        val capacity = Config.memoryCacheCapacity
        if (bitmap == null || capacity <= 0) {
            return
        }
        var wrapper: BitmapWrapper? = cache[key]
        if (wrapper == null) {
            wrapper = BitmapWrapper(bitmap)
            cache[key] = wrapper
            sum += wrapper.bytesCount.toLong()
            if (sum > capacity) {
                trimToSize(capacity * 9 / 10)
            }
        }
    }

    private fun trimToSize(size: Long) {
        val iterator = cache.entries.iterator()
        while (iterator.hasNext() && sum > size) {
            val entry = iterator.next()
            val wrapper = entry.value
            WeakCache.put(entry.key, wrapper.bitmap)
            iterator.remove()
            sum -= wrapper.bytesCount.toLong()
        }
    }
}

LinkedHashMap 構(gòu)造函數(shù)的第三個參數(shù):accessOrder,傳入true時, 元素會按訪問順序排列,最后訪問的在遍歷器最后端。
進(jìn)行淘汰時,移除遍歷器前端的元素,直至緩存總大小降低到指定大小以下。

有時候需要加載比較大的圖片,占用內(nèi)存較高,放到LruCache可能會“擠掉”其他一些bitmap;
或者有時候滑動列表生成大量的圖片,也有可能會“擠掉”一些bitmap。
這些被擠出LruCache的bitmap有可能很快又會被用上,但在LruCache中已經(jīng)索引不到了,如果要用,需重新解碼。
值得指出的是,被擠出LruCache的bitmap,在GC時并不一定會被回收,如果bitmap還被引用,則不會被回收;
但是不管是否被回收,在LruCache中都索引不到了。

我們可以將一些可能短暫使用的大圖片,以及這些被擠出LruCache的圖片,放到弱引用的容器中。
在被回收之前,還是可以根據(jù)key去索引到bitmap。

internal object WeakCache {
    private val cache = HashMap<Long, BitmapWeakReference>()
    private val queue = ReferenceQueue<Bitmap>()

    private class BitmapWeakReference internal constructor(
            internal val key: Long,
            bitmap: Bitmap,
            q: ReferenceQueue<Bitmap>) : WeakReference<Bitmap>(bitmap, q)

    private fun cleanQueue() {
        var ref: BitmapWeakReference? = queue.poll() as BitmapWeakReference?
        while (ref != null) {
            cache.remove(ref.key)
            ref = queue.poll() as BitmapWeakReference?
        }
    }

    @Synchronized
    operator fun get(key: Long?): Bitmap? {
        cleanQueue()
        val reference = cache[key]
        return reference?.get()
    }

    @Synchronized
    fun put(key: Long, bitmap: Bitmap?) {
        if (bitmap != null) {
            cleanQueue()
            val reference = cache[key]
            if (reference == null) {
                cache[key] = BitmapWeakReference(key, bitmap, queue)
            }
        }
    }
}

以上實(shí)現(xiàn)中,BitmapWeakReference是WeakReference的子類,除了引用Bitmap的功能之外,還記錄著key, 以及關(guān)聯(lián)了ReferenceQueue;
當(dāng)Bitmap被回收時,BitmapWeakReference會被放入ReferenceQueue,
我們可以遍歷ReferenceQueue,移出ReferenceQueue的同時,取出其中記錄的key, 到cache中移除對應(yīng)的記錄。
利用WeakReference和ReferenceQueue的機(jī)制,索引對象的同時又不至于內(nèi)存泄漏,類似用法在WeakHashMap和Glide源碼中都出現(xiàn)過。

最后,綜合LruCacheWeakCache,統(tǒng)一索引:

internal object MemoryCache {
    fun getBitmap(key: Long): Bitmap? {
        var bitmap = LruCache[key]
        if (bitmap == null) {
            bitmap = WeakCache[key]
        }
        return bitmap
    }

    fun putBitmap(key: Long, bitmap: Bitmap, toWeakCache: Boolean) {
        if (toWeakCache) {
            WeakCache.put(key, bitmap)
        } else {
            LruCache.put(key, bitmap)
        }
    }
    // ......
}

聲明內(nèi)存緩存策略:

object MemoryCacheStrategy{
    const val NONE = 0
    const val WEAK = 1
    const val LRU = 2
}

NONE: 不緩存到內(nèi)存
WEAK: 緩存到WeakCache
LRU:緩存到LruCache

4.2.2 磁盤緩存

前面提到,Glide有兩種磁盤緩存:“原圖緩存”和“結(jié)果緩存”,
Doodle也仿照類似的策略,可以選擇緩存原圖和結(jié)果。
原圖緩存指的是Http請求下來的未經(jīng)解碼的文件;
結(jié)果緩存指經(jīng)過解碼,剪裁,變換等,變成最終的bitmap之后,通過bitmap.compress()壓縮保存。
其中,后者通常比前者更小,而且解碼時不需要再次剪裁和變換等,所以從結(jié)果緩存獲取bitmap通常要比從原圖獲取快得多。

為了盡量使得api相似,Doodle設(shè)置直接用Glide v3的緩存策略定義(Glide v4有一些變化)。

object DiskCacheStrategy {
    const val NONE = 0
    const val SOURCE = 1
    const val RESULT = 2
    const val ALL = 3
}

NONE: 不緩存到磁盤
SOURCE: 只緩存原圖
RESULT: 只緩存結(jié)果
ALL: 既緩存原圖,也緩存結(jié)果。

Doodle的HttpClient是用的OkHttp, 所以網(wǎng)絡(luò)緩存,包括原圖的緩存就交給OkHttp了,
至于本地的圖片源,本就在SD卡,只是各種形式而已,也就無所謂緩存了。

結(jié)果緩存,Doodle沒有用DiskLruCache, 而是自己實(shí)現(xiàn)了磁盤緩存。
DiskLruCache是比較通用的磁盤緩存解決方案,筆者覺得對于簡單地存?zhèn)€圖片文件可以更精簡一些,所以自己設(shè)計了一個更專用的方案。

其實(shí)磁盤緩存的管理最主要是設(shè)計記錄日志,方案要點(diǎn)如下:
1、一條記錄存儲key(long)和最近訪問時間(long),一條記錄16字節(jié);
2、每條記錄依次排列,由于比較規(guī)整,可以根據(jù)偏移量隨機(jī)讀寫;
3、用mmap方式映射日志文件,以4K為單位映射。

文件記錄之外,內(nèi)存中還需要一個HashMap記錄key到"文件記錄"的映射, 其中,文件記錄對象如下:

private class JournalValue internal constructor(
            internal var key: Long,
            internal var accessTime: Long,
            internal var fileLen: Long,
            internal var offset: Int) : Comparable<JournalValue> {
        // ...
    }

只需記錄key, 訪問時間,文件大小,以及記錄在日志文件中的位置即可。

那文件名呢?文件命名為key的十六進(jìn)制,所以可以根據(jù)key運(yùn)算出文件名。

運(yùn)作機(jī)制:
訪問DiskCache時,先讀取日志文件,填充HashMap;
后面的訪問中,只需讀取HashMap就可以知道有沒有對應(yīng)的磁盤緩存;
存入一個“結(jié)果文件”則往HashMap存入記錄,同時更新日志文件。
這種機(jī)制其實(shí)有點(diǎn)像SharePreferences, 二級存儲,文件讀一次之后接下來都是寫入。

該方案的優(yōu)點(diǎn)為:
1、節(jié)省空間,一頁(4K)能記錄256個文件;
2、格式規(guī)整,解析快;
3、mmap映射,可批量記錄,自動定時寫入磁盤,降低磁盤IO消耗;
4、二級存儲,訪問速度快。

當(dāng)容量超出限制需要淘汰時,根據(jù)訪問時間,先刪除最久沒被訪問的文件;
除了實(shí)現(xiàn)LRU淘汰規(guī)則外,還可實(shí)現(xiàn)最大保留時間,刪除一些太久沒用到的圖片文件。

雖然名為磁盤緩存,其實(shí)不僅僅緩存文件,“文件記錄”也很關(guān)鍵,二者關(guān)系猶如文件內(nèi)容和文件的元數(shù)據(jù), 相輔相成。

4.3 解碼

SDK提供了BitmapFactory,提供各種API,從圖片源解碼成bitmap,但這僅是圖片解碼的最基礎(chǔ)的工作;
圖片解碼,前前后后要準(zhǔn)備各種材料,留心各種細(xì)節(jié),是圖片加載過程中最繁瑣的步驟之一。

4.3.1 解析數(shù)據(jù)源

前面提到,圖片的來源有多種,我們需要識別圖片來源,
然后根據(jù)各自的特點(diǎn)提供統(tǒng)一的處理方法,為后續(xù)的具體解碼工作提供方便。

internal abstract class Source : Closeable {
    // 魔數(shù),提供文件格式的信息
    internal abstract val magic: Int
    // 旋轉(zhuǎn)方向,EXIF專屬信息
    internal abstract val orientation: Int

    internal abstract fun decode(options: BitmapFactory.Options): Bitmap?
    internal abstract fun decodeRegion(rect: Rect, options: BitmapFactory.Options): Bitmap?

    internal class FileSource constructor(private val file: File) : Source() {
        //...
    }

    internal class AssetSource(private val assetStream: AssetManager.AssetInputStream) : Source() {
        //...
    }

    internal class StreamSource  constructor(inputStream: InputStream) : Source() {
        //...
    }

    companion object {
        private const val ASSET_PREFIX = "file:///android_asset/"
        private const val FILE_PREFIX = "file://"

        fun valueOf(src: Any?): Source {
            if (src == null) {
                throw IllegalArgumentException("source is null")
            }
            return when (src) {
                is File -> FileSource(src)
                is AssetManager.AssetInputStream -> AssetSource(src)
                is InputStream -> StreamSource(src)
                else -> throw IllegalArgumentException("unsupported source " + src.javaClass.simpleName)
            }
        }

        fun parse(request: Request): Source {
            val path = request.path
            return when {
                path.startsWith("http") -> {
                    val builder = okhttp3.Request.Builder().url(path)
                    if (request.diskCacheStrategy and DiskCacheStrategy.SOURCE == 0) {
                        builder.cacheControl(CacheControl.Builder().noCache().noStore().build())
                    } else if (request.onlyIfCached) {
                        builder.cacheControl(CacheControl.FORCE_CACHE)
                    }
                    valueOf(Downloader.getSource(builder.build()))
                }
                path.startsWith(ASSET_PREFIX) -> valueOf(Doodle.appContext.assets.open(path.substring(ASSET_PREFIX.length)))
                path.startsWith(FILE_PREFIX) -> valueOf(File(path.substring(FILE_PREFIX.length)))
                else -> valueOf(Doodle.appContext.contentResolver.openInputStream((request.uri ?: Uri.parse(path))))
            }
        }
    }
}

以上代碼,從資源id, path, 和Uri等形式,最終轉(zhuǎn)換成FileSource, AssetSource, StreamSource等。

  • FileSource: 本地文件
  • AssetSource:asset文件,drawable/raw資源文件
  • StreamSource:網(wǎng)絡(luò)文件,ContentProvider提供的圖片文件,如相機(jī),相冊等。

其中,網(wǎng)絡(luò)文件從OkHttp的網(wǎng)絡(luò)請求獲得,如果緩存了原圖, 則會獲得FileSource。
其實(shí)各種圖片源最終都可以轉(zhuǎn)化為InputStream,例如AssetInputStream其實(shí)就是InputStream的一種, 文件也可以轉(zhuǎn)化為FileInputStream。
那為什么區(qū)分開來呢? 這一切都要從讀取圖片頭信息開始講。

4.3.2 預(yù)讀頭信息

解碼過程中通常需要預(yù)讀一些頭信息,如文件格式,圖片分辨率等,作為接下來解碼策略的參數(shù),例如用圖片分辨率來計算壓縮比例。
當(dāng)inJustDecodeBounds設(shè)置為true時, BitmapFactory不會返回bitmap, 而是僅僅讀取文件頭信息,其中最重要的是圖片分辨率。

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, options)

讀取了頭信息,計算解碼參數(shù)之后,將inJustDecodeBounds設(shè)置為false,
再次調(diào)用BitmapFactory.decodeStream即可獲取所需bitmap。
可是,有的InputStream不可重置讀取位置,同時BitmapFactory.decodeStream方法要求從頭開始讀取。
那先關(guān)閉流,然后再次打開不可以嗎? 可以,不過效率極低,尤其是網(wǎng)絡(luò)資源時,不敢想象……

有的InputStream實(shí)現(xiàn)了mark(int)和reset()方法,就可以通過標(biāo)記和重置支持重新讀取。
這一類InputStream會重載markSupported()方法,并返回true, 我們可以據(jù)此判斷InputStream是否支持重讀。

幸運(yùn)的是AssetInputStream就支持重讀;
不幸的是FileInputStream居然不支持,OkHttp的byteStream()返回InputStream也不支持。

對于文件,我們通過搭配RandomAccessFile和FileDescriptor來重新讀??;
而對于其他的InputStream,只能曲折一點(diǎn),通過緩存已讀字節(jié)來支持重新讀取。
SDK提供的BufferedInputStream就是這樣一種思路, 通過設(shè)置一定大小的緩沖區(qū),以滑動窗口的形式提供緩沖區(qū)內(nèi)重新讀取。
遺憾的是,BufferedInputStream的mark函數(shù)需指定readlimit,緩沖區(qū)會隨著需要預(yù)讀的長度增加而擴(kuò)容,但是不能超過readlimit;
若超過readlimit,則讀取失敗,從而解碼失敗。

    /**
     * @param readlimit the maximum limit of bytes that can be read before
     *                  the mark position becomes invalid.
     */
    public void mark(int readlimit) {
        marklimit = readlimit;
        markpos = pos;
    }

于是readlimit設(shè)置多少就成了考量的因素了。
Picasso早期版本設(shè)置64K, 結(jié)果遭到大量的反饋說解碼失敗,因為有的圖片需要預(yù)讀的長度不止64K。
從Issue的回復(fù)看,Picasso的作者也很無奈,最終妥協(xié)地將readlimit設(shè)為MAX_INTEGER。
但即便如此,后面還是有反饋有的圖片無法預(yù)讀到圖片的大小。
筆者很幸運(yùn)地遇到了這種情況,經(jīng)調(diào)試代碼,最終發(fā)現(xiàn)Android 6.0的BufferedInputStream,
其skip函數(shù)的實(shí)現(xiàn)有問題,每次skip都會擴(kuò)容,即使skip后的位置還在緩沖區(qū)內(nèi)。
造成的問題是有的圖片預(yù)讀時需多次調(diào)用skip函數(shù),然后緩沖區(qū)就一直double直至拋出OutOfMemoryError……
不過Picasso最終還是把圖片加載出來了,因為其catch了Throwable, 然后重新直接解碼(不預(yù)讀大小);
雖然加載出來了,但是代價不小:只能全尺寸加載,以及前面預(yù)讀時申請的大量內(nèi)存(雖然最終會被GC),所造成的內(nèi)存抖動。

Glide沒有這個問題,因為Glide自己實(shí)現(xiàn)了類似BufferedInputStream功能的InputStream,完美地繞過了這個坑;
Doodle則是copy了Android 8.0的SDK的BufferedInputStream, 精簡代碼,加入一些緩沖區(qū)復(fù)用的代碼等,可以說是改裝版BufferedInputStream。

回頭看前面一節(jié)的問題,為什么不統(tǒng)一用“改裝版BufferedInputStream”來解碼?
因為有的圖片預(yù)讀的長度很長,需要開辟較大的緩沖區(qū),從這個角度看,F(xiàn)ileSource和AssetSource更節(jié)約內(nèi)存。

4.3.3 圖片壓縮

有時候需要顯示的bitmap比原圖的分辨率小。
比方說原圖是 4096 * 4096, 如果按照ARGB_8888的配置全尺寸解碼出來,需要占用64M的內(nèi)存!
不過app中所需的bitmap通常會小很多, 這時就要壓縮了。
比方說需要300 * 300的bitmap, 該怎么做呢?
網(wǎng)上通常的說法是設(shè)置 options.inSampleSize 來降采樣。
閱讀SDK文檔,inSampleSize 需是整數(shù),而且是2的倍數(shù),
不是2的倍數(shù)時,會被 “be rounded down to the nearest power of 2”。
比方說前面的 4096 * 4096 的原圖,
當(dāng)inSampleSize = 16時,解碼出256 * 256 的bitmap;
當(dāng)inSampleSize = 8時,解碼出512 * 512 的bitmap。
即使是inSampleSize = 8,所需內(nèi)存也只有原來的1/64(1M),效果還是很明顯的。

Picasso和Glide v3就是這么降采樣的。
如果你發(fā)現(xiàn)解碼出來的圖片是300 * 300 (比如使用Picasso時調(diào)用了fit()函數(shù)),應(yīng)該是有后續(xù)的處理(通過Matrix 和 Bitmap.createBitmap 繼續(xù)縮放)。

那能否直接解碼出300 * 300的圖片呢? 可以的。
查看 BitmapFactory.cpp 的源碼,其中有一段:

const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
   scale = (float) targetDensity / density;
}

對應(yīng)BitmapFactory.Options的兩個關(guān)鍵參數(shù):inDensity 和 inTargetDensity。
上面的例子,設(shè)置inTargetDensity=300, inDensity=4096(還要設(shè)置inScale=true), 則可解碼出300 * 300的bitmap。
額外提一下,Glide v4也換成這種壓縮策略了。

平時設(shè)計給切圖,要放對文件夾,也是這個道理。
比如設(shè)計給了144 * 144(xxhdpi) 的icon, 如果不小心放到hdpi的資源目錄下;
假如機(jī)器的dpi在320dpi ~ 480dpi之間(xxhdpi),則解碼出來的bitmap是288 * 288的分辨率,;
如果剛好ImageView又是wrap_content設(shè)置的寬高,視覺上會比預(yù)期的翻了一番-_-。

言歸正傳,解碼的過程為,通過獲取圖片的原始分辨率,結(jié)合Request的width和height, 以及ScaleType,
計算出最終要解碼的寬高, 設(shè)置inDensity和inTargetDensity然后decode。
當(dāng)然,有時候decode出來之后還要做一些加工,比方說ScaleType為CENTER_CROP而圖片寬高又不相等,
則需要在decode之后進(jìn)行裁剪,取出中間部分的像素。

關(guān)于ScaleType,Doodle是直接獲取ImageView的ScaleType, 所以無需再特別調(diào)用函數(shù)指定;
當(dāng)然也提供了指定ScaleType的API, 對于target不是ImageView時或許會用到。

fun scaleType(scaleType: ImageView.ScaleType)

還有就是,解碼階段的壓縮是向下采樣的。
比如,如果原圖只有100 * 100, 但是ImageView是200 * 200,最終也是解碼出100 * 100的bitmap。
因為ImageView假如是CENTER_CROP或者FIX_XY等ScaleType,顯示時通常會在渲染階段自行縮放的。
如果確實(shí)就是需要200 * 200的分辨率,可以在解碼后的變換(Transformation)階段處理。

4.3.4 圖片旋轉(zhuǎn)

相信不少開發(fā)都遇到拍照后圖片旋轉(zhuǎn)的問題(尤其是三星的手機(jī))。
網(wǎng)上有不少關(guān)于此問題的解析,這是其中一篇:關(guān)于圖片EXIF信息中旋轉(zhuǎn)參數(shù)Orientation的理解

Android SDK提供了ExifInterface 來獲取Exif信息,Picasso正是用此API獲取旋轉(zhuǎn)參數(shù)的。
很可惜ExifInterface要到 API level 24 才支持通過InputStream構(gòu)造對象,低于此版本,僅支持通過文件路徑構(gòu)造對象。
故此,Picasso當(dāng)前版本僅在傳入?yún)?shù)是文件路徑(或者文件的Uri)時可處理旋轉(zhuǎn)問題。

Glide自己實(shí)現(xiàn)了頭部解析,主要是獲取文件類型和exif旋轉(zhuǎn)信息。
Doodle抽取了Glide的HeaderParser,并結(jié)合工程做了一些精簡和代碼優(yōu)化, 嗯, 又一個“改裝版”。
decode出bitmap之后,根據(jù)獲取的旋轉(zhuǎn)信息,調(diào)用setRotatepostScale進(jìn)行對應(yīng)的旋轉(zhuǎn)和翻轉(zhuǎn),即可還原正確的顯示。

4.3.5 變換

解碼出bitmap之后,有時候還需要做一些處理,如圓形剪裁,圓角,濾鏡等。
Picasso和Glide都提供了類似的API:Transformation

interface Transformation {
    fun transform(source: Bitmap): Bitmap?
    fun key(): String
}

實(shí)現(xiàn)變換比較簡單,實(shí)現(xiàn)Transformation接口,處理source,返回處理后的bitmap即可;
當(dāng)然,還要在key()返回變換的標(biāo)識,通常寫變換的名稱就好,如果有參數(shù), 需拼接上參數(shù)。
Transformation也是決定bitmap長什么樣的因素之一,所以需要重載key(), 作為Request的key的一部分。
Transformation可以設(shè)置多個,處理順序會按照設(shè)置的先后順序執(zhí)行。

Doodle預(yù)置了三個常用的Transformation。
CircleTransformation:圓形剪裁,如果寬高不相等,會先取中間部分(類似CENTER_CROP);
RoundedTransformation:圓角剪裁,可指定半徑;
ResizeTransformation:大小調(diào)整,寬高縮放到指定大小。

需要指出的一點(diǎn)是, Request中指定大小之后并不總是能夠解碼出指定大小的bitmap,
如果原圖分辨率小于指定大小,基于向下采樣的策略,并不會主動縮放到指定的大小(前面有提到)。
若需要確定大小的bitmap, 可應(yīng)用ResizeTransformation。

更多的變換,可以到glide-transformations尋找,
雖然不能直接導(dǎo)入引用, 但是處理方法是類似的,改造一下就可使用。

4.3.6 GIF圖

GIF有靜態(tài)的,也有動態(tài)的。
BitmapFactory支持解碼GIF圖片的第一幀,所以各個圖片框架都支持GIF縮率圖。
至于GIF動圖,Picasso當(dāng)前是不支持的,Glide支持,但據(jù)反饋有些GIF動圖Glide顯示不是很流暢。
Doodle本身也沒有實(shí)現(xiàn)GIF動圖的解碼,但是留了拓展接口,結(jié)合第三方GIF解碼庫, 可實(shí)現(xiàn)GIF動圖的加載和顯示。
GIF解碼庫,推薦 android-gif-drawable。

具體用法:
在App啟動時, 注入GIF解碼的實(shí)現(xiàn)類(實(shí)現(xiàn)GifDecoder 接口):

    fun initApplication(context: Application) {
        Doodle.init(context)
                // ... 其他配置
                .setGifDecoder(gifDecoder)
    }

    private val gifDecoder = object : GifDecoder {
        override fun decode(bytes: ByteArray): Drawable {
            return GifDrawable(bytes)
        }
    }

使用時和加載到普通的ImageView沒區(qū)別,如果圖片源是GIF圖片,會自動調(diào)用gifDecoder進(jìn)行解碼。

Doodle.load(url).into(gifImageView)

當(dāng)然也可以指定不需要顯示動圖, 調(diào)用asBitmap()方法即可。

4.3.7 圖片復(fù)用

很多文章講圖片優(yōu)化時都會提到兩個點(diǎn),壓縮和圖片復(fù)用。
Doodle在設(shè)計階段也考慮了圖片復(fù)用,并且也實(shí)現(xiàn)了,但實(shí)現(xiàn)后一直糾結(jié)其收益和成本-_-

  • 1、正在使用的圖片不能被復(fù)用,所以要添加引用計數(shù)策略,附加代碼很多;
  • 2、即使圖片沒有被引用,根據(jù)局部性原理,該圖片可能稍后有可能被訪問,所以也不應(yīng)該馬上被復(fù)用;
  • 3、大多數(shù)情況下,符合復(fù)用條件(不用一段時間,尺寸符合要求)的并不多;
  • 4、占用一些額外的計算資源。

最終,在看了帖子 picasso_vs_glide 之后,下決心移除了圖片復(fù)用的代碼。
以下該帖子中,Picasso的作者JakeWharton 的原話:

Slight correction here: "Glide reuses bitmaps period". Picasso does not at all. Nor do we have plans to. This is actually a performance optimization in some cases as we can retained cached images longer. It'd be nice to support both modes with programmer hints, but since ImageDecoder doesn't even support re-use I see no point to adding it.

Doodle定位是小而美的輕量級圖片框架,過程中移除了不少價值不高的功能和復(fù)雜的實(shí)現(xiàn)。
有舍必有得,編程與生活,莫不如此。

4.4 線程調(diào)度

圖片獲取和解碼都是耗時的操作,需放在異步執(zhí)行;
而通常需要同時請求多張圖片,故此,線程調(diào)度不可或缺。

Doodle的線程調(diào)度依賴于筆者的另一個項目Task, 具體內(nèi)容詳見:《如何實(shí)現(xiàn)一個線程調(diào)度框架》(又發(fā)了一波廣告?-_-)。
簡單的說,主要用到了Task的幾個特性:

  • 1、支持優(yōu)先級;
  • 2、支持生命周期(在Activity/Fragment銷毀時取消任務(wù));
  • 3、支持根據(jù) Activity/Fragment 的顯示/隱藏動態(tài)調(diào)整優(yōu)先級;
  • 4、支持任務(wù)去重。

關(guān)于任務(wù)去重,主要是以Request的key作為任務(wù)的tag, 相同tag的任務(wù)串行執(zhí)行,
如此,當(dāng)?shù)谝粋€任務(wù)完成,后面的任務(wù)讀緩存即可,避免了重復(fù)計算。
對于網(wǎng)絡(luò)圖片源的任務(wù),則以URL作為tag, 以免重復(fù)下載。
此外,線程池,在UI線程回調(diào)結(jié)果,在當(dāng)前線程獲取結(jié)果等操作,都能基于Task簡單地實(shí)現(xiàn)。

4.5 Dispatcher

從Request,到開始解碼,從解碼完成,到顯示圖片, 之間不少零碎的處理。
把這些處理都放到一個類中,卻不知道怎么命名了,且命名為Dispatcher吧。

都有哪些處理呢?
1、檢查ImageView有沒有綁定任務(wù)(啟動任務(wù)后會將Request放入ImageView的tag中),
如果有,判斷是否相同(根據(jù)請求的key), 相同且前面的任務(wù)在執(zhí)行,則取消之;
2、啟動任務(wù)前顯示占位圖(如果設(shè)置了的話);
3、任務(wù)結(jié)束,如果任務(wù)失敗,顯示錯誤圖片;
4、如果加載成功且設(shè)置了過渡動畫,執(zhí)行動畫;
5、各種target的回調(diào);
6、任務(wù)的暫停和開始。

其中,最后一點(diǎn),在顯示有大量數(shù)據(jù)源的RecycleView或者ListView時,
執(zhí)行快速滑動時最好能暫停任務(wù),停下來才恢復(fù)加載,這樣能節(jié)省很多不必要的請求。

簡而言之,Dispatcher有兩個職責(zé):
1、橋接的作用,連接外部于內(nèi)部組件(有點(diǎn)像主板);
2、處理結(jié)果的反饋(如圖片的顯示)。

五、回顧

第三章梳理了流程和架構(gòu);
第四章分解了各部分功能實(shí)現(xiàn);
這一章我們做一下回顧和梳理。

5.1 依賴關(guān)系

先回顧一下圖片框架的架構(gòu):


  • Doodle作為框架的入口,提供全局參數(shù)配置(Config)以及單個圖片的請求(Request);
  • Request被很多類所依賴,事實(shí)上,Request貫穿了整個請求過程。
    添加功能時,一般也是從Request開始,添加變量和方法,然后在后面的流程中尋找注入點(diǎn),插入控制代碼,完成功能添加。
  • Dispatcher和Worker是相互依賴的關(guān)系,表現(xiàn)為Dispatcher發(fā)起啟動Worker, Worker將結(jié)果反饋給Dispatcher。
  • Downloader給Source提供圖片文件的InputStream, 圖片下載的具體執(zhí)行為Downloader中的OkHttpClient。

整個框架以Doodle為起點(diǎn),以Worker為核心,類之間調(diào)用不會太深, 總體上結(jié)構(gòu)還是比較緊湊的。
了解這幾個類,就基本上了解整個框架的構(gòu)成了。

5.2 執(zhí)行流

這一節(jié),我們結(jié)合各個核心類,再次梳理一下執(zhí)行流程:

上圖依然是簡化版的執(zhí)行流,但弄清楚了基本流程,其他細(xì)枝末節(jié)的流程也都好理解了。

1、圖片加載流程,從框架的Doodle.load()開始,返回Request對象;

object Doodle {
    fun load(path: String): Request {
        return Request(path)
    }
}

2、封裝Request參數(shù)之后,以into收尾,由Dispatcher啟動請求;

class Request {
    fun into(target: ImageView?) 
        fillSizeAndLoad(viewWidth, viewHeight)
    }
    
    private fun fillSizeAndLoad(targetWidth: Int, targetHeight: Int) {
        Dispatcher.start(this)
    }
}

3、先嘗試從內(nèi)存緩存獲取bitmap, 無則開啟異步請求

internal object Dispatcher {
    fun start(request: Request?) {
        val bitmap = MemoryCache.getBitmap(request.key)
        if (bitmap == null) {
            val loader = Worker(request, imageView)
            loader.priority(request.priority)
                    .hostHash(request.hostHash)
                    .execute()
        }
    }
}

4、核心的工作都在Worker中執(zhí)行,包括獲取文件(解析,下載),解碼,變換,及緩存圖片等

internal class Worker(private val request: Request, imageView: ImageView?) : UITask<Void, Void, Any>() {
   private var fromMemory = false
   private var fromDiskCache = false

   override fun doInBackground(vararg params: Void): Any? {
       var bitmap: Bitmap? = null
       var source: Source? = null
       try {
           bitmap = MemoryCache.getBitmap(key) // 檢查內(nèi)存緩存
           if (bitmap == null) {
               val filePath = DiskCache[key] // 檢查磁盤緩存(結(jié)果緩存)
               fromDiskCache = !TextUtils.isEmpty(filePath)
               source = if (fromDiskCache) Source.valueOf(File(filePath!!)) else Source.parse(request) // 解析
               bitmap = Decoder.decode(source, request, fromDiskCache) // 解碼
               bitmap = transform(request, bitmap) // 變換
               if (bitmap != null) {
                   if (request.memoryCacheStrategy != MemoryCacheStrategy.NONE) {
                       val toWeakCache = request.memoryCacheStrategy == MemoryCacheStrategy.WEAK
                       MemoryCache.putBitmap(key, bitmap, toWeakCache) // 緩存到內(nèi)存
                   }
                   if (!fromDiskCache && request.diskCacheStrategy and DiskCacheStrategy.RESULT != 0) {
                       storeResult(key, bitmap) // 緩存到磁盤
                   }
               }
           }
           return bitmap
       } catch (e: Throwable) {
           LogProxy.e(TAG, e)
       } finally {
           Utils.closeQuietly(source)
       }
       return null
   }

   override fun onPostExecute(result: Any?) {
       val imageView = target
       if (imageView != null) {
           imageView.tag = null
       }
       // 顯示結(jié)果
       Dispatcher.feedback(request, imageView, result, false)
   }
}

以上代碼中,有兩點(diǎn)需要提一下:

  • Dispatcher啟動Worker之前已經(jīng)檢查內(nèi)存緩存了,為什么Worker中又檢查一次?
    因為可能存在多個請求的bitmap是相同的(key所決定),只是target不同,然后Worker會串行執(zhí)行這些請求;
    當(dāng)?shù)谝粋€請求結(jié)束,圖片已經(jīng)放到內(nèi)存緩存了,接下來的請求可以從內(nèi)存緩存中直接獲取bitmap,無需再次解碼。
  • 為什么沒有看到Downloader下載文件?
    Downloader出現(xiàn)在Source.parse(request)方法中,主要是返回一個InputStream;
    文件的下載過程在發(fā)生在Decoder.decode()方法中,邊下載邊解碼。

5、回歸Dispatcher, 刷新ImageView

internal object Dispatcher {
    fun feedback(request: Request, imageView: ImageView? ...) {
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap)
        } 
    }
}

六、API

前面說了這么多實(shí)現(xiàn)細(xì)節(jié),那到底最終都實(shí)現(xiàn)了些什么功能呢?
看有什么功能,看接口層的三個類就可以了。

6.1 Doodle (框架入口)

方法 作用
init() : Config 返回全局配置對象
trimMemory(int) 整理內(nèi)存(LruCache),傳入ComponentCallbacks2的不同level有不同的策略
clearMemory() 移除LruCache中所有bitmap
load(String): Request 傳入圖片路徑,返回Request
load(int): Request 傳入資源ID,返回Request
load(Uri): Request 傳入URI,返回Request
downloadOnly(String): File? 僅下載圖片文件,不解碼。此方法會走網(wǎng)絡(luò)請求,不可再UI線程調(diào)用
getSourceCacheFile(url: String): File? 獲取原圖緩存,無則返回null。不走網(wǎng)絡(luò)請求,可以在UI線程調(diào)用
cacheBitmap(String,Bitmap,Boolean) 緩存bitmap到Doodle的MemoryCache, 相當(dāng)于開放MemoryCache, 復(fù)用代碼,統(tǒng)一管理。
getCacheBitmap(String): Bitmap? 獲取緩存在Cache中的bitmap
pauseRequest() 暫停往任務(wù)隊列中插入請求,對RecycleView快速滑動等場景,可調(diào)用此函數(shù)
resumeRequest() 恢復(fù)請求
notifyEvent(Any, int) 發(fā)送頁面生命周期事件(通知頁面銷毀以取消請求等)

6.2 Config (全局配置)

方法 作用
setUserAgent(String) 設(shè)置User-Agent頭,網(wǎng)絡(luò)請求將自動填上此Header
setDiskCachePath(String) 設(shè)置結(jié)果緩存的存儲路徑
setDiskCacheCapacity(Long) 設(shè)置結(jié)果緩存的容量
setDiskCacheMaxAge(Long) 設(shè)置結(jié)果緩存的最大保留時間(從最近一次訪問算起),默認(rèn)30天
setSourceCacheCapacity(Long) 設(shè)置原圖緩存的容量
setMemoryCacheCapacity(Long) 設(shè)置內(nèi)存緩存的容量,默認(rèn)為maxMemory的1/6
setCompressFormat(Bitmap.CompressFormat) 設(shè)置結(jié)果緩存的壓縮格式, 默認(rèn)為PNG
setDefaultBitmapConfig(Bitmap.Config) 設(shè)置默認(rèn)的Bitmap.Config,默認(rèn)為ARGB_8888
setGifDecoder(GifDecoder) 設(shè)置GIF解碼器

6.3 Request (圖片請求)

方法 作用
sourceKey(String) 設(shè)置數(shù)據(jù)源的key
url默認(rèn)情況下作為Request的key的一部分,有時候url有動態(tài)的參數(shù),使得url頻繁變化,從而無法緩存。此時可以設(shè)置sourceKey,提到path作為Request的key的一部分。
override(int, int) 指定剪裁大小
并不最終bitmap等大小并不一定等于override指定的大?。▋?yōu)先按照 ScaleType剪裁,向下采樣),若需確切大小的bitmap可配合ResizeTransformation實(shí)現(xiàn)。
scaleType(ImageView.ScaleType) 指定縮放類型
如果target為ImageView則會自動從ImageView獲取。
memoryCacheStrategy(int) 設(shè)置內(nèi)存緩存策略,默認(rèn)LRU策略
diskCacheStrategy(int) 設(shè)置磁盤緩存策略,默認(rèn)ALL
noCache() 不做任何緩存,包括磁盤緩存和內(nèi)存緩存
onlyIfCached(boolean) 指定網(wǎng)絡(luò)請求是否只從緩存讀?。ㄔ瓐D緩存)
noClip() 直接解碼,不做剪裁和壓縮
config(Bitmap.Config) 指定單個請求的Bitmap.Config
transform(Transformation) 設(shè)置解碼后的圖片變換,可以連續(xù)調(diào)用(會按順序執(zhí)行)
priority(int) 請求優(yōu)先級
keepOriginalDrawable() 默認(rèn)情況下請求開始會先清空ImageView之前的Drawable, 調(diào)用此方法后會保留之前的Drawable
placeholder(int) 設(shè)置占位圖,在結(jié)果加載完成之前會顯示此drawable
placeholder(Drawable) 同上
error(int) 設(shè)置加載失敗后的占位圖
error(Drawable) 同上
goneIfMiss() 加載失敗后imageView.visibility = View.GONE
animation(int) 設(shè)置加載成功后的過渡動畫
animation(Animation) 同上
fadeIn(int) 加載成功后顯示淡入動畫
crossFate(int) 這個動畫效果是原圖從透明度100到0, bitmap從0到100。
當(dāng)設(shè)置placeholder且內(nèi)存緩存中沒有指定圖片時, placeholder為原圖。
如果沒有設(shè)置placeholder, 效果和fadeIn差不多。
需要注意的是,這個動畫在原圖和bitmap寬高不相等時,動畫結(jié)束時圖片會變形。
因此,慎用crossFade。
alwaysAnimation(Boolean) 默認(rèn)情況下僅在圖片是從磁盤或者網(wǎng)絡(luò)加載出來時才做動畫,可通過此方法設(shè)置總是做動畫
asBitmap() 當(dāng)設(shè)置了GifDecoder時,默認(rèn)情況下只要圖片是GIF圖片,則用GifDecoder解碼。調(diào)用此方法后,只取Gif文件第一幀,返回bitmap
host(Any) 傳入宿主(Activity/Fragment), 以觀察其生命周期
preLoad() 預(yù)加載
get(long) : Bitmap? 當(dāng)前線程獲取圖片,加載時阻塞當(dāng)前線程, 可設(shè)定timeout時間(默認(rèn)3000ms),超時未完成則取消任務(wù),返回null。
into(SimpleTarget) 加載圖片后通過SimpleTarget回調(diào)圖片(加載時不阻塞當(dāng)前線程)
into(ImageView, Callback) 加載圖片圖片到ImageView,同時通過Callback回調(diào)。如果Callback中返回true, 說明已經(jīng)處理該bitmap了,則Doodle不會再setBitmap到ImageView了。
into(ImageView?) 加載圖片圖片到ImageView

七、總結(jié)

本文從架構(gòu),流程等方面入手,梳理圖片加載框架的脈絡(luò),以及介紹了其中部分細(xì)節(jié)。
從文中可以看出,實(shí)現(xiàn)過程大量借鑒了Glide和Picasso, 在此對Glide和Picasso的開源工作者表示敬意和感謝。
這里就不做太詳細(xì)的對比了,這里只比較下方法數(shù)和包大?。üδ芎托阅懿惶帽容^)。

框架 版本 方法數(shù) 包大小
Glide 4.8.0 3193 691k
Picasso 2.71828 527 119k
Doodle 1.1.0 442 104k

Doodle先是用Java實(shí)現(xiàn)的,后面用Kotlin改寫,方法數(shù)從200多增加到400多,包大小從60多K增加到100K(用kotlin改寫library, 包大小會增加50%左右)。

麻雀雖小,五臟俱全。
Doodle在完備度上是不輸Picasso的,并且相對前二者有不少微創(chuàng)新。
而相對于Glide, Doodle則是較輕量。
對于大小敏感的項目,或可嘗試一下這個框架。

感興趣的讀者可以參與進(jìn)來,歡迎提建議和提代碼。

項目已發(fā)布到j(luò)center和github, 項目地址:https://github.com/BillyWei001/Doodle
看多遍不如跑一遍,可以Download下來運(yùn)行一下,會比看文章有更多的收獲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容