In the development of iOS client applications, it is often necessary to download pictures from the server. Although the system provides download tools: NSData, NSURLSession and so on, there are many factors to consider in the process of downloading pictures, such as asynchronous download, image caching, error handling, coding and decoding, and loading different pictures according to different networks. Pictures and other requirements, so download operation is not a simple download action can be solved.
To solve the above problems, SDWebImage is a common open source library. It solves the problems of asynchronous downloading, image caching, error handling and so on. It has been widely used, making it very convenient to set up pictures of UIImageView and UIButton objects. From the point of view of source code, this paper analyses the specific implementation of this excellent open source library.
Class Structure Diagram
The class structure diagram and download flow chart of SDWebImage source code are officially available. Description document There are introductions. The internal structure of the framework is introduced in detail through the UML class structure diagram, and the download process is introduced through the flow chart.
The following figure is the structure diagram of SDWebImage summarized by me. Simply divide the SDWebImage source files according to their functions. It is convenient to have a general understanding of the source code and speed up the reading efficiency when reading the source code.
![](http://upload-images.jianshu.io/upload_images/1843940-c51585b28704fae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)Introduction to Key Class Functions:
- 1. Download class
SDWeb Image Downloader: Provides download methods for SDWeb Image Manager to use, provides maximum concurrent download control, timeout, cancel download, download hang, whether to decompress pictures, and so on. At the same time, it also provides notifications for users to start and stop downloading. If users do not need to monitor the download status, they do not need to monitor the notification. This design mode is flexible and provides users with more convenient choices.
extern NSString * _Nonnull const SDWebImageDownloadStartNotification; extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
SDWeb Image Downloader Operation: Inherited from NSOperation, it is a specific implementation class of image download, which is added to NSOperationQueue and then opened in the start method.
- 2. Picture Caching
SDImageCacheConfig: Provides cache configuration information, such as whether to decompress images, whether to cache in memory, maximum cache time (default is a week) and the maximum number of bytes cached, and so on.
SDImageCache: The cache implementation class provides the control of the largest cache bytes, the largest cache entries, and the functions of caching to memory and disk, deleting from memory or disk, querying and retrieving cache information.
- 3. Classification
UIImageView+WebCache: The classification of UIImageView provides a variety of ways to set up UIImageView object images. The following methods can be said to be the most commonly used in SD WebImage framework.
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options; // Assignment Method with Complete block - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock;
UIButton+WebCache: The classification of UIButton provides the function of setting button pictures and button background pictures.
- (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder;
- 4. Tool class
SDWeb Image Decoder: Tool class for image decoding, which is decoded immediately by loading images through imageNames, but not by imageWithContentsOfFile:
SDWeb Image Prefetcher: Batch Image Download Tool. When you need to download more than one picture in UI interface and keep smooth experience in sliding, you can use this tool class to download pictures in batches, and then when you set pictures for specific UI controls, you can get them directly from the cache.
SDWebImage Manager: Download management class tool is the core class of SDWebImage. It can be seen from the class diagram of official documents. It provides functions of checking whether the picture has been cached, downloading the picture, caching the picture, canceling all downloads and so on.
- 5. Picture format class
NSData + Image Content Type: Get the format of the image according to the first byte of the image data, which can distinguish PNG, JPEG, GIF, TIFF and WebP.
The above is just a simple analysis of SDWebImage class structure diagram. If you need to know more about the specific implementation of each class, please refer to the information at the end of the article. Some people have introduced the principle or method of functional implementation of each class in detail.
application
Here's a source code implementation of setting up UI pictures using SDWebImage
Application of UI Image View
Setting Pictures
Set up pictures for UIImageView objects by setting URL s, placeholder pictures, image configurations, picture download progress callbacks, and settings to complete callbacks
// ViewController.m [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://rescdn.qqmail.com/dyimg/20140302/73EB27F4A350.jpg"] placeholderImage:[UIImage imageNamed:@"gift-icon"] options:0 progress:nil completed:nil];
The above code calls the method in UIImageView+WebCache.m
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:nil setImageBlock:nil progress:progressBlock completed:completedBlock]; }
Then call the method in UIView+WebCache.m to get the picture, and then make different settings according to the type of option.
// UIView+WebCache.m - (void)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options operationKey:(nullable NSString *)operationKey setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { ... if (url) { ... __weak __typeof(self)wself = self; // Start loading pictures id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { ... dispatch_main_async_safe(^{ if (!sself) { return; } if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { // Putting a picture in a completed block is usually done manually, because it allows further processing of the picture. completedBlock(image, error, cacheType, url); return; } else if (image) { // Setting Pictures [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock]; [sself sd_setNeedsLayout]; } else { // Delayed loading of placeholder maps (after taking pictures) if ((options & SDWebImageDelayPlaceholder)) { [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock]; [sself sd_setNeedsLayout]; } } // The callback completes the block, but if it is nil, it does not call it if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; } else { // Handling the case where url is nil dispatch_main_async_safe(^{ [self sd_removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); } }
The specific implementation code for loading images is in SDB Image Manager. First, the images are taken from the cache. If there are no pictures in the cache, the pictures are downloaded from the network. Then, the pictures are set up. Finally, the pictures are cached.
// SDWebImageManager.m - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { ... // Pictures from Cache operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { if (operation.isCancelled) { // If the operation is cancelled, the operation is deleted from the runningOperations operation array [self safelyRemoveOperationFromRunning:operation]; return; } if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { if (cachedImage && options & SDWebImageRefreshCached) { // If options are set to update the cache, you need to download new images from the server and then update the local cache. [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; } ... // Create a downloader to download pictures from the server SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; ... else { // Setting options to retry for failure adds the failed url to the failed URLs array if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } } ... } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { // transform the picture dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; // pass nil if the image was transformed, so we can recalculate the data from the image [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil]; } [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; }); } else { // There are two ways to cache pictures, one is to cache them to memory and the other is to cache them to disk. if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil]; } // Callback completed block [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } } if (finished) { // When the download is complete, delete the operation from the runningOperations array [self safelyRemoveOperationFromRunning:strongOperation]; } }]; // Setting callbacks to cancel Downloads operation.cancelBlock = ^{ [self.imageDownloader cancel:subOperationToken]; __strong __typeof(weakOperation) strongOperation = weakOperation; [self safelyRemoveOperationFromRunning:strongOperation]; }; } else if (cachedImage) { // From the cache to the picture, the callback completes the block __strong __typeof(weakOperation) strongOperation = weakOperation; [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; [self safelyRemoveOperationFromRunning:operation]; } ... }]; return operation; }
To retrieve pictures from the cache, we first retrieve them from memory, and if they are retrieved from memory, we call back doneBlock directly in the current thread; if there is no memory, we open a sub-thread to retrieve them from disk; if we retrieve pictures, we call back doneBlock.
// SDImageCache.m - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock { ... // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { NSData *diskData = nil; if ([image isGIF]) { diskData = [self diskImageDataBySearchingAllPathsForKey:key]; } if (doneBlock) { doneBlock(image, diskData, SDImageCacheTypeMemory); } return nil; } NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { // do not call the completion if cancelled return; } @autoreleasepool { // data of Pictures from Disk NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; // Take pictures directly from disk UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); // Cached into memory [self.memCache setObject:diskImage forKey:key cost:cost]; } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, SDImageCacheTypeDisk); }); } } }); return operation; }
The process of downloading images is carried out in SDWebImageDownloader.m. In essence, the object is added to download Queue through SDWebImageDownloader Operation (inherited from NSOperation), and then the image is downloaded through NSURLSession in the start method. (NSOperation has two methods: main and start. If you want to use synchronization, the easiest way is to write logic in main() and use asynchrony. You need to write logic in start() and then join the queue.)
// SDWebImageDownloader.m - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { __weak SDWebImageDownloader *wself = self; return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{ __strong __typeof (wself) sself = wself; NSTimeInterval timeoutInterval = sself.downloadTimeout; // Set timeout time to 15s if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; if (sself.headersFilter) { request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = sself.HTTPHeaders; } SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options]; operation.shouldDecompressImages = sself.shouldDecompressImages; ... // Join the operation queue and start downloading [sself.downloadQueue addOperation:operation]; ... return operation; }]; }
Add the SDWebImageDownloaderOperation object to the operation queue and start calling the object's start method.
// SDWebImageDownloaderOperation.m - (void)start { // If the operation is cancelled, reset settings are set @synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } ... NSURLSession *session = self.unownedSession; if (!self.unownedSession) { // Create session configuration NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest = 15; // Create session objects self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; session = self.ownedSession; } self.dataTask = [session dataTaskWithRequest:self.request]; self.executing = YES; } // Start downloading tasks [self.dataTask resume]; if (self.dataTask) { for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { progressBlock(0, NSURLResponseUnknownLength, self.request.URL); } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); } else { // Failed to create task [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]]; } ... }
In the download process, it will involve authentication, status code judgment of response (404, 304, etc.), and progress callback after receiving data, etc. Do the final processing in the final didCompleteWithError, and then call back the completed block. Here, I will only analyze the didCompleteWithError method.
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { ... if (error) { [self callCompletionBlocksWithError:error]; } else { if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { /** * See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash * Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response * and images for which responseFromCached is YES (only the ones that cannot be cached). * Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication */ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) { // If options ignore the cache and the picture is taken from the cache, the callback is passed in nil [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES]; } else if (self.imageData) { UIImage *image = [UIImage sd_imageWithData:self.imageData]; // Cache Pictures NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; // Jump image size image = [self scaledImageForKey:key image:image]; // Do not force decoding animated GIFs if (!image.images) { // Not Gif images if (self.shouldDecompressImages) { if (self.options & SDWebImageDownloaderScaleDownLargeImages) { #if SD_UIKIT || SD_WATCH image = [UIImage decodedAndScaledDownImageWithImage:image]; [self.imageData setData:UIImagePNGRepresentation(image)]; #endif } else { image = [UIImage decodedImageWithImage:image]; } } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { // Download is image size 0 [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; } else { // Callback the downloaded image as a parameter [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES]; } } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; } } } ... }
Above is the process of setting pictures for UIImageView objects. It can be seen that it is still more complicated and has to admire the author's coding ability. As for UIButton's image setup process, the analysis is similar, and no analysis is made here.
SDWebImage source code in the process of setting pictures, but also applied a variety of technologies: GCD thread group, lock mechanism, concurrency control, queue, image decoding, cache control, etc., is a very comprehensive project, through reading the source code, the use of these technologies has been further recognized, the author's programming ability is deeply convinced.
At the end of the analysis of SDWebImage, this paper simply analyses the source code structure and the use of UIImageView. I hope it will be helpful to the friends who read the source code. If there are some shortcomings in this article, I hope to point out that we can learn from each other.
Reference material
SDWebImage Source Code Interpretation