一、前言
圖片加載的輪子有很多了,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)過。
最后,綜合LruCache和WeakCache,統(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)用setRotate和postScale進(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)行一下,會比看文章有更多的收獲。