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

前言

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

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

一张图片加载的过程

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

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

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

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

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

kotlin 复制代码
//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语法特性)

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初始化配置:

scss 复制代码
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的方法:

kotlin 复制代码
// 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方法:

kotlin 复制代码
// 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方法即可)。

kotlin 复制代码
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方法的实现

kotlin 复制代码
// 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的实现逻辑:

kotlin 复制代码
// 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中遇到了图片缓存逻辑

kotlin 复制代码
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)
            ...

        }

}

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

kotlin 复制代码
//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中:

kotlin中 复制代码
// 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中的使用:

kotlin 复制代码
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
    }
    ...
    ...
}

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

kotlin 复制代码
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

kotlin 复制代码
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

kotlin 复制代码
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的能力

kotlin 复制代码
// 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,创建时间,修改时间等。用于磁盘缓存的一些判断逻辑。

kotlin 复制代码
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的数据。同时还有一个操作记录保存在单独的文件中。

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

拓展性

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

kotlin 复制代码
// 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了呢?

总结

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

相关推荐
2202_7544215410 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
蓝染-惣右介12 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习13 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
HoneyMoose15 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热17 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余18 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp18 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea
.Ayang21 分钟前
tomcat 后台部署 war 包 getshell
java·计算机网络·安全·web安全·网络安全·tomcat·网络攻击模型
一直学习永不止步26 分钟前
LeetCode题练习与总结:最长回文串--409
java·数据结构·算法·leetcode·字符串·贪心·哈希表
hummhumm40 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j