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