SDWebImage 4.0.0 Source Parsing

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

SDWebImage Source Code Interpretation

SDWebImage Source (1) - An Overview of SDWebImage

iOS development - do you really use SDWebImage?

Keywords: iOS Session network Programming

Added by keigowei on Wed, 12 Jun 2019 02:28:27 +0300