Coil——新一代的图片加载库好在哪里?

前言

最近一直在整理草稿箱,发现两年前记载过Coil图片加载框架的笔记以及部分源码分析,相比于似乎是上个时代的Fresco,Coil似乎是新时代产物:轻量,易拓展,优雅,完全的贴合kotlin语言特性...因此,我觉得还是整理一些笔记同时也重温一下coil,毕竟虽然认识很久,但是实际使用还是不多。

本文分析的源码是Coil 2.x分支的最新代码

一张图片加载的过程

不管怎样的图片加载框架,无非是这么几个部分:

  • 图片加载请求构造
  • 内存缓存策略
  • 网络请求
  • 磁盘缓存策略
  • 解码逻辑
  • 数据(bitmap/drawable)转换
  • 图片显示

而只要走完一张图片的加载过程,那么以上这些部分的逻辑都会经历到。

因此,我们就跟踪一张图片的加载过程,来看看Coil的结构。

先把Coil的源码项目下载下来,把coil-sample-view这个demo跑起来,里面就有现成的图片加载的代码调用。

//ImageListAdapter.kt
...
...
holder.image.apply {
    ...
    load(item.uri) {
        placeholder(ColorDrawable(item.color))
        error(ColorDrawable(Color.RED))
        parameters(item.parameters)
        listener { _, result -> placeholder = result.memoryCacheKey }
    }
}
...

这是一个比较典型的图片加载调用,load方法是ImageView的拓展方法(kotlin语法特性)

inline fun ImageView.load(
    data: Any?,
    imageLoader: ImageLoader = context.imageLoader, 
    builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
//ImageRequest.builder config
    val request = ImageRequest.Builder(context)
        .data(data)
        .target(this)
        .apply(builder)
        .build()
    return imageLoader.enqueue(request)
}

构造ImageRequest图片加载请求,然后通过imageLoader添加到请求队列中。这个imageloader是全局单例,创建的过程在Demo的Application中,主要是ImageLoader初始化配置:

override fun newImageLoader(): ImageLoader {
    return ImageLoader.Builder(this)
        .components {
            // GIFs
            if (SDK_INT >= 28) {
                add(ImageDecoderDecoder.Factory())// 解码器
            } else {
                add(GifDecoder.Factory())
            }
            // SVGs
            add(SvgDecoder.Factory())
            // Video frames
            add(VideoFrameDecoder.Factory())
        }// 内存缓存设置
        .memoryCache {
            MemoryCache.Builder(this)
                // Set the max size to 25% of the app's available memory.
                .maxSizePercent(0.25)
                .build()
        }// 磁盘缓存设置
        .diskCache {
            DiskCache.Builder()
                .directory(filesDir.resolve("image_cache"))
                .maxSizeBytes(512L * 1024 * 1024) // 512MB
                .build()
        }// 图片请求框架
        .okHttpClient {
            val dispatcher = Dispatcher().apply { maxRequestsPerHost = maxRequests }
            OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .build()
        }

        .crossfade(true)
        // 忽略网络缓存(cache-control),总是读取自己的磁盘缓存或者网络数据
        .respectCacheHeaders(false)
        .apply { if (BuildConfig.DEBUG) logger(DebugLogger()) }
        .build()
}

以上配置是官方给出的demo配置,不去细说,我们直接进入imageLoader.enqueue的方法:

// RealImageLoader.kt

internal class RealImageLoader(
...
// 这些差不多是ImageLoader整个框架核心配置所在
    override val components = componentRegistry.newBuilder()
        // 数据类型转换
        .add(HttpUrlMapper()) // 把HttpUrl转换为string
        .add(StringMapper()) // string数据转换为Uri   网络请求一般传入string
        .add(FileUriMapper())
        .add(ResourceUriMapper())
        .add(ResourceIntMapper())
        .add(ByteArrayMapper())
        // Keyers 缓存的keys
        .add(UriKeyer())
        .add(FileKeyer(options.addLastModifiedToFileCacheKey))
        // 数据来源提取(来源可以是网络,文件,或者asset等)
        .add(HttpUriFetcher.Factory(callFactoryLazy, diskCacheLazy, options.respectCacheHeaders))
        .add(FileFetcher.Factory())
        .add(AssetUriFetcher.Factory())
        .add(ContentUriFetcher.Factory())
        .add(ResourceUriFetcher.Factory())
        .add(DrawableFetcher.Factory())
        .add(BitmapFetcher.Factory())
        .add(ByteBufferFetcher.Factory())
        //解码器
        .add(BitmapFactoryDecoder.Factory(options.bitmapFactoryMaxParallelism, options.bitmapFactoryExifOrientationPolicy))
        .build()
        // 拦截器,目前只有EngineInterceptor,因为用户端没有设置
    private val interceptors = components.interceptors +
        EngineInterceptor(this, systemCallbacks, requestService, logger)
        
 ...       
override fun enqueue(request: ImageRequest): Disposable {
    val job = scope.async {
        executeMain(request, REQUEST_TYPE_ENQUEUE).also { result ->
            if (result is ErrorResult) logger?.log(TAG, result.throwable)
        }
    }
    ...
}

    @MainThread
    private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
        ...
        val request = initialRequest.newBuilder().defaults(defaults).build()
        try {
            ...
            // 正式处理请求,协程
            val result = withContext(request.interceptorDispatcher) {
                RealInterceptorChain(
                    initialRequest = request,
                    interceptors = interceptors,
                    index = 0,
                    request = request,
                    size = size,
                    eventListener = eventListener,
                    isPlaceholderCached = placeholderBitmap != null
                ).proceed(request)
            }

            // 获取数据成功或失败的回调
            when (result) {
                is SuccessResult -> onSuccess(result, request.target, eventListener)
                is ErrorResult -> onError(result, request.target, eventListener)
            }
            return result
        } catch (throwable: Throwable) {
            
        }
    }
}

于是我们接着跟进到RealInterceptorChain.proceed方法:

// RealInterceptorChain.kt

override suspend fun proceed(request: ImageRequest): ImageResult {
    if (index > 0) checkRequest(request, interceptors[index - 1])
    val interceptor = interceptors[index]
    val next = copy(index = index + 1, request = request)
    // 进入拦截器 也就是EngineInterceptor
    val result = interceptor.intercept(next)
    return result
}

进入EngineInterceptor拦截器处理请求的逻辑(如果嫌太长可以只看intercept和execute方法即可)。


internal class EngineInterceptor(
    private val imageLoader: ImageLoader,
    private val systemCallbacks: SystemCallbacks,
    private val requestService: RequestService,
    private val logger: Logger?,
) : Interceptor {
    
    private val memoryCacheService = MemoryCacheService(imageLoader, requestService, logger)

    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        try {
            val request = chain.request
            val data = request.data
            val size = chain.size
            val eventListener = chain.eventListener
            val options = requestService.options(request, size)
            val scale = options.scale

        /******************** 内存缓存逻辑  ****************************/
            // 请求数据类型转换(string --> Uri)
            val mappedData = imageLoader.components.map(data, options)
            // 通过mappedData构造cache key (UriKeyer)
            val cacheKey = memoryCacheService.newCacheKey(request, mappedData, options, eventListener)
            // 通过cache key尝试获取内存缓存
            val cacheValue = cacheKey?.let { memoryCacheService.getCacheValue(request, it, size, scale) }

            // 如果有内存缓存,直接用,不必请求数据
            if (cacheValue != null) {
                return memoryCacheService.newResult(chain, request, cacheKey, cacheValue)
            }

              //没有缓存,就往下进行图片请求的逻辑
            return withContext(request.fetcherDispatcher) {
                // Fetch Image execute方法就在下面
                val result = execute(request, mappedData, options, eventListener)
                ....

                //拿到了数据,就保存在内存缓存中
                val isCached = memoryCacheService.setCacheValue(cacheKey, request, result)

                // Return the result.
               ...
            }
        } catch (throwable: Throwable) {
            ...
        }
    }


    private suspend fun execute(
        request: ImageRequest,
        mappedData: Any,
        _options: Options,
        eventListener: EventListener
    ): ExecuteResult {
        var options = _options
        var components = imageLoader.components
        var fetchResult: FetchResult? = null
        val executeResult = try {
            options = requestService.updateOptionsOnWorkerThread(options)

            // Fetch the data.
            fetchResult = fetch(components, request, mappedData, options, eventListener)

            //fetch返回的数据有两种,
            // SourceResult,可以理解为未解码的图片数据(网络请求返回这种),
            // DrawableResult 可以理解为解码后的可以直接展示的数据
            when (fetchResult) {
            //
                is SourceResult -> withContext(request.decoderDispatcher) {
                // 未解码图片数据,赶紧进行解码,解码之后就可以进行后面的处理
                    decode(fetchResult, components, request, mappedData, options, eventListener)
                }
                is DrawableResult -> {
                    ExecuteResult(
                        drawable = fetchResult.drawable,
                        isSampled = fetchResult.isSampled,
                        dataSource = fetchResult.dataSource,
                        diskCacheKey = null // This result has no file source.
                    )
                }
            }
        } 
        ...

        // 解码后的图片数据,进行转换(裁剪能圆形,或者圆角等)
        val finalResult = transform(executeResult, request, options, eventListener)
       //构建GPU绘制位图的相关缓存?在解码和转换完成之后,调用这个可以优化显示速度(之前也没咋接触过)
        (finalResult.drawable as? BitmapDrawable)?.bitmap?.prepareToDraw()
        return finalResult
    }

    // fetch image
    private suspend fun fetch(
        components: ComponentRegistry,
        request: ImageRequest,
        mappedData: Any,
        options: Options,
        eventListener: EventListener
    ): FetchResult {
        val fetchResult: FetchResult
        var searchIndex = 0
        while (true) {
            //mappedData是Uri,那么自然会选择到  HttpUriFetcher
            val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
           
            val fetcher = pair.first
            searchIndex = pair.second + 1

            ...
            val result = fetcher.fetch() //HttpUriFetcher.fetch()
            ...
        }
        return fetchResult
    }
    // 解码过程
    private suspend fun decode(
        fetchResult: SourceResult,
        components: ComponentRegistry,
        request: ImageRequest,
        mappedData: Any,
        options: Options,
        eventListener: EventListener
    ): ExecuteResult {
        val decodeResult: DecodeResult
        var searchIndex = 0
        while (true) {
            // 就是Application中传入的解码器+Coil内置的系统原生解码器里面选
            
            val pair = components.newDecoder(fetchResult, options, imageLoader, searchIndex)

            val decoder = pair.first
            searchIndex = pair.second + 1
            // 默认当然是原生解码器
            val result = decoder.decode()

            if (result != null) {
                decodeResult = result
                break
            }
        }

        // 解码完成之后就是一个可以绘制的图片了
        return ExecuteResult(
            drawable = decodeResult.drawable,
            isSampled = decodeResult.isSampled,
            dataSource = fetchResult.dataSource,
            diskCacheKey = (fetchResult.source as? FileImageSource)?.diskCacheKey
        )
    }


    @VisibleForTesting
    internal suspend fun transform(
        result: ExecuteResult,
        request: ImageRequest,
        options: Options,
        eventListener: EventListener
    ): ExecuteResult {
        //由于demo中并没有添加额外的图片处理,因此会直接返回
        val transformations = request.transformations
        if (transformations.isEmpty()) return result
        ...
        ...
        // 进行图片处理,具体的图片处理可以看 CircleCropTransformation
        return withContext(request.transformationDispatcher) {
            val input = convertDrawableToBitmap(result.drawable, options, transformations)
            eventListener.transformStart(request, input)
            val output = transformations.foldIndices(input) { bitmap, transformation ->
                transformation.transform(bitmap, options.size).also { ensureActive() }
            }
            eventListener.transformEnd(request, output)
            result.copy(drawable = output.toDrawable(request.context))
        }
    }
    ...
    ...
}

果然,一个EngineInterceptor拦截器基本上把图片加载的主要流程都涵盖了,缓存,网络请求,解码,图形转换。

但是我们还没走到网络请求,接着往下看HttpUriFetcher.fetch方法的实现


// HttpUriFetcher.kt
internal class HttpUriFetcher(
    private val url: String,
    private val options: Options,
    private val callFactory: Lazy<Call.Factory>,
    private val diskCache: Lazy<DiskCache?>,
    private val respectCacheHeaders: Boolean
) : Fetcher {

    override suspend fun fetch(): FetchResult {
    /**************************** 磁盘缓存逻辑  ******************************/
        // 先读取磁盘缓存
        var snapshot = readFromDiskCache()
        try {
            val cacheStrategy: CacheStrategy
            if (snapshot != null) {
                if (fileSystem.metadata(snapshot.metadata).size == 0L) {
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, null),
                        dataSource = DataSource.DISK
                    )
                }

                if (respectCacheHeaders) {
                    cacheStrategy = CacheStrategy.Factory(newRequest(), snapshot.toCacheResponse()).compute()
                    if (cacheStrategy.networkRequest == null && cacheStrategy.cacheResponse != null) {
                        return SourceResult(
                            source = snapshot.toImageSource(),
                            mimeType = getMimeType(url, cacheStrategy.cacheResponse.contentType),
                            dataSource = DataSource.DISK
                        )
                    }
                } else {
                    // Skip checking the cache headers if the option is disabled.
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, snapshot.toCacheResponse()?.contentType),
                        dataSource = DataSource.DISK
                    )
                }
            } else {
                cacheStrategy = CacheStrategy.Factory(newRequest(), null).compute()
            }



            // 磁盘缓存没走到,就走网络请求的逻辑 executeNetworkRequest
            var response = executeNetworkRequest(cacheStrategy.networkRequest!!)
            var responseBody = response.requireBody()
            try {
                // 把网络请求回来的数据写入磁盘缓存
                // 然后再读出来使用
                snapshot = writeToDiskCache(
                    snapshot = snapshot,
                    request = cacheStrategy.networkRequest,
                    response = response,
                    cacheResponse = cacheStrategy.cacheResponse
                )
                if (snapshot != null) {
                // 返回数据
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, snapshot.toCacheResponse()?.contentType),
                        dataSource = DataSource.NETWORK
                    )
                }
                ...
                ...
            } catch (e: Exception) {
                response.closeQuietly()
                throw e
            }
        } catch (e: Exception) {
            snapshot?.closeQuietly()
            throw e
        }
    }
    ...
    ...
}

我个人对这里的逻辑有一点疑惑:从网络请求回来的数据,先写入到磁盘中做磁盘缓存,然后再从磁盘中读出,然后使用。不说这里的时间浪费,单就网络数据和磁盘数据的逻辑强绑定,就有点不合理,未来假如磁盘的缓存逻辑出了一点小问题都会直接影响到后面的图片解码和展示

以上是个人的看法,可能不够全面,不过我们接着追executeNetworkRequest的实现逻辑:

// HttpUriFetcher.kt

private suspend fun executeNetworkRequest(request: Request): Response {
    val response = if (isMainThread()) {
        // callFactory就是Application中设置的,当然不设置默认也是
        callFactory.value.newCall(request).execute() // 终于看到Okhttp了
    } else {
        // Suspend and enqueue the request on one of OkHttp's dispatcher threads.
        callFactory.value.newCall(request).await()
    }
    ...
    return response
}

到了okhttp这里,我们就默认网络请求成功且正常获取到了数据即可。

到这里,我们把一张图片的请求过程分析完成了,我们遇到了请求构造,内存缓存,磁盘缓存,网络请求,图片解码,图形转换,基本上就是一个图片加载库的主框架了。

有了这个主框架,如果想要细致的分析Coil也会容易的多。

接下来我主要讲讲我在看代码的过程中学习到的一些Coil的特点,一些有别于其他框架(如Fresco)的新特点

图片缓存

对于图片加载框架而言,图片缓存可能是最核心的部分了,缓存策略做的好不好直接决定了这个框架的上限。

而对于Coil而言,我认为在本地的二级缓存(内存缓存,磁盘缓存)做的都很不错(虽然也有说三级缓存,加上服务端的缓存)。

先来看看内存缓存

内存缓存的新东西

我们在EngineInterceptor中遇到了图片缓存逻辑

override suspend fun intercept(chain: Interceptor.Chain): ImageResult {

        // 创建cache key
        val cacheKey = memoryCacheService.newCacheKey(request, mappedData, options, eventListener)
        // 尝试获取cache bitmap
        val cacheValue = cacheKey?.let { memoryCacheService.getCacheValue(request, it, size, scale) }

        // 缓存存在,直接返回
        if (cacheValue != null) {
            return memoryCacheService.newResult(chain, request, cacheKey, cacheValue)
        }

        // 否则就请求
        return withContext(request.fetcherDispatcher) {
            // Fetch and decode the image.
            val result = execute(request, mappedData, options, eventListener)

            // 拿到请求到的数据,在写入内存缓存
            val isCached = memoryCacheService.setCacheValue(cacheKey, request, result)
            ...

        }

}

这个逻辑很正常,其他图片框架差不多也是这样,但是我们进入图片缓存逻辑内部,就会发现有些不一样

//MemoryCacheService.kt
fun getCacheValue(
    request: ImageRequest,
    cacheKey: MemoryCache.Key,
    size: Size,
    scale: Scale,
): MemoryCache.Value? {
    if (!request.memoryCachePolicy.readEnabled) return null
    // 
    val cacheValue = imageLoader.memoryCache?.get(cacheKey)
    return cacheValue?.takeIf { isCacheValueValid(request, cacheKey, it, size, scale) }
}

imageLoader.memoryCache也是Application中设置的(当然,不设置也有默认的),最终构建在MemoryCache.Builder中:

// MemoryCache.kt
fun build(): MemoryCache {

// 默认情况下 weakReferencesEnabled strongReferencesEnabled均为true

    val weakMemoryCache = if (weakReferencesEnabled) {
        RealWeakMemoryCache()
    } else {
        EmptyWeakMemoryCache()
    }
    val strongMemoryCache = if (strongReferencesEnabled) {
        val maxSize = if (maxSizePercent > 0) {
            calculateMemoryCacheSize(context, maxSizePercent)
        } else {
            maxSizeBytes
        }
        if (maxSize > 0) {
            RealStrongMemoryCache(maxSize, weakMemoryCache)
        } else {
            EmptyStrongMemoryCache(weakMemoryCache)
        }
    } else {
        EmptyStrongMemoryCache(weakMemoryCache)
    }
    // 构造了两个cache:RealWeakMemoryCache 和 EmptyWeakMemoryCache
    // 返回RealMemoryCache
    return RealMemoryCache(strongMemoryCache, weakMemoryCache)
}

从图片缓存的构造中就能看到不同的东西了,Coil构造了两个内存图片缓存类,一个强引用吗,一个弱引用。

我们接着看他们在RealMemoryCache中的使用:

internal class RealMemoryCache(
    private val strongMemoryCache: StrongMemoryCache,
    private val weakMemoryCache: WeakMemoryCache
) : MemoryCache {

    override val size get() = strongMemoryCache.size
    // 最大尺寸只看强引用的缓存队列
    override val maxSize get() = strongMemoryCache.maxSize

    override val keys get() = strongMemoryCache.keys + weakMemoryCache.keys
    // 获取缓存时,先尝试从强引用队列中获取,再从弱引用对了中找
    override fun get(key: Key): MemoryCache.Value? {
        return strongMemoryCache.get(key) ?: weakMemoryCache.get(key)
    }
    // 设置缓存时,只设置强引用缓存队列
    override fun set(key: Key, value: MemoryCache.Value) {
        strongMemoryCache.set(
            key = key.copy(extras = key.extras.toImmutableMap()),
            bitmap = value.bitmap,
            extras = value.extras.toImmutableMap()
        )
    }
    // 手动移除时,两个队列都移除
    override fun remove(key: Key): Boolean {
         
        val removedStrong = strongMemoryCache.remove(key)
        val removedWeak = weakMemoryCache.remove(key)
        return removedStrong || removedWeak
    }
    ...
    ...
}

你可能看出了一点他们之间的关系,不过先别急,他们之间的关系展示的还不完整,接着看强引用缓存队列的内部逻辑:

internal class RealStrongMemoryCache(
    maxSize: Int,
    private val weakMemoryCache: WeakMemoryCache
) : StrongMemoryCache {
    // LRU算法来管理内存缓存,经典策略
    private val cache = object : LruCache<Key, InternalValue>(maxSize) {
        override fun sizeOf(key: Key, value: InternalValue) = value.size
        
        override fun entryRemoved( // LRU剪除强引用缓存图片时,同时添加到弱引用缓存队列中
            evicted: Boolean,
            key: Key,
            oldValue: InternalValue,
            newValue: InternalValue?
        ) = weakMemoryCache.set(key, oldValue.bitmap, oldValue.extras, oldValue.size)
    }
    ...
    // 设置缓存
    override fun set(key: Key, bitmap: Bitmap, extras: Map<String, Any>) {
        val size = bitmap.allocationByteCountCompat
        if (size <= maxSize) { //如果当前图片大小还没超过最大限制,接着存
            cache.put(key, InternalValue(bitmap, extras, size)) 
        } else { // 如果当前图片大小已经超过了最大缓存限制,就不能存了,而是存到弱引用缓存队列中
            cache.remove(key)
            weakMemoryCache.set(key, bitmap, extras, size)
        }
    }
    ...

}

我想,到这里我们应该就清楚了,Coil在内存中维护两个队列,一个强引用缓存队列StrongMemoryCache,一个弱引用缓存队列WeakMemoryCache,真正的内存缓存策略由StrongMemoryCache来承担,但是当StrongMemoryCache出现溢出的情况时,会把移除的图片保存在WeakMemoryCache中。

这样做有一个好处:理论上溢出的图片被移除之后,图片的内存空间会被回收掉,但是什么时候回收是不确定的,我们把这个bitmap保存在WeakMemoryCache中,既不影响它的内存回收,同时如果出现了图片再次被需要,又可以重新被利用起来了。

这是一种很精细的内存优化策略。想到这一步真的是把图片加载的过程研究的细致入微了。如果有心的话,可以对WeakMemoryCache的命中情况做一个统计,帮助调整内存缓存的大小达到内存缓存的边际最优。

我们接着看一下RealWeakMemoryCache

internal class RealWeakMemoryCache : WeakMemoryCache {
    //它是一个链表里面包裹着数组,也就是说cache key对应的是一个bitmap数组
    @VisibleForTesting internal val cache = LinkedHashMap<Key, ArrayList<InternalValue>>()

    @Synchronized
    override fun get(key: Key): Value? {
        val values = cache[key] ?: return null

        //根据cache key,找到缓存中第一个可用的bitmap
        val value = values.firstNotNullOfOrNullIndices { value ->
            value.bitmap.get()?.let { Value(it, value.extras) }
        }

        cleanUpIfNecessary()
        return value
    }

    @Synchronized
    override fun set(key: Key, bitmap: Bitmap, extras: Map<String, Any>, size: Int) {
        val values = cache.getOrPut(key) { arrayListOf() }
        run {
            val identityHashCode = bitmap.identityHashCode
            val newValue = InternalValue(identityHashCode, WeakReference(bitmap), extras, size)
            for (index in values.indices) {
                val value = values[index]
                if (size >= value.size) {//数组中的bitmap从大到小的排列
                    if (value.identityHashCode == identityHashCode && value.bitmap.get() === bitmap) {
                        values[index] = newValue
                    } else {
                        values.add(index, newValue)
                    }
                    return@run
                }
            }
            values += newValue
        }

        cleanUpIfNecessary()
    }
    ...
    ...
}

ok,问题来了,为什么RealWeakMemoryCache中会设计同一个key对应多个bitmap的情况呢?StrongMemoryCache并没有这么做。

答案是Coil可以对图片进行放缩,因此同一个url的图片可能会出现不一样大小的bitmap,StrongMemoryCache不可能为同一个key保存不同大小的bitmap,这样缓存使用效率太差了,但是RealWeakMemoryCache没有这个问题,因为它本身并不创造内存空间,只是利用现有空间,当然尽可能获取被移除的bitmap咯。

磁盘缓存的高性能

磁盘缓存相对还有点复杂,它完全依赖了okio这个框架,因此读写性能应当比原生的IO接口好一些。

磁盘缓存的构造也在对应的DiskCache.Builder


private var fileSystem = FileSystem.SYSTEM

fun build(): DiskCache {
    val directory = checkNotNull(directory) { "directory == null" }
    val maxSize = if (maxSizePercent > 0) {
        try {
            val stats = StatFs(directory.toFile().apply { mkdir() }.absolutePath)
            val size = maxSizePercent * stats.blockCountLong * stats.blockSizeLong
            size.toLong().coerceIn(minimumMaxSizeBytes, maximumMaxSizeBytes)
        } catch (_: Exception) {
            minimumMaxSizeBytes
        }
    } else {
        maxSizeBytes
    }
    // RealDiskCache是磁盘缓存的实现类
    return RealDiskCache(
        maxSize = maxSize,
        directory = directory,
        fileSystem = fileSystem,
        cleanupDispatcher = cleanupDispatcher
    )
}

这里需要提一下FileSystem,它是okio提供的一个跨平台,高效的文件系统,Coil主要依赖它进行文件的读写操作。

FileSystem.SYSTEM还用到了nio的能力

// FileSystem.kt
...
  @JvmField
  val SYSTEM: FileSystem = run {
    try {
      Class.forName("java.nio.file.Files")
      return@run NioSystemFileSystem()
    } catch (e: ClassNotFoundException) {
      return@run JvmSystemFileSystem()
    }
  }
  

在Java7以上以及Android8.0及以上系统中都可以支持nio,java中的nio不知道是该称作non-blocking IO,还是New IO,总之它提供了non-blocking IO的一些特性,对比于面向流的IO框架而言,前者有更好的性能。具体的留给大家研究吧。

Coil中使用nio.file读取文件的一些文件的meta信息,比如size,创建时间,修改时间等。用于磁盘缓存的一些判断逻辑。

internal class RealDiskCache(
    override val maxSize: Long,
    override val directory: Path,
    override val fileSystem: FileSystem,
    cleanupDispatcher: CoroutineDispatcher
) : DiskCache {
    // LRU磁盘缓存
    private val cache = DiskLruCache(
        fileSystem = fileSystem,
        directory = directory,
        cleanupDispatcher = cleanupDispatcher,
        maxSize = maxSize,
        appVersion = 1,
        valueCount = 2,
    )

    override val size get() = cache.size()
   // 获取Snapshot
    override fun openSnapshot(key: String): Snapshot? {
        return cache[key.hash()]?.let(::RealSnapshot)
    }
    // 获取磁盘缓存,称作快照
    @Suppress("OVERRIDE_DEPRECATION")
    override fun get(key: String) = openSnapshot(key)
    // Editor,包含一些缓存文件的Meta信息和文件路径等,还有一些缓存区关闭等操作
    //主要用于写入
    override fun openEditor(key: String): Editor? {
        return cache.edit(key.hash())?.let(::RealEditor)
    }

    @Suppress("OVERRIDE_DEPRECATION")
    override fun edit(key: String) = openEditor(key)

    override fun remove(key: String): Boolean {
        return cache.remove(key.hash())
    }
    ...
    ...
    
}

Coil的磁盘缓存逻辑相对复杂一些,但是主要是因为使用的新的IO体系,缓存策略并没有太多新的东西,因此不再往下分析代码了。

coil的磁盘缓存会把数据分为两个文件存储,一个保存数据体,一个保存response header部分数据,一个保存response body的数据。同时还有一个操作记录保存在单独的文件中。

image.png

Coil磁盘缓存的性能提升主要来源于okio以及nio的部分。

拓展性

Coil的拓展性可能是目前主流的图片加载框架中最好的。我们可以回到RealImageLoadercomponents配置

// RealImageLoader.kt

internal class RealImageLoader(
...
// 这些差不多是ImageLoader整个框架核心配置所在
    override val components = componentRegistry.newBuilder()
        // 数据类型转换
        .add(HttpUrlMapper()) // 把HttpUrl转换为string
        .add(StringMapper()) // string数据转换为Uri   网络请求一般传入string
        .add(FileUriMapper())
        .add(ResourceUriMapper())
        .add(ResourceIntMapper())
        .add(ByteArrayMapper())
        // Keyers 缓存的keys
        .add(UriKeyer())
        .add(FileKeyer(options.addLastModifiedToFileCacheKey))
        // 数据来源提取(来源可以是网络,文件,或者asset等)
        .add(HttpUriFetcher.Factory(callFactoryLazy, diskCacheLazy, options.respectCacheHeaders))
        .add(FileFetcher.Factory())
        .add(AssetUriFetcher.Factory())
        .add(ContentUriFetcher.Factory())
        .add(ResourceUriFetcher.Factory())
        .add(DrawableFetcher.Factory())
        .add(BitmapFetcher.Factory())
        .add(ByteBufferFetcher.Factory())
        //解码器
        .add(BitmapFactoryDecoder.Factory(options.bitmapFactoryMaxParallelism, options.bitmapFactoryExifOrientationPolicy))
        .build()
        // 拦截器,目前只有EngineInterceptor,因为用户端没有设置
    private val interceptors = components.interceptors +
        EngineInterceptor(this, systemCallbacks, requestService, logger)
               
        

从一个图片加载的过程中,无论是请求数据的类型转换,cache key的类型设置,缓存策略,数据加载来源,解码器支持,还是图片请求过程中的interceptor拦截。又或者是图片显示前图形转换等等。

可以说在图片加载的每一个重要环节,coil都留下了可以拓展的空间。

除此之外,轻量级,方法数少,深度贴合kotlin和lifesycle等现代开发组件这些特点都不可小觑。或许未来大家都使用Coil了呢?

总结

结合之前的笔记以及目前最新的代码整理了这篇文章,可能会有一些错漏,有问题可以提到评论区,看到会即时修改。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容