Preface:

There are a lot of articles about SDWebImage on the Internet. Today’s sD-related article is on the one hand to learn the excellent open source framework code, and on the other hand to summarize the excellent ideas in the framework. The accumulation of knowledge itself is also to summarize. This blog focuses on the partial implementation of these classes:

  • SDWebImageManager
  • SDImageCache
  • SDWebImageDownloader
  • conclusion

A, SDWebImageManager

SDWebImageManager is the core of the SDWebImage class that manages SDWebImageDownloader and SDImageCache SDWebImageDownloader downloader object for the picture, It mainly manages SDWebImageDownloaderOperation download of images, SDImageCache mainly dealing with the image cache, first analyze the SDWebImageManage look what it did:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock Completed: (completedBlock SDWebImageCompletionWithFinishedBlock) {/ / package download operation object __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; // To prevent multithreaded access errors, Synchronized (self.failedurls) {isFailedUrl = [self.failedurls containsObject:url]; synchronized (self.failedurls) {isFailedUrl = [self.failedurls containsObject:url]; } if (url.absoluteString.length == 0 || (! (options & SDWebImageRetryFailed) && isFailedUrl)) { dispatch_main_sync_safe(^{ NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; completedBlock(nil, error, SDImageCacheTypeNone, YES, url); }); return operation; } @synchronized (self.runningOperations) {[self.runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; CacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {if (operation.iscancelled) {@synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; ImageDownloader is called to download images. The internal implementation of imageDownloader will be described in a moment.  id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (! strongOperation || strongOperation.isCancelled) { // Do nothing if the operation was cancelled // See #699 for more details // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data } else if (error) { dispatch_main_sync_safe(^{ if (strongOperation && ! strongOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } }); if ( error.code ! = NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code ! = NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code ! = NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code ! = NSURLErrorCannotConnectToHost) {/ / failed download url add images to failedURLs set @ synchronized (self. FailedURLs) {[self. FailedURLs addObject:url]; }}} else {// Download failed, Remove url if ((options & SDWebImageRetryFailed)) {@synchronized (self.failedurls) {synchronized (self.failedurls) { [self.failedURLs removeObject:url];}} // whether to cacheOnDisk BOOL cacheOnDisk =! (options & SDWebImageCacheMemoryOnly); if (options & SDWebImageRefreshCached && image && ! downloadedImage) { // Image refresh hit the NSURLCache cache, Else if (downloadedImage && (! downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate RespondsToSelector: @ the selector (imageManager: transformDownloadedImage: withURL:)]) {...} else {/ / if the download is complete and the image is cached images (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (strongOperation && ! strongOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } });  } } if (finished) { @synchronized (self.runningOperations) { if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } } }]; operation.cancelBlock = ^{ [subOperation cancel];  @synchronized (self.runningOperations) { __strong __typeof(weakOperation) strongOperation = weakOperation;  if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } }; } else if (image) {// completedBlock dispatch_main_sync_safe(^{__strong __typeof(weakOperation)) strongOperation = weakOperation; if (strongOperation && ! strongOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } });  @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; }} else {// callback failure dispatch_main_sync_safe(^{__strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && ! weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); } });  @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; }Copy the code

Second, the SDImageCache

Above is made a brief analysis about SDWebImageManager source code, I want to here to emphatically analyze the entrance: first SD call queryDiskCacheForKey: done: to see if any in memory we want pictures, so what it did for:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { ... // In memory search, SD memory cache using NSCache implementation. UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } NSOperation *operation = [NSOperation new]; // Create a sub-thread to execute the tasks in the block to the ioQueue. This prevents I/O operations from blocking the main thread. Can see ioQueue actually _ioQueue = dispatch_queue_create (" com. Hackemist. SDWebImageCache ", DISPATCH_QUEUE_SERIAL); SD uses GCD asynchronous serial to achieve IO operation, which not only ensures that UI is not blocked, but also ensures the serial execution of the code in the block, preventing data errors caused by multithreaded access. Dispatch_async (self.ioQueue, ^{if (operation.isCancelled) {return; } @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); }}); return operation; }Copy the code

1. Self. IoQueue actually for _ioQueue = dispatch_queue_create (” com. Hackemist. SDWebImageCache DISPATCH_QUEUE_SERIAL); SD uses GCD asynchronous serial to achieve IO operations, which not only ensures that UI is not blocked, but also ensures that the code in the block is executed in serial. In fact, including the subsequent disk writing operations are executed in this ioQueue, the main purpose is to prevent data competition caused by multithreading access and data errors.

2.@autoreleasepool, SD is optimized for memory using autofree pools. DiskImage objects actually take up a lot of memory if the image is large, In addition, the image object returned by self diskImageForKey:key is actually released automatically by autoRelease. As a result, this object can only be released in the autoReleasepool in the next event loop, which increases memory and affects performance.

Third, SDWebImageDownloader

If the image is not locally available then it goes to imageDownloader. ImageDownloader is a downloader object that handles the logic for downloading the image. So what is implemented in imageDownloader?

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { . __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; Operation = [[wself.operationClass alloc] initWithRequest: Request inSession:self.session options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (! sself) return; __block NSArray *callbacksForURL;  dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; });  for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];  if (callback) callback(receivedSize, expectedSize); });  } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself;  if (! sself) return; __block NSArray *callbacksForURL;  dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy];  if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } });  for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished);  } } cancelled:^{ SDWebImageDownloader *sself = wself; if (! sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }];  operation.shouldDecompressImages = wself.shouldDecompressImages; If (wself.urlcredential) {operation.credential = wself.urlcredential;  } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } / / download priority if (options & SDWebImageDownloaderHighPriority) {operation. QueuePriority = NSOperationQueuePriorityHigh;  } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow;  } [wself.downloadQueue addOperation:operation]; / / set is download the order according to the queue or stack the if (wself. ExecutionOrder = = SDWebImageDownloaderLIFOExecutionOrder) {/ / Emulate LIFO execution order by systematically adding new operations as last operation's dependency [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; }Copy the code

It focuses on my addProgressCallback: completedBlock: forURL: createCallback: the implementation of look at it all did something:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { ... // dispatch_barrier_sync(self.barrierQueue, ^{BOOL first = NO; //URLCallbacks actually store a mutable dictionary of all image download callbacks urls to the address of the image we requested, with the URL as the key and the value as a mutable array. So what's stored in a mutable array? Let's go down. if (! self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; NSMutableArray *callbacksForURL = self.urlcallbacks [url]; // Create a mutable dictionary callbacks that actually stores the progress of the download and the completion callback block. NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; // Add this mutable dictionary to the mutable array callbacksForURL we just created. [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; // If it is the first download, i.e. the URLCallbacks store all download callbacks in a dictionary that does not have the current one, then it is considered the first download and executes createCallback() to download the image, otherwise nothing is done. if (first) { createCallback(); }}); }Copy the code

1. The dispatch_barrier_sync block, as its name implies, is a block that intercepts all tasks in the queue before it is executed. And barrierQueue actually _barrierQueue = dispatch_queue_create (” com. Hackemist. SDWebImageDownloaderBarrierQueue “, DISPATCH_QUEUE_CONCURRENT); In other words, in a concurrent queue, all the task blocks added before the barrier are executed, and then the task blocks added after the barrier are executed. In this way, layer locks are added to the contents of the blocks to ensure thread safety.

2.URLCallbacks is a mutable dictionary that stores all block callbacks that called the download. If it’s the first download then the download is executed, if it’s not the first time then the callback is stored inside the URLCallbacks and nothing is done. I’ll use the download schedule as an example to explore what URLCallbacks do. In its callback it is implemented like this:

SDWebImageDownloader *sself = wself; if (! sself) return; __block NSArray *callbacksForURL; Dispatch_sync (sself.barrierQueue, ^{// All download callbacks to this url, array type, store all download callbacks to this URL. callbacksForURL = [sself.URLCallbacks[url] copy]; }); For (NSDictionary *callbacks in callbacksForURL) {// Back to the main thread, Dispatch_async (dispatch_get_main_queue(), dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); });Copy the code

Iv. Summary:

1. The use of GCD, multi-threaded locking to prevent resource competition and the use of barrier blocks.

2. Use @Autoreleasepool to optimize the memory if the for loop or resource is too expensive.

3. In the case of downloading the same resource from different places, we can try to use SD block callback storage and callback timing strategy to ensure that only one resource is downloading, and different places can get callback, progress callback, completion callback, failure callback, etc.