An in depth analysis of the management and use of image caching in iOS applications

  • 2020-05-27 07:15:00
  • OfStack

Our iOS application contains a lot of images. Creating attractive views relies on a large number of decorative images, all of which must first be fetched from a remote server. If you want to get every image from the server again and again every time you open an application, then the user experience will definitely not achieve a good effect, so it is very necessary to cache remote images locally.

There are two ways to load local images
1. Load the image via the imageNamed: method
Once the image is loaded into memory, it will not be destroyed until the program exits. (that is, imageNamed: there will be an image cache, so it will be faster the next time you access the image.)
Loading the image in this way, the memory management of the image is not controlled by the programmer.

UIImage *image = [UIImage imageNamed: @ " image " ]

"UIImage" means to create an UIImage object, not that image itself is an image, but that image refers to an image. The actual image is not loaded into memory when the object is created, but only when the image is used.
In the example above, if the image object is set to nil, if it is another object, then no strong pointer to an object will be destroyed. But even if image = nil, the image resource it points to will not be destroyed.
2. Load the image by imageWithContentsOfFile
Use this method to load the image. When the pointer to the image object is destroyed or points to another object, the image object has no other strong pointer to it. The image object will be destroyed and will not stay in memory.

Since there is no cache, if the same image is loaded multiple times, there will be multiple image objects to occupy memory instead of using the cached image.

Using this method, the full path of file is required. (the loading file of NSString, NSArray and so on is also the same. For example, stringWithContentsOfFile:, when you see file, you will know that you need to pass in the full path.)

NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

Note that this method cannot be used if the image is in Images.xcassets. So if you want to do your own image memory management (you don't want to have cached images), drag the image resources directly into the project, not into Images.xcassets.

Fast queues and slow queues
We set up two queues, one serial and one parallel. Images that are urgently required on the screen go to the parallel queue (fastQueue), and images that may be needed later go to the serial queue (slowQueue).
For the implementation of UITableView, this means that the table cells on the screen get images from fastQueue, and each closed screen row image is preloaded from slowQueue.

You don't need to deal with images now

Let's say we want to request a page of information from the server that contains 30 events, and when the request comes back, we can queue up to pre-fetch each of the images.

- (void)pageLoaded:(NSArray *)newEvents {   
    for (SGEvent *event in newEvents) {       
       [SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];   

slowGetImageForURL: this method adds images to the slowQueue queue, allowing them to be taken out one by one without blocking network traffic.
thenDo: this block of code is not implemented here because we don't need to do anything with the image yet. All we need to do is make sure they are in the local disk cache and ready to be used when we swipe a table on the screen.

Now we need to deal with the pictures

The tables that are displayed on the screen want to display their images immediately, so implement them in the table cell subclass:

- (void)setEvent:(SGEvent *)event {   
    __weak SGEventCell *me = self;   
    [SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) {       
       me.imageView.image = image;    }

getImageForURL: this method adds the process of fetching images to the fastQueue queue, meaning that they will be executed in parallel as long as the iOS system allows. If the image fetching process already exists in the slowQueue queue, it will be moved to the fastQueue queue to avoid duplicate requests.

1 straight asynchronous

Wait, getImageForURL: isn't it an asynchronous method? What if you know the image is already in the cache, but don't want to use it immediately on the main thread? Your gut tells you that's wrong.
Loading images from disk costs resources, as does unpacking images. You can configure and add tables as you slide, and this last thing you want to do when you slide a table is dangerous, because it blocks the main thread, and you get stuck.
Using getImageForURL: takes disk-loaded actions off the main thread, so that when thenDo:, the code block for finishing work, is executed it already has an instance of UIImage, so there is no danger of slippage. If the image already exists in the local cache, the code block for the closure will be executed in the next run cycle, and the user will not notice the difference between the two. What they'll notice is that the slide won't get stuck.

Now, you don't need to execute fast

If the user quickly slides the table down to the bottom, a few tens or hundreds of table cells will appear on the screen, request image data from fastQueue, and then quickly disappear from the screen. All of a sudden this parallel queue is flooding the network with requests for images that are actually no longer needed. When the user finally stops swiping, the corresponding table cell views on the current screen will place their image requests behind those that are not urgently needed, and the network is blocked.
This is where wheremoveTaskToSlowQueueForURL: this method comes in.

// a table cell is going off screen-
(void)tableView:(UITableView *)table       
didEndDisplayingCell:(UITableViewCell *)cell       
forRowAtIndexPath:(NSIndexPath*)indexPath {   
     // we don't need it right now, so move it to the slow queue            
     [SGImageCache moveTaskToSlowQueueForURL:[[(id)cell event] imageURL]];

This ensures that there are only tasks in fastQueue that really need to be performed quickly. Any tasks that were previously thought to need to be done quickly but are not now needed will be moved to slowQueue.

Focus and selection

There are already quite a few iOS image cache libraries. Some of them are specific to certain application scenarios, and some of them provide scalability for different scenarios. Our library is neither specific to certain application scenarios nor has many large and comprehensive features. We have three basic priorities for our users:
Point 1: the best frame rate
Many libraries are very focused on this one point, using a highly customized and complex approach, although the benchmark does not conclusively show this to be effective. We found that the best frame rates are determined by these:
Detach access to the disk (and almost everything else) from the main thread.
Use UIImage's memory cache to avoid unnecessary disk access and image decompression.

Point 2: give priority to the most important images
Most libraries think about making queue management a concern for others. This is almost the most important point for our application.
Getting the right picture on the screen at the right time boils down to a simple question: "do we need it now or later?" . The images that need to be displayed immediately are loaded in parallel, while everything else is added to the serial queue. All the things that were urgent before but are not urgent now will be transferred from fastQueue to slowQueue. And while fastQueue is working, slowQueue is suspended.
This allows those images that need to be displayed to access the network independently, and also ensures that a non-urgent image can become an urgent image after 1, because it is stored in the cache and ready to be displayed at any time.

Point 3: API as simple as possible
Most libraries do this. Many libraries provide classification of UIImageView to hide details, and many libraries make the process of grabbing an image as easy as possible. Our library has chosen three main methods for the three things we do most often:
Quick catch a picture

__weak SGEventCell *me = self;[SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) {    me.imageView.image = image;}];

Wait in line for a picture we only need

[SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];

Notifies the cache that a graph that is in urgent need of display no longer needs to be displayed immediately

[SGImageCache moveTaskToSlowQueueForURL:event.imageURL];


By focusing on prefetch, queue management, removing time-consuming tasks from the main thread, and relying on UIImage's built-in memory cache, we strive to get good results from a simple package.

Related articles: