The world says that reading source code for the improvement of skill is very significant, but many well-known open source framework source code is often tens of thousands of lines, the complexity is too high, here only do the basic analysis.

Concise interface

First, let’s introduce the famous open source framework SDWebImage. The main functions of this open source framework are:

Asynchronous image downloader with cache support with an UIImageView category.

A UIImageView classification that asynchronously downloads images and supports caching.

The most commonly used method in frameworks is this:

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]].Copy the code

Of course, there’s also a UIButton category in this framework, so you can asynchronously load images to UIButton, but it’s not as commonly used as the UIImageView category method.

The framework’s design is still extremely elegant and simple, and its main function is a single line of code behind which all the complicated implementation details are hidden, which is exactly what it says:

Leave simplicity to others and complexity to yourself.

Now that we’ve seen the framework’s neat interface, how does SDWebImage elegantly implement asynchronous image loading and caching?

Complex implementation

It’s not that SDWebImage’s implementation is bad. On the contrary, its implementation is amazing. We will ignore many implementation details here and not read every line of source code.

First, let’s take a look at how the framework is organized at a high level.

UIImageView+WebCache and UIButton+WebCache provide interfaces directly to the surface UIKit framework, The SDWebImageManger handles and coordinates the SDWebImageDownloader and SDWebImageCache. And interact with the UIKit layer, which has classes that support higher-level abstractions.

UIImageView+WebCache

So what we’re going to do is start with UIImageView plus WebCache

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder;
Copy the code

This approach provides an entry point to see how SDWebImage works. Let’s open up the implementation of this method UIImageView+WebCache.m

Of course you can git clone [email protected]: rs/SDWebImage git to local to check.

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url
            placeholderImage:placeholder
                     options:0
                    progress:nil
                   completed:nil];
}
Copy the code

The only thing this method does is call another method

[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
Copy the code

In this file, you’ll see a lot of sd_setImageWithURL…… Method, they all end up calling the method above, just passing in different parameters as needed, which is common in many open source projects and even the projects we write. This method is also the core method in UIImageView+WebCache.

I don’t want to copy the full implementation of this method here.

Management of operations

Here is the first line of code for this method:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: # 1

[self sd_cancelCurrentImageLoad];
Copy the code

This seemingly simple line of code was initially ignored by me, and I later discovered the idea behind this line of code, namely the way SDWebImage manages operations.

All the operations in the framework are actually managed by an operationDictionary, and that dictionary is actually a property that’s dynamically added to UIView, and why it’s added to UIView, Mainly because the operationDictionary needs to be reused on UIButton and UIImageView, it needs to be added to their root class.

This line of code ensures that no asynchronous download operations are currently in progress and will not conflict with upcoming operations. It calls:

// UIImageView+WebCache
// sd_cancelCurrentImageLoad # 1

[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
Copy the code

This method causes all operations in the current UIImageView to be cancelled. Subsequent downloads will not be affected.

The implementation of a placeholder map

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: # 4

if(! (options & SDWebImageDelayPlaceholder)) { self.image = placeholder; }Copy the code

If none of the options passed SDWebImageDelayPlaceholder (by default options = = 0), so would be to add a temporary UIImageView image, namely placeholder figure.

Get photo

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: # 8

if (url)
Copy the code

The incoming URL is then checked to see if it is non-empty. If not, a global SDWebImageManager calls the following methods to get the image:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
Copy the code

Will be called after the download is complete (SDWebImageCompletionWithFinishedBlock) completedBlock for UIImageView. Image assignment, needed to add on the final image.

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: # 10

dispatch_main_sync_safe(^{
    if(! wself)return;
    if (image) {
        wself.image = image;
        [wself setNeedsLayout];
    } else {
        if ((options & SDWebImageDelayPlaceholder)) {
            wself.image = placeholder;
            [wself setNeedsLayout]; }}if(completedBlock && finished) { completedBlock(image, error, cacheType, url); }});Copy the code

Dispatch_main_sync_safe macro definition

Dispatch_main_sync_safe in the above code is a macro definition. If you click on it, this is how the macro is defined

#define dispatch_main_sync_safe(block)\
    if([NSThread isMainThread]) {\ block(); The \}else{\ dispatch_sync(dispatch_get_main_queue(), block); The \}Copy the code

Since images can only be drawn on the main thread, dispatch_main_sync_safe ensures that blocks can be executed on the main thread.

And finally, in the [SDWebImageManager sharedManager downloadImageWithURL: options: progress: completed:] returns the operation at the same time, A key-value pair is also added to the operationDictionary to indicate that an operation is in progress:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: # 28

[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
Copy the code

It stores the opertion in the operationDictionary for future cancels.

Now that we have analyzed this method in the SDWebImage framework, we will analyze the method in the SDWebImage manager

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
Copy the code

SDWebImageManager

You can find a description of SDWebImageManager in sdWebImagemanager.h:

The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.

This is the class behind UIImageView+WebCache that handles asynchronous downloading and image caching, Of course you also can directly use the above methods SDWebImageManager downloadImageWithURL: options: progress: completed: to download images directly.

As you can see, the main purpose of this class is to build a bridge between UIImageView+WebCache and SDWebImageDownloader and SDImageCache, so that they can work better together. Here we analyze the source code of this core method. How it coordinates asynchronous downloads and image caching.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: # 6

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

if(! [url isKindOfClass:NSURL.class]) { url = nil; }Copy the code

The function of this code is to determine if the URL is passed in correctly. If the parameter is passed in as an NSString, it will be converted to NSURL. If the conversion fails, the URL will be assigned a null value and the download operation will fail.

SDWebImageCombinedOperation

When the URL is passed in correctly, a very strange “operation” is instantiated, which is actually a subclass of NSObject that follows the SDWebImageOperation protocol. And the protocol is pretty simple:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

Copy the code

I’m just wrapping this SDWebImageOperation class into a class that looks like NSOperation but isn’t ACTUALLY NSOperation, The only thing this class has in common with NSOperation is that it can respond to the cancel method. Please read it several times.

Calling this class exists to make the code more concise, because calling the class’s Cancel method causes both operations it holds to be cancelled.

// SDWebImageCombinedOperation
// cancel # 1

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if(self.cancelBlock) { self.cancelBlock(); _cancelBlock = nil; }}Copy the code

This class, on the other hand, should be designed for a more concise cancel operation.

Now that we’ve got the URL, let’s get the key from the URL

NSString *key = [self cacheKeyForURL:url]; The next step is to use a key to look in the cache to see if the same image has been downloaded before.

operation.cacheOperation = [self.imageCache
		queryDiskCacheForKey:key
        			    done:^(UIImage *image, SDImageCacheType cacheType) { ... }];
        			    
Copy the code

Here call SDImageCache queryDiskCacheForKey instance methods: done: to try to obtain image data in the cache. And what this method returns is the actual NSOperation.

If we find the image in the cache, we call the completedBlock callback block to finish the image download.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: # 47

dispatch_main_sync_safe(^{
    completedBlock(image, nil, cacheType, YES, url);
});
Copy the code

If we don’t find the image, then the instance method of SDWebImageDownloader is called:

id <SDWebImageOperation> subOperation =
  [self.imageDownloader downloadImageWithURL:url
                                     options:downloaderOptions
                                    progress:progressBlock
                                   completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];
                                   
Copy the code

If this method returns the correct downloadedImage, then we store the image’s data in the global cache:

[self.imageCache storeImage:downloadedImage
	   recalculateFromImage:NO
                  imageData:data
                     forKey:key
                     toDisk:cacheOnDisk];
                     
Copy the code

And call completedBlock to add an image to UIImageView or UIButton, or whatever.

Finally, we add the cancel operation for subOperation to operation.cancelBlock. Convenient operation of cancellation.

operation.cancelBlock = ^{
    [subOperation cancel];
    }
Copy the code

SDWebImageCache

The SDWebImageCache. H class has this comment in the source code:

SDImageCache maintains a memory cache and an optional disk cache.

It maintains an in-memory cache and an optional disk cache. Let’s look at two methods that were not covered in the previous phase, starting with:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key
                                 done:(SDWebImageQueryCompletedBlock)doneBlock;
Copy the code

The main function of this method is to query the image cache asynchronously. Because the image cache can be in two places, this method first looks in memory to see if there is a cache for the image.

// SDWebImageCache
// queryDiskCacheForKey:done: # 9

UIImage *image = [self imageFromMemoryCacheForKey:key];
Copy the code

Would this imageFromMemoryCacheForKey method in SDWebImageCache maintaining cache lookup to see if there is the corresponding data in memCache, and memCache is an NSCache.

If the image cache is not found in memory, you need to find the image cache in disk, which is more troublesome..

So there’s a method called diskImageForKey which I’m not going to cover here, which is a lot of the underlying Core Foundation framework, but the file name is stored using the MD5 file name.

// SDImageCache
// cachedFileNameForKey: # 6

CC_MD5(str, (CC_LONG)strlen(str), r);
Copy the code

Not to mention the other implementation details…

If we find a corresponding image on disk, we copy it to memory for future use.

// SDImageCache
// queryDiskCacheForKey:done: # 24

UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
    CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
    [self.memCache setObject:diskImage forKey:key cost:cost];
}
Copy the code

That’s the core of SDImageCache, and I’ll show you how images can be downloaded if the cache misses.

SDWebImageDownloader

As usual, let’s take a look at the description of this class in sdWebImageDownloader. h.

Asynchronous downloader dedicated and optimized for image loading.

Dedicated and optimized image asynchronous downloader.

The core function of this class is to download images, and the core methods are described above:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
        options:(SDWebImageDownloaderOptions)options
       progress:(SDWebImageDownloaderProgressBlock)progressBlock
      completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
Copy the code

The callback

This method directly calls another key method:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                     forURL:(NSURL *)url
             createCallback:(SDWebImageNoParamsBlock)createCallback
Copy the code

It adds a callback block to the download operation and performs some operations while the download is in progress or at the end of the download. First read the source code for this method:

// SDWebImageDownloader
// addProgressCallback:andCompletedBlock:forURL:createCallback: # 10

BOOL first = NO;
if(! self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download requestfor the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

if (first) {
    createCallback();
}
Copy the code

The callback method first checks to see if the URL has a callback, using a dictionary URLCallbacks held by the downloader.

If the callback is added for the first time, first = YES is executed. This assignment is critical because if first is not YES, the HTTP request will not be initialized and the image will not be retrieved.

This method then revises the callback block stored in URLCallbacks.

NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
Copy the code

If it is the first to add a callback blocks, you will run directly this createCallback this block, and the block, is what we in the previous method downloadImageWithURL: options: progress: completed: The callback block passed in.

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: # 4

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];
Copy the code

Let’s examine the code passed in with no arguments. First, this code initializes an NSMutableURLRequest:

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: # 11

NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
		initWithURL:url
        cachePolicy:...
    timeoutInterval:timeoutInterval];
Copy the code

This request is used to send HTTP requests later.

After the initialization of the request, and initialize a SDWebImageDownloaderOperation instances, this instance, is used to request the operation of the network resources. It’s a subclass of NSOperation,

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: # 20

operation = [[SDWebImageDownloaderOperation alloc]
		initWithRequest:request
                options:options
               progress:...
              completed:...
              cancelled:...}];
              
Copy the code

But after initialization, the operation will not start (the NSOperation instance will only be executed if the start method is called or the NSOperationQueue is joined), and we need to add the operation to an NSOperationQueue.

// SDWebImageDownloader
// downloadImageWithURL:option:progress:completed: # 59

[wself.downloadQueue addOperation:operation];
Copy the code

This operation will only take place if it is added to the download queue.

SDWebImageDownloaderOperation

This is the class that handles HTTP requests and URL connections. When an instance of this class is enqueued, the start method is called, and the start method first generates an NSURLConnection.

// SDWebImageDownloaderOperation
// start # 1

@synchronized (self) {
    if (self.isCancelled) {
        self.finished = YES;
        [self reset];
        return;
    }
    self.executing = YES;
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    self.thread = [NSThread currentThread];
}
Copy the code

The connection will then run:

// SDWebImageDownloaderOperation
// start # 29

[self.connection start];
Copy the code

It will be a SDWebImageDownloadStartNotification notice

// SDWebImageDownloaderOperation
// start # 35

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
Copy the code

The agent

After the start method invocation is NSURLConnectionDataDelegate agent in the method call.

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
Copy the code

The first two of the three proxy methods constantly call back to the progressBlock to indicate the progress of the download.

The last proxy method calls completionBlock to update the last UIImageView.image after the image is downloaded.

The progressBlock completionBlock cancelBlock that was called was stored in the URLCallbacks dictionary earlier.

So far, we’ve basically parsed SDWebImage

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]].Copy the code

This method performs the entire process.

conclusion

The image loading process of SDWebImage actually fits our intuition:

View the cache cache hit * return image update UIImageView cache hit * Asynchronously download image add to cache update UIImageView