图片加载库Coil源码浅析

前言

公司项目使用了Kotlin协程处理常规任务调度,代码简洁优雅。最近看到Coil图片加载库使用了Kotlin协程,清爽统一,就研读了一下源码。

Coil(代表 "Coroutine Image Loader")是一个流行的 Kotlin 图片加载库,它利用 Kotlin 协程和 Jetpack Compose 提供了一个简洁、高效的方式来处理图片。

Coil 集成和使用分析

1. 集成依赖

要开始使用 Coil,首先需要将其依赖添加到项目中。在你的 app 模块的 build.gradle 文件中,添加以下依赖:

scss 复制代码
implementation("io.coil-kt:coil:1.4.0")

2.全局配置

kotlin 复制代码
class Application : Application(), ImageLoaderFactory {

    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 {
                // Don't limit concurrent network requests by host.
                val dispatcher = Dispatcher().apply { maxRequestsPerHost = maxRequests }

                // Lazily create the OkHttpClient that is used for network operations.
                OkHttpClient.Builder()
                    .dispatcher(dispatcher)
                    .build()
            }
            // Show a short crossfade when loading images asynchronously.
            .crossfade(true)
            // Ignore the network cache headers and always read from/write to the disk cache.
            .respectCacheHeaders(false)
            // Enable logging to the standard Android log if this is a debug build.
            .apply { if (BuildConfig.DEBUG) logger(DebugLogger()) }
            .build()
    }
}

通过上述的全局配置代码,清晰的可以看出也是同Glide一样使用了三级缓存

3.加载图片到ImageView

kotlin 复制代码
holder.image.apply {
    load(item.uri) {
                placeholder(ColorDrawable(item.color))
                error(ColorDrawable(Color.RED))
                parameters(item.parameters)
                listener { _, result -> placeholder = result.memoryCacheKey }
        }
}

综上三点基本的使用我们就已经清楚了,配合Kotlin一些语法糖,是不是极其的简单。

前置知识

图片缓存

首先根据全局配置代码,同样使用了三级缓存,了解Glide的同学对这部分一定是很容易了,简要的说一下。

  1. 内存缓存

    • Coil 使用内存缓存来快速加载已经加载过的图片。当请求一个图片时,Coil 首先会检查内存缓存是否已经有这个图片的副本。
    • 如果找到了,图片可以立即从内存中加载,避免了网络请求或磁盘读取的开销。
    • Coil 默认使用的内存缓存是基于 LruCache 实现的。
  2. 磁盘缓存

    • 如果图片不在内存缓存中,Coil 接下来会检查磁盘缓存。磁盘缓存存储了之前从网络上下载的图片文件。
    • 这可以减少对网络请求的依赖,特别是在没有网络连接或网络连接较慢的情况下。
    • Coil 通常会利用 OkHttp 的磁盘缓存机制,因为 Coil 内部使用 OkHttp 进行网络请求。
  3. 网络请求

    • 如果一个图片既不在内存缓存中,也不在磁盘缓存中,Coil 最后会发起一个网络请求来获取这个图片。
    • 下载的图片会被存入磁盘缓存(如果配置了的话),并放入内存缓存中,以便于后续的快速加载。

如何设计图片加载框架?

图片占用实际内存空间计算

首先图片加载到内存中会根据 1像素所占字节数 * 图片宽高 = 实际内存占用。

在Android中实际会根据图片属性来计算像素占字节数大小,举例常用的Bitmap属性:

  • ARGB_8888 带有alpha透明通道的图片格式,最佳质量的图片格式,可以存储丰富的红绿蓝信息。那么一像素占用4字节(8 * 4 / 8 = 4)。
  • RGB_565 没有透明通道的红绿蓝像素存储格式,绿色为什么要用多一位呢?是因为人眼对绿色比较敏感所以为了更加让绿色战士的精准,所以针对绿色多了一位去存储。那么一像素占用2字节(5 + 6 + 5 = 16 / 8 = 2)。

不过在Android系统中,加载本地文件(darwable文件夹下)时,由于我们的设备屏幕尺寸是多样的,并且对应的屏幕密度(dpi)也是不一样的,所以不同分辨率的图片在不同密度的屏幕上占用的内存也是不一样的。所以为什么我们在做屏幕适配的时候要对应不同分辨率的图片,设计会切出不同图片(hdpip、xhdpi、xxhdpi...)

ini 复制代码
drawable资源图片:占用内存空间大小 = 每个像素大小 x 相对宽度 x 相对高度
相对宽度 = 图片宽度 x (手机设备像素密度 / drawable-xx目录对应的像素密度 )
相对高度 = 图片高度 x (手机设备像素密度 / drawable-xx目录对应的像素密度 )
    
drawable-xx目录对应的像素密度值,如下:
标准值为mdpi=160,hdpi=1.5x160=240,xdpi=2x160=320,xxdpi=3x160=480
    
真实手机设备像素密度值怎么计算呢?如下:
手机像素密度值 = 手机对角线上多少个像素 / 手机对角线上多少英寸

Coil#ImageLoader

同样提供了两个同步异步的方法去真正执行图片加载流程。 而同步的方法使用挂起函数来实现的。

Coil#RealImageLoader

RealImageLoader装配了很多属性,比如三级缓存配置、事件回调、类型访问者、还有责任链拦截器配置。 红框中代码可知是配置拦截器,并且EngineIntercepter是最后一个拦截器(非常像Okhttp对吧)。

Coil#Interceptor

Coil#RealInterceptorChain

似曾相识吧?OkHttp责任链模式也是以类似的方式进行启动整体的责任链单一职责的流程。

Coil#RealImageLoader#executeMain()

看一下真正执行图片加载的入口。 首先如果有占位图,那么优先从内存缓存中找到占位图的bitmap,然后调用target接口中的onStart方法把占位图设置进去,让ImageView加载。 之后会调用proceed()方法执行调用链,很简单吧。

Coil#EngineInterceptor

这一部分就是从包装类ImageRequest获取到请求图片的信息,并从chain.size获取到加载到目标空间的尺寸,这个很重要,贯穿整个decode流程。

之后根据request信息从内存缓存map中查找,如果内存中已经存在该图片资源那么就直接返回查询到的图片资源。当然这个map也是使用的Lru算法,将最近经常使用的图片保留移除不经常使用的图片,这个我们后续说。 若内存缓存中不存在该图片资源那么就要执行后续的execute()方法了。

为什么我们不直接在这里去检查DiskCache呢? 哈哈,本质上只有网络加载才需要DiskCache, 所以在Coil中创建了一个Fetcher接口,区分图片加载源是Url、Drawable、File...

Coil#EngineInterceptor#execute()

这里过度了一步,如果没有自定义fetcher和decoder,那么就采用默认的方式。然后执行fetch()方法去真正执行获取图片资源的流程,如果fetch()结束,会返回fetchResult对象,然后针对是什么类型的资源做后续的逻辑,如果是SourceResult就需要IO操作进行图像解码,这个decode也是一个接口根据图片类型使用一对一的解码器。而如果是DrawableResult那么就可以直接返回一个包装类型ExecuteResult(),当解码结束,就是图像转换流程,同样对应不同的转换器。

接下来我们看看HttpUriFetcher。

Coil#HttpUriFetcher#fetch()

从磁盘中找到缓存图像,如果存在直接返回图像资源。

如果没有则执行executeNetworkRequest(cacheStrategy.networkRequest!!)去真正执行网络请求。请求成功,将Response写入到磁盘中,并标记数据来源是网络。

Coil#HttpUriFetcher#executeNetworkRequest()

使用OkHttp进行网络请求。 这里有线程判断 如果在主线程就会去调用execute()同步方法,否则使用 await()。这个await()方法内部是调用了OkHttp的异步方法,只不过使用了suspendCancellableCoroutine()将异步转为同步。 Coil针对fetch, decode流程都有对应的协程Dispatcher,所以可以指定调度。 到这里以后网络加载的流程就结束了。

Coil#EngineInterceptor#decode()

components.newDecoder(fetchResult, options, imageLoader, searchIndex) 这行代码针对不同的图片类型通过工厂模式创建出对应的解码器,我们以BitmapFactoryDecoder看一下。

kotlin 复制代码
class BitmapFactoryDecoder(
    private val source: ImageSource,
    private val options: Options,
    private val parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE),
    private val exifOrientationPolicy: ExifOrientationPolicy = ExifOrientationPolicy.RESPECT_PERFORMANCE
) : Decoder {

    @Deprecated(message = "Kept for binary compatibility.", level = DeprecationLevel.HIDDEN)
    constructor(
        source: ImageSource,
        options: Options
    ) : this(source, options)

    @Deprecated(message = "Kept for binary compatibility.", level = DeprecationLevel.HIDDEN)
    constructor(
        source: ImageSource,
        options: Options,
        parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE)
    ) : this(source, options, parallelismLock)

    override suspend fun decode() = parallelismLock.withPermit {
        runInterruptible { BitmapFactory.Options().decode() }
    }

    private fun BitmapFactory.Options.decode(): DecodeResult {
        val safeSource = ExceptionCatchingSource(source.source())
        val safeBufferedSource = safeSource.buffer()

        // Read the image's dimensions.
        inJustDecodeBounds = true
        BitmapFactory.decodeStream(safeBufferedSource.peek().inputStream(), null, this)
        safeSource.exception?.let { throw it }
        inJustDecodeBounds = false

        // Get the image's EXIF data.
        val exifData = ExifUtils.getExifData(outMimeType, safeBufferedSource, exifOrientationPolicy)
        safeSource.exception?.let { throw it }

        // Always create immutable bitmaps as they have better performance.
        inMutable = false

        if (SDK_INT >= 26 && options.colorSpace != null) {
            inPreferredColorSpace = options.colorSpace
        }
        inPremultiplied = options.premultipliedAlpha

        configureConfig(exifData)
        configureScale(exifData)

        // Decode the bitmap.
        val outBitmap: Bitmap? = safeBufferedSource.use {
            BitmapFactory.decodeStream(it.inputStream(), null, this)
        }
        safeSource.exception?.let { throw it }
        checkNotNull(outBitmap) {
            "BitmapFactory returned a null bitmap. Often this means BitmapFactory could not " +
                "decode the image data read from the input source (e.g. network, disk, or " +
                "memory) as it's not encoded as a valid image format."
        }

        // Fix the incorrect density created by overloading inDensity/inTargetDensity.
        outBitmap.density = options.context.resources.displayMetrics.densityDpi

        // Reverse the EXIF transformations to get the original image.
        val bitmap = ExifUtils.reverseTransformations(outBitmap, exifData)

        return DecodeResult(
            drawable = bitmap.toDrawable(options.context),
            isSampled = inSampleSize > 1 || inScaled
        )
    }

    /** Compute and set [BitmapFactory.Options.inPreferredConfig]. */
    private fun BitmapFactory.Options.configureConfig(exifData: ExifData) {
        var config = options.config

        // Disable hardware bitmaps if we need to perform EXIF transformations.
        if (exifData.isFlipped || exifData.isRotated) {
            config = config.toSoftware()
        }

        // Decode the image as RGB_565 as an optimization if allowed.
        if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == MIME_TYPE_JPEG) {
            config = Bitmap.Config.RGB_565
        }

        // High color depth images must be decoded as either RGBA_F16 or HARDWARE.
        if (SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
            config = Bitmap.Config.RGBA_F16
        }

        inPreferredConfig = config
    }

    /** Compute and set the scaling properties for [BitmapFactory.Options]. */
    private fun BitmapFactory.Options.configureScale(exifData: ExifData) {
        // Requests that request original size from a resource source need to be decoded with
        // respect to their intrinsic density.
        val metadata = source.metadata
        if (metadata is ResourceMetadata && options.size.isOriginal) {
            inSampleSize = 1
            inScaled = true
            inDensity = metadata.density
            inTargetDensity = options.context.resources.displayMetrics.densityDpi
            return
        }

        // This occurs if there was an error decoding the image's size.
        if (outWidth <= 0 || outHeight <= 0) {
            inSampleSize = 1
            inScaled = false
            return
        }

        // srcWidth and srcHeight are the original dimensions of the image after
        // EXIF transformations (but before sampling).
        val srcWidth = if (exifData.isSwapped) outHeight else outWidth
        val srcHeight = if (exifData.isSwapped) outWidth else outHeight

        val dstWidth = options.size.widthPx(options.scale) { srcWidth }
        val dstHeight = options.size.heightPx(options.scale) { srcHeight }

        // Calculate the image's sample size.
        inSampleSize = DecodeUtils.calculateInSampleSize(
            srcWidth = srcWidth,
            srcHeight = srcHeight,
            dstWidth = dstWidth,
            dstHeight = dstHeight,
            scale = options.scale
        )

        // Calculate the image's density scaling multiple.
        var scale = DecodeUtils.computeSizeMultiplier(
            srcWidth = srcWidth / inSampleSize.toDouble(),
            srcHeight = srcHeight / inSampleSize.toDouble(),
            dstWidth = dstWidth.toDouble(),
            dstHeight = dstHeight.toDouble(),
            scale = options.scale
        )

        // Only upscale the image if the options require an exact size.
        if (options.allowInexactSize) {
            scale = scale.coerceAtMost(1.0)
        }

        inScaled = scale != 1.0
        if (inScaled) {
            if (scale > 1) {
                // Upscale
                inDensity = (Int.MAX_VALUE / scale).roundToInt()
                inTargetDensity = Int.MAX_VALUE
            } else {
                // Downscale
                inDensity = Int.MAX_VALUE
                inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
            }
        }
    }

    class Factory(
        maxParallelism: Int = DEFAULT_MAX_PARALLELISM,
        private val exifOrientationPolicy: ExifOrientationPolicy = ExifOrientationPolicy.RESPECT_PERFORMANCE
    ) : Decoder.Factory {

        @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
        @SinceKotlin("999.9") // Only public in Java.
        constructor() : this()

        @Deprecated(message = "Kept for binary compatibility.", level = DeprecationLevel.HIDDEN)
        constructor(maxParallelism: Int = DEFAULT_MAX_PARALLELISM) : this(maxParallelism)

        private val parallelismLock = Semaphore(maxParallelism)

        override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder {
            return BitmapFactoryDecoder(result.source, options, parallelismLock, exifOrientationPolicy)
        }

        override fun equals(other: Any?) = other is Factory

        override fun hashCode() = javaClass.hashCode()
    }

    /** Prevent [BitmapFactory.decodeStream] from swallowing [Exception]s. */
    private class ExceptionCatchingSource(delegate: Source) : ForwardingSource(delegate) {

        var exception: Exception? = null
            private set

        override fun read(sink: Buffer, byteCount: Long): Long {
            try {
                return super.read(sink, byteCount)
            } catch (e: Exception) {
                exception = e
                throw e
            }
        }
    }

    internal companion object {
        internal const val DEFAULT_MAX_PARALLELISM = 4
    }
}

这个解码器比较重要,涉及了常见的Bitmap处理过程。 首先这里使用Okio进行对图片流进行写入,使用Bitmap.decodeStream()将输入流解码为Bitmap,当然前后先使用inJustDecodeBounds去控制是否加载到内存,之后获取到EXIF资源,去做图片像素存储方式以及采样率和尺寸比例的操作。

kotlin 复制代码
        configureConfig(exifData)
        configureScale(exifData)

这两个方法就对Bitmap资源进行处理的两个方法。我们分别看一下。

kotlin 复制代码
    /** Compute and set [BitmapFactory.Options.inPreferredConfig]. */
    private fun BitmapFactory.Options.configureConfig(exifData: ExifData) {
        var config = options.config

        // Disable hardware bitmaps if we need to perform EXIF transformations.
        if (exifData.isFlipped || exifData.isRotated) {
            config = config.toSoftware()
        }

        // Decode the image as RGB_565 as an optimization if allowed.
        if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == MIME_TYPE_JPEG) {
            config = Bitmap.Config.RGB_565
        }

        // High color depth images must be decoded as either RGBA_F16 or HARDWARE.
        if (SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
            config = Bitmap.Config.RGBA_F16
        }

        inPreferredConfig = config
    }

如果当前图片是JPEG并且当前的属性是ARGB_8888,那么就设置为RGB_565, 因为JPEG是不支持Alpha通道的,所以使用ARGB_8888是资源浪费,但前提是要支持RGB_565。

kotlin 复制代码
private fun BitmapFactory.Options.configureScale(exifData: ExifData) {
        // Requests that request original size from a resource source need to be decoded with
        // respect to their intrinsic density.
        val metadata = source.metadata
        if (metadata is ResourceMetadata && options.size.isOriginal) {
            // 代码 1
            inSampleSize = 1
            inScaled = true
            inDensity = metadata.density
            inTargetDensity = options.context.resources.displayMetrics.densityDpi
            return
        }

        // This occurs if there was an error decoding the image's size.
        if (outWidth <= 0 || outHeight <= 0) {
            inSampleSize = 1
            inScaled = false
            return
        }

        // srcWidth and srcHeight are the original dimensions of the image after
        // EXIF transformations (but before sampling).
        // 代码 2
        val srcWidth = if (exifData.isSwapped) outHeight else outWidth
        val srcHeight = if (exifData.isSwapped) outWidth else outHeight

        // 代码 3
        val dstWidth = options.size.widthPx(options.scale) { srcWidth }
        val dstHeight = options.size.heightPx(options.scale) { srcHeight }

        // 代码 4
        // Calculate the image's sample size.
        inSampleSize = DecodeUtils.calculateInSampleSize(
            srcWidth = srcWidth,
            srcHeight = srcHeight,
            dstWidth = dstWidth,
            dstHeight = dstHeight,
            scale = options.scale
        )

        // 代码 5
        // Calculate the image's density scaling multiple.
        var scale = DecodeUtils.computeSizeMultiplier(
            srcWidth = srcWidth / inSampleSize.toDouble(),
            srcHeight = srcHeight / inSampleSize.toDouble(),
            dstWidth = dstWidth.toDouble(),
            dstHeight = dstHeight.toDouble(),
            scale = options.scale
        )

        // Only upscale the image if the options require an exact size.
        if (options.allowInexactSize) {
            scale = scale.coerceAtMost(1.0)
        }
        // 代码 6
        inScaled = scale != 1.0
        if (inScaled) {
            if (scale > 1) {
                // Upscale
                inDensity = (Int.MAX_VALUE / scale).roundToInt()
                inTargetDensity = Int.MAX_VALUE
            } else {
                // Downscale
                inDensity = Int.MAX_VALUE
                inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
            }
        }
    }

这部分代码代表对Bitmap采样率&尺寸进行调整的过程,我们知道如果Bitmap的实际宽高大于要加载的目标控件(ImageView)的宽高,那么对内存来说就是资源浪费,我们要把Bitmap进行裁剪或者宽高调整。所以这个方法的核心是对上述内容的处理。

  • 在代码1处,在加载res/文件夹下的资源图片,如果目标尺寸未指定,那么加载以原始图片宽高为准,设置采样率为1,以及需要根据图片在不同文件(hdpi、xhdpi...)下的固有密度进行调整,所以 inDensity 等于图片的密度,inTargetDensity设置为屏幕的像素密度。

  • 在代码2处,若当前图片资源发生过旋转,那么图片目标计算宽高要取相反的,以确保内容可以被充分加载。

kotlin 复制代码
internal val ExifData.isSwapped get() = rotationDegrees == 90 || rotationDegrees == 270
  • 在代码3处,想要将图片加载到UI控件时要计算的最终大小dstWidth&dstHeight,如果是原始大小就是图片资源的大小,否则就是控件的大小。
kotlin 复制代码
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {  
    return if (isOriginal) original() else width.toPx(scale)  
}  
  
internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {  
    return if (isOriginal) original() else height.toPx(scale)  
}
  • 在代码4处,要根据图片原始宽高与目标UI控件大小进行采样率计算。
less 复制代码
@JvmStatic  
fun calculateInSampleSize(  
@Px srcWidth: Int,  
@Px srcHeight: Int,  
@Px dstWidth: Int,  
@Px dstHeight: Int,  
scale: Scale  
): Int {  
    val widthInSampleSize = (srcWidth / dstWidth).takeHighestOneBit()  
    val heightInSampleSize = (srcHeight / dstHeight).takeHighestOneBit()  
    return when (scale) {  
        Scale.FILL -> minOf(widthInSampleSize, heightInSampleSize)  
        Scale.FIT -> maxOf(widthInSampleSize, heightInSampleSize)  
    }.coerceAtLeast(1)  
}

可见,很简单的计算, 原始宽度 / 目标宽度取整,原始高度 / 目标高度取整,然后根据缩放类型FILL&FIT进行计算。

    • 如果 scaleScale.FILL,函数选择宽度和高度采样大小中的较小者。这意味着图像将被填充到目标尺寸,可能会被裁剪。
    • 如果 scaleScale.FIT,则选择较大者。这意味着图像将完全适应目标尺寸,但可能会留下空白区域。
  • 在代码5处,由于设置过采样率,如果采样率为2,那么响应图片资源的宽和高会缩2倍。 所以在这里用srcWidth/inSampleSize去作为采样后的宽和高,之后再计算缩放比例,因为这时候需要把图片加载到UI控件上,需要根据控件的大小对应调整。
kotlin 复制代码
fun computeSizeMultiplier(  
@Px srcWidth: Double,  
@Px srcHeight: Double,  
@Px dstWidth: Double,  
@Px dstHeight: Double,  
scale: Scale  
): Double {  
    val widthPercent = dstWidth / srcWidth  
    val heightPercent = dstHeight / srcHeight  
    return when (scale) {  
        Scale.FILL -> maxOf(widthPercent, heightPercent)  
        Scale.FIT -> minOf(widthPercent, heightPercent)  
    }  
}

相似的逻辑去计算缩放百分比。

  • 代码6处,如果百分比为1,说明不需要进行缩放调整。如果不等于那么就要进行缩放, 由于缩放是基于密度的改变,所以缩放比例如果 > 1,说明图片需要放大,假设scale = 2,这里使用了 Int.MAX_VALUE作为一个锚点,如果需要放大那么就将 inDensity调整为Int.MAX_VALUE / 2, inTargetDensity = Int.MAX_VALUE这样就实现了放大2倍的效果。

小知识点

  • 协程挂起函数runInterruptible

runInterruptible是Kotlin协程中的一个函数,它允许你在协程中运行可中断的代码块。这个函数特别有用于在协程中执行那些可能需要长时间运行且需要能够响应取消请求的操作。在 Kotlin 协程中,取消是协作性的,这意味着协程代码需要定期检查取消状态,并在适当时候响应取消。然而,有些操作(如阻塞 I/O 操作或某些系统调用)本身不是可中断的,也就是说,它们不会自己检查协程的取消状态。这就是 runInterruptible 派上用场的地方。在Coil中大量的使用可中断函数,防止b某些阻塞式行为本应该释放但仍然占用CPU的问题。

使用runInterruptible

runInterruptible如何做到的中断?什么原理?

原理很简单,执行可中断挂起函数时,会调用ThreadState#setup()方法,为job注入当前的ThreadState 对象,由于当前的ThreadState类复写了invoke方法,可以看到invoke方如何判断,如果当前ThreadState==WORKING时,并且当前automic要更新为INTERRUPTING状态时,会调用targetThread.interrupted()方法, 当前的Thread的IO等等阻塞事件将会结束。

至此整体流程大概梳理了一下,但是省略了一些代码,比如 FetchDecoder所有的实现类,有兴趣大家可以看看,比如GIFDecoder等等。还有内存&磁盘缓存也都是常谈的LruCache(LinkedHashMap),以及在什么时机进行触发清除不经常使用的Bitmap,内存缓存又分为强缓存与弱缓存,当移除不常用的Bitmap时会将不常用的Bitmap转移到弱缓存之中,等待流程。

有其他的博客评估过Coil的性能,相比于Glide,Coil加载图片的平均耗时会比Glide慢100ms左右,可能跟协程的性能少许关系,后续等有空看一下。

相关推荐
Mercury Random24 分钟前
Qwen 个人笔记
android·笔记
苏苏码不动了31 分钟前
Android 如何使用jdk命令给应用/APK重新签名。
android
aqi001 小时前
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
android·ffmpeg·音视频·直播·流媒体
xiaoduyyy2 小时前
【Android】ToolBar,滑动菜单,悬浮按钮和可交互提示等的使用方法
android
liyy6142 小时前
Android架构组件:MVVM模式的实战应用与数据绑定技巧
android
K1t04 小时前
Android-UI设计
android·ui
吃汉堡吃到饱6 小时前
【Android】浅析MVC与MVP
android·mvc
深海呐12 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang12 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼12 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android