Skip to content

New In Kingfisher 5

onevcat edited this page Jun 5, 2022 · 5 revisions

Why Kingfisher 5

I made the first commit to Kingfisher in April 2015. It was the "barbarous age" of Swift. There was no protocol extension, no throwing, and even no API guidelines. Not surprising, if you open that commit, you will find some Objective-C code written in Swift.

You needed to use println to print a log with "\n" at that time. I bet you did not know (remember) it!

Things got better with Swift 3, a bunch of new features and an API guideline was added to the language. However, the fundamental structure of Kingfisher remained mostly. There were quite a few legacy codes and mess-up in the framework, which becomes an obstacle and prevents the framework continually evolving. So I decided to rewrite it from scratch. It does not only brings stability and performance benefit, but also expand the possibility of Kingfisher for the future.

New Cache System

Kingfisher is using a hybrid cache system for image caching. It will first try to search the target image in memory cache. If not found, search in disk cache. This model works pretty well, but there were some issues according to recent feedbacks:

Aggressive Strategy By Default

In Kingfisher 4, by default, the memory cache was using a "greedy" way to accept images. It will keep sending images to memory cache without an upper limitation, until a .didReceiveMemoryWarningNotification received. As soon as the system detects the memory availability level is low, Kingfisher will free up the used memory to make the system happy.

This mechanism worked well, at least for a time. We received reports on users apps crash due to memory pressure recently. After some investigation, we found that sometimes, the system won't deliver the .didReceiveMemoryWarningNotification correctly. It, in turn, makes Kingfisher think there is still plenty of memory to use. But it is not the truth.

This problem can be fixed by limiting the max memory cache size in Kingfisher. But since the default behavior is "no limit", so it keeps happening for new users of Kingfisher. In Kingfisher 5, we use a more conservative strategy by default. Now a maximum of 25% device memory would be used as the memory cache.

So if you have some code setting maxMemoryCost of ImageCache to limit memory usage, it is a good time to remove it now. If you want to customize the maximum value, do this:

let memoryCache = ImageCache.default.memoryStorage
// Limit memory cache size to 300 MB.
memoryStorage.config.totalCostLimit = 300 * 1024 * 1024

Full Control of Expiration, Fine Tuned

Disk cache has a default expiration duration in Kingfisher 4: if an image was not accessed for 7 days, it will be marked as expired and be cleaned when the user switches your app to background. This brings two possible predicaments:

  1. The expiration setting is a per cache behavior. It means you cannot set different expiration to different images unless you throw them to different caches.
  2. The expired images are still available before you switch the app to the background or call cleanExpiredDiskCache explicitly.

This brought additional use burden and unnecessary complexity. Now in Kingfisher 5, you can set an expiration duration per image task. Use .diskCacheExpiration in options with a expressive API to specify it:

// Expires after 30 days.
imageView.kf.setImage(with: url1, options: [.diskCacheExpiration(.days(30))])

// Never expires. (But it is still possible be purged by system when your disk is full.)
imageView.kf.setImage(with: url1, options: [.diskCacheExpiration(.never)])

If you do not provide a .diskCacheExpiration when set the image, the value in disk cache config will be used:

// Make the disk cache expires after a certain `Date`.
let date = dateFormatter.date(from: "2019-12-31")!
let diskStorage = ImageCache.default.diskStorage
diskStorage.config.expiration = .date(date)

The similar mechanism also applied to the memory cache. In Kingfisher 4, there is no memory cache expiration. All images in memory cache were just there until you call clean or a low memory notification received. Now, the memory cached images will be expired after 5 minutes before the last access. An internal timer is used to run a task (once per two minutes) for checking expired images in memory. All these behaviors can be customized with the memoryStorage config too. And similar to disk cache, you can have a per image set by using .memoryCacheExpiration option.

Loading From Arbitrary Source

Loading images from the Internet is great. But there is also use case to load a local image, or another library would take the responsibility to download the image and you only want to display and cache it.

It was not an easier task in Kingfisher 4, due to the only way to set an image with Kingfisher is using a URL. In Kingfisher 5, we expand the acceptable source to raw data. Now, you can create a type conforming to ImageDataProvider and provide it to Kingfisher's method.

public protocol ImageDataProvider {
    var cacheKey: String { get }
    func data(handler: @escaping (Result<Data, Error>) -> Void)
}

Below is an example of loading a local file and setting it with Kingfisher:

struct LocalFileImageDataProvider: ImageDataProvider {
    let fileURL: URL
    public var cacheKey: String

    init(fileURL: URL, cacheKey: String? = nil) {
        self.fileURL = fileURL
        self.cacheKey = cacheKey ?? fileURL.absoluteString
    }

    func data(handler: (Result<Data, Error>) -> Void) {
        handler( Result { try Data(contentsOf: fileURL) } )
    }
}

let fileURL: URL = //...A URL in local disk.
let provider = LocalFileImageDataProvider(fileURL: fileURL)

// Load image data from `provider`, use `p` to process the image, and store it in cache.
imageView.kf.setImage(with: provider, options: [.processor(p)])

By doing so, you can provide data from an arbitrary source, at the same time, keep using Kingfisher's other functionality.

The LocalFileImageDataProvider above is provided as a part of Kingfisher. There are also Base64ImageDataProvider and RawImageDataProvider built-in. You can also extend it easily for your use case.

Downsampling for High-Resolution Images

Think about the case we want to show some large images in a table view or a collection view. In the ideal world, we expect to get smaller thumbnails for them, to reduce downloading time and memory use. But in the real world, maybe your server doesn't prepare such a thumbnail version for you. The newly added DownsamplingImageProcessor rescues. It downsamples the high-resolution images to a certain size before loading to memory:

imageView.kf.setImage(
    with: resource,
    placeholder: placeholderImage,
    options: [
        .processor(DownsamplingImageProcessor(size: imageView.size)),
        .scaleFactor(UIScreen.main.scale),
        .cacheOriginalImage
    ])

Typically, DownsamplingImageProcessor is used with .scaleFactor and .cacheOriginalImage. It provides a reasonable image pixel scale for your UI, and prevent future downloading by caching the original high-resolution image.

You may wonder, why not just use a ResizingImageProcessor for it? The ResizingImageProcessor is rendering the pixels of an image in another size, while DownsamplingImageProcessor loads the data to a smaller canvas. There will be some memory spikes when you resize the large image, while DownsamplingImageProcessor always perform better. Since the downsampling happens to the raw image data, it must be the first processor in your processing chain, and animated images are not supported yet.

Kingfisher Options

The rich options makes Kingfisher a powerful and flexible framework. You can customize an image task for using a different processor, sending the result to a different cache, or modifying the original request, by sending an array of KingfisherOptionsInfoItem.

Inside Kingfisher, we are also using these options to pass information and requirement when communicating between different parts. It is not so efficient to this every time. So now we introduced a KingfisherParsedOptionsInfo type. The input KingfisherOptionsInfo will be parsed once to a better format that Kingfisher could understand.

In Kingfisher 5, you can still use the old way to pass the options into Kingfisher APIs. However, if you are trying to access some properties in KingfisherOptionsInfo in your own code (for example, in a customized image processor), you may expect to see a warning, which tells you to create a KingfisherParsedOptionsInfo first. You can fix the waring by changing your code to something like this:

// Before
let options: KingfisherOptionsInfo = //...
if options.cacheOriginalImage { /* */ }
if options.forceRefresh { /* */ }
...

// After
let options: KingfisherOptionsInfo = //...
let parsedOptions = KingfisherParsedOptionsInfo(options)
if parsedOptions.cacheOriginalImage { /* */ }
if parsedOptions.forceRefresh { /* */ }