This is the 13th day of my participation in Gwen Challenge

Kingfisher source Reading Notes (1) continuation.

Kingfisher source Reading Notes (2) continuation.

This article is received at the time of reading the Kingfisher source. This article focuses on the design of MemoryStorage, which is responsible for MemoryStorage.

MemoryStorage is the namespace responsible for MemoryStorage in Kingfisher. Backend is used to manage all stored transactions. In Backend, NSCache is used for caching.

let storage = NSCache<NSString.StorageObject<T> > ()Copy the code

Methods in Backend are basically done around NSCache:

  1. Through NSCachesetObject(_ obj:, forKey:, cost:)Method to add storage;
  2. Through NSCacheobject(forKey:)Method to obtain the cache corresponding to the specified key, or determine whether the cache is already cached;
  3. Through NSCacheremoveObject(forKey:)orremoveAllObjectsMethod, actively delete or periodically (2 minutes) clean up expired storage.

There was a bug causing crashes

Kingfisher had a bug causing crashes and the author’s fix was remove-cache-delegate. Here is an analysis of the causes of this bug and the author’s solution.

The cause of the bug

First, NSCache is thread-safe and does not require a thread lock. Note, however, that when adding cached data to NSCache in different threads, if the memory upper limit and quantity set by NSCache are reached, The cache(_:, willEvictObject) method of NSCache’s delegate (NSCacheDelegate) is triggered in the current thread to notify the outside world that the corresponding cached data has been removed.

Second, NSCache does not provide a method for traversing all the data currently stored, but only the specified key: object(forKey:). Therefore, you need to maintain keys corresponding to all cached data in NSCache so that all data in NSCache can be traversed when expired data is cleared.

In Kingfisher, the default validity period for cached data in memory is 300s. A periodic two-minute task checks to see if the data has expired and, if so, deletes it from memory.

The key that records all cached data is defined as follows:

var keys = Set<String>()
Copy the code

In the buggy Kingfisher version, all of the events involved in modifying cache keys can be seen below:

In the main thread:

  1. Periodically clearing expired cache (default: once every 2 minutes) removes corresponding keys.
  2. Users actively delete cached data;
  3. After downloading the image, add the corresponding key when adding the cache;
  4. If the NSCache memory size reaches the upper limit or the NSCache memory size reaches the upper limit, the NSCache automatically deletes cached data, triggering the callback methodcache(_: ,willEvictObject:)Delete the corresponding key.Now the callback is inThe main threadIn the.

In the ioQueue thread responsible for operating on IO:

  1. After the cache image is obtained from the disk, the corresponding key is added when the cache is added.
  2. If the NSCache memory size reaches the upper limit or the NSCache memory size reaches the upper limit, the NSCache automatically deletes cached data, triggering the callback methodcache(_: ,willEvictObject:)Delete the corresponding key.Now the callback is inioQueueIn the thread.

The bug was caused by changing the keys Set without locking in the callback method cache(_:,willEvictObject:).

The solution

To solve this problem, the first thought is to lock, and you need to replace the original NSLock with a recursive lock NSRecursiveLock, because NSCache’s cleanup method removeObject(forKey:) is called in a loop.

But Kingfisher author Onevcat doesn’t want to introduce recursive locking for this purpose, instead breaking the strict use of keys to track cached objects to avoid additional locking operations.

If the user deletes the cached data directly, the corresponding key will be removed. When NSCache triggers a cache removal operation, the corresponding key is not removed.

Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding key would be also removed. However, for the object removing triggered by cache rule/policy of system, the key will be remained there until next removeExpired happens.

Breaking the strict tracking could save additional locking behaviors. See github.com/onevcat/Kin…

Kingfisher’s solution provides us with a new idea for solving multi-threaded solutions: strict accuracy is sometimes at a cost, and sometimes giving up strict accuracy may bring efficiency gains.