Introduction of Coil

Coil is an Android image loading library that uses Kotlin coroutines to load images. Features are as follows:

  • Faster: Coil has a number of performance optimizations, including memory and disk caching, storing thumbnails in memory, recycling bitmaps, and automatically suspending and canceling image network requests.
  • Lighter weight: Coil has only 2000 methods (if you integrate OkHttp and Coroutines into your APP), Coil and Picasso have about the same number of methods and are much lighter than Glide and Fresco.
  • Easier to use: The Coil API takes advantage of the new features of the Kotlin language, simplifying and eliminating a lot of boilerplate code.
  • More popular: Coil is developed in Kotlin and uses the most popular open source libraries including Coroutines, OkHttp, Okio, and AndroidX Lifecycles.

Being more popular and lighter isn’t a fake. Indeed, as the authors mentioned, we’re looking at two other features of Coil

Easier to use

Code calls

Glide usage

Glide.with(this)
    .load("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg! op_type=3&columns=290&rows=290&magick=png")
    .placeholder(R.drawable.ic_launcher_foreground)
    .error(R.drawable.ic_launcher_foreground)
    .into(imageView)
Copy the code

Coil common usage

Coil.imageLoader(this).enqueue(
    ImageRequest.Builder(this@TestActivity).data("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg! op_type=3&columns=290&rows=290&magick=png")
        .placeholder(R.drawable.ic_launcher_foreground)
        .error(R.drawable.ic_launcher_foreground)
        .target(imageView)
        .build()
)
Copy the code

Many people here say that it is not as simple as Glide, but the author uses kotlin’s extension function to cover the above code and then has the following method of using it

imageView.load("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg! op_type=3&columns=290&rows=290&magick=png"){
    placeholder(R.drawable.ic_launcher_foreground)
    error(R.drawable.ic_launcher_foreground)
}
Copy the code

Coil recommends initializing the imageLoader in the Application (we’ll see why later) and then using the Context extension property to get the imageLoader singleton. If all the load placeholders and exception graphs are the same, you can configure it in the imageLoader initialization. Use imageView.load(XXX) to do the image load, which makes Coil easier to use

First loading network image source analysis

Load the HTTP image sequence diagram1. Coil notifying imageView to load placeholder image [5];

2. Image stream obtained by network request [10];

3. Generate BitmapDrawable[12] according to the size of the imageView, scaling scale and the required accuracy of the image;

4. Finally notify imageView to load network images [14];

This is just a network image. Coil can also support multiple sources and decode multiple types of bitmaps

Coil loads a variety of image sources and decodes a variety of images

Combined with the source code, I combed the structure and divided Coil into the following five layers

  • API: Exposed to external interface calls
  • Middle: loads the core logic of the bitmap
  • Cache: Determines whether to store LruCache or WeakReference
  • Fetcher: Loads bitmaps from various channel sources
  • Decode: Picture decoding library

If we look at the ImageRequest source code, we see that data is an any type. How does Coil know which Fetcher to use and which Decode to use

/** * ImageRequest.kt * Set the data to load. * The default supported data types are: * - [String] (mapped to a [Uri]) * - [Uri] ("android.resource", "content", "file", "http", and "https" schemes only) * - [HttpUrl] * - [File] * - [DrawableRes] * - [Drawable] * - [Bitmap] */
fun data(data: Any?). = apply {
    this.data = data
}
Copy the code

We go back to the sequence diagram request interception method [8], where we first verify the data type, throw an exception if it is not supported, and then fetch the fetcher

/** *EngineInterceptor.kt */
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val mappedData = registry.mapData(data)
valfetcher = request.fetcher(mappedData) ? : registry.requireFetcher(mappedData) }Copy the code

Look at Registry mapData and requireFetcher

/** *ComponentRegistries.kt */
internal fun ComponentRegistry.mapData(data: Any): Any {
    var mappedData = data
    mappers.forEachIndices { (mapper, type) ->
        if (type.isAssignableFrom(mappedData::class.java) && (mapper as Mapper<Any, *>).handles(mappedData)) {
            mappedData = mapper.map(mappedData)
        }
    }
    return mappedData
}
 
@Suppress("UNCHECKED_CAST")
internal fun <T : Any> ComponentRegistry.requireFetcher(data: T): Fetcher<T> {
    val result = fetchers.findIndices { (fetcher, type) ->
        type.isAssignableFrom(data: :class.java) && (fetcher as Fetcher<Any>).handles(data)
    }
    checkNotNull(result) { "Unable to fetch data. No fetcher supports: $data" }
    return result.first as Fetcher<T>
}
Copy the code

MapData: determine if the data of type Any is one of the types in the collection, and then generate the mappedData object requireFetcher: find one of the fetcher in the collection based on the mappedData object. Let’s take a look at the RealImageLoader class

private val registry = componentRegistry.newBuilder()
    // Mappers
    .add(StringMapper())
    .add(FileUriMapper())
    .add(ResourceUriMapper(context))
    .add(ResourceIntMapper(context))
    // Fetchers.add(HttpUriFetcher(callFactory)) .add(HttpUrlFetcher(callFactory)) .add(FileFetcher(options.addLastModifiedToFileCacheKey)) .add(AssetUriFetcher(context)) .add(ContentUriFetcher(context))  .add(ResourceUriFetcher(context, drawableDecoder)) .add(DrawableFetcher(drawableDecoder)) .add(BitmapFetcher())// Decoders
    .add(BitmapFactoryDecoder(context))
    .build()
Copy the code

When RealImageLoader is initialized, the Mappers and fetchers collection is created

Of type string url, for example: 1. com ponentRegistry. MapData according to string traversal Mappers StringMapper collection, calls to map the url string into a Uri object 2. We’re going to iterate through the fetchers set based on the URI to get HttpUriFetcher, and then we’re going to execute fetch to get the image stream and this logic will tell us why the author expects you to create a singleton ImageLoader in Applcation, because there’s so much functionality to initialize, you’re going to get the best performance out of singleton, As opposed to creating ImageLoader every time

faster

I haven’t seen the source code of the other libraries, so I can’t say that Coil is faster than the others. In the faster section, Coil introduces itself as three features: (1) automatically suspending and canceling image network requests, (2) memory and disk caching, and (3) recycling bitmaps

Automatically suspends and cancels image network requests

And those of you who were careful noticed that if you put the network load inside of the ativity onCreate it’s going to print the following log, it’s going to cancel, and then load again

RealImageLoader: 🏗 Cancelled - https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg! op_type=3&columns=290&rows=290&magick=png RealImageLoader: ☁ ️ Successful (NETWORK) - https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg! op_type=3&columns=290&rows=290&magick=pngCopy the code

Start/cancel the task sequence diagram based on the view state

1. The first request loads DelegateService the extending method createRequestDelegate judgment view attributes isAttachedToWindowCompat, found ImageView still not visible, so cancel the loading job [6].

RealImageLoder captures CancellationException and prints the first log [7];

3. DelegateService createRequestDelegate in method by expanding function to add the ImageView OnAttachStateChangeListener listening, wait to perform when ImageView visible callback function [8];

4. Finally, add the task to the queue and restart the execution [9].

5. If we finish transferring the page before the end of the task, coil will also cancel the job[12]

Lifecycle and view visible listening are used to automatically pause and cancel image network requests

Memory cache and disk cache

This way is an important logical image loading, coil into StrongMemoryCache/LruCache memory cache, weakMemoryCache/WeakReference, used the OkHttpClient cache cache disk cache strategy sequence diagram

StrongMemoryCache, weakMemoryCache, how does The Coil decide which one to use?

For the first time:

BuildDefaultMemoryCache () calculates the maximum memory size used by the LruCache according to the set percentage. MaxSize Determines which one to use according to the maxSize and trackWeakReferences Boolean values

internal interface StrongMemoryCache {

    companion object {
        operator fun invoke(
            weakMemoryCache: WeakMemoryCache,
            referenceCounter: BitmapReferenceCounter,
            maxSize: Int,
            logger: Logger?).: StrongMemoryCache {
            return when {
                maxSize > 0 -> RealStrongMemoryCache(weakMemoryCache, referenceCounter, maxSize, logger)
                weakMemoryCache is RealWeakMemoryCache -> ForwardingStrongMemoryCache(weakMemoryCache)
                else -> EmptyStrongMemoryCache
            }
        }
    }
}
Copy the code

The second time: If the image size to be loaded cannot be stored in lruCache, weakMemoryCache will also be enabled

@Synchronized
override fun set(key: Key, bitmap: Bitmap, isSampled: Boolean) {
    // If the bitmap is too big for the cache, don't even attempt to store it. Doing so will cause
    // the cache to be cleared. Instead just evict an existing element with the same key if it exists.
    val size = bitmap.allocationByteCountCompat + cache.size()
    if (size > maxSize) {
        val previous = cache.remove(key)
        if (previous == null) {
            // If previous ! = null, the value was already added to the weak memory cache in LruCache.entryRemoved.
            weakMemoryCache.set(key, bitmap, isSampled, size)
        }
        return
    }
    Log.d("RealImageLoader"."RealStrongMemoryCache increment $bitmap")
    referenceCounter.increment(bitmap)
    cache.put(key, InternalValue(bitmap, isSampled, size))
}

Copy the code

Disk cache: When creating the ImageLoader singleton, okhttpClient creates a 10MB image_cache folder under the data partition, and sets the cache to expire for one year, during which network requests will be loaded from disk

// To create the an optimized Coil disk cache, use CoilUtils.createDefaultCache(context).
val cacheDirectory = File(filesDir, "image_cache").apply { mkdirs() }
val cache = Cache(cacheDirectory, 10*1024*1024)

// Rewrite the Cache-Control header to cache all responses for a year.
val cacheControlInterceptor = ResponseHeaderInterceptor("Cache-Control"."max-age=31536000,public")
Copy the code

Recycle Bitmaps

Before each load, check memory to see if there is a bitmap, and return if there is

val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null
// Short circuit if the cached bitmap is valid.
if(value ! =null && isCachedValueValid(memoryCacheKey, value, request, size)) {
    returnSuccessResult( drawable = value.bitmap.toDrawable(context), request = request, metadata = Metadata( memoryCacheKey = memoryCacheKey, isSampled = value.isSampled, dataSource = DataSource.MEMORY_CACHE, isPlaceholderMemoryCacheKeyPresent = chain.cached ! =null))}Copy the code

conclusion