I have seen the source code of SDWebImage before. I was just looking at it for fun, but this time I came to see it for the purpose of learning. The following two sections are code parsing and design thinking. I’m sorry to say that I feel like I’m paying off my debt when I look at the code. I hope this article gives you a different perspective.

SDWebImageDownloader

init

- (nonnull instancetype)init {
    return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}

- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
    self = [super init];
    if (self) {
        ...
    }
    return self;
}
Copy the code

If config is not passed when initializing SDWebImageDownloader, the default defaultDownloaderConfig is used, which records the maximum number of concurrent requests, timeout duration, and other configurations. SDWebImageDownloader has a lot of non-mandatory input parameters, if passed one by one will increase the complexity of the SDWebImageDownloader, use config to uniformly close, and provide the default setting defaultDownloaderConfig. Another small detail is that defaultDownloaderConfig is a singleton. Even if the SDWebImageDownloader is created frequently, defaultDownloaderConfig is created only once to reduce resource consumption.

_config = [config copy];
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
Copy the code

Copy config to prevent the caller from modifying config to cause inconsistent status. If the parameters in config need to be modified later, the following methods shall be used:

SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] initWithConfig:config]; downloader.config.maxConcurrentDownloads = 4; _downloadQueue = [NSOperationQueue new]; / / set the maximum number of concurrent _downloadQueue. MaxConcurrentOperationCount = _config. MaxConcurrentDownloads; _downloadQueue.name = @"com.hackemist.SDWebImageDownloader"; Operation _URLOperations = [NSMutableDictionary new]; NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary]; // create a lock for _HTTPHeadersLock SD_LOCK_INIT(_HTTPHeadersLock); // create a lock for _operationsLock SD_LOCK_INIT(_operationsLock); / / create a default configuration session NSURLSessionConfiguration * sessionConfiguration = _config sessionConfiguration; if (! sessionConfiguration) { sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; } _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];Copy the code

Initialize the _HTTPHeadersLock and _operationsLock locks, where SD_LOCK_INIT(lock) used the spin lock before iOS10 and os_UNFAIR_lock after. Os_unfair_lock is an OSSpinLock that is a busy wait lock. The thread checks whether the resource is available and does not suspend, avoiding the overhead of context switching.

downloadImageWithURL

/ / lock SD_LOCK (_operationsLock); id downloadOperationCancelToken; / / by url for operation NSOperation < SDWebImageDownloaderOperation > * operation = [self. URLOperations objectForKey: url]; if (! operation || operation.isFinished || operation.isCancelled) { operation = [self createDownloaderOperationWithUrl:url options:options context:context]; if (! Operation) {// unlock SD_UNLOCK(_operationsLock); // Create operation failed to call up... } @weakify(self); operation.completionBlock = ^{ @strongify(self); if (! self) { return; } // Delete operation SD_LOCK(self->_operationsLock); [self.URLOperations removeObjectForKey:url]; SD_UNLOCK(self->_operationsLock); }; self.URLOperations[url] = operation; downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; [self.downloadQueue addOperation:operation]; } // unlock SD_UNLOCK(_operationsLock);Copy the code

The isCancel and isFinished properties of opertaion will also change in a new thread, so the Operation will need to be locked. One more detail, if you want to listen for Operation completion, you need to configure completionBlock before addOperation, Because after addOperation the NSOperation is automatically removed from the NSOperationQueue, it might have been done before listening if it had not been in the current order.

// Set the priority of operation according to the configuration. operation.isExecuting) { if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } else { operation.queuePriority = NSOperationQueuePriorityNormal; }}Copy the code

createDownloaderOperationWithUrl:options:context

Create Request and configure the corresponding properties.

NSTimeInterval timeoutInterval = self.config.downloadTimeout; If (timeoutInterval == 0.0) {timeoutInterval = 15.0; } NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval]; mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies); mutableRequest.HTTPShouldUsePipelining = YES; SD_LOCK(_HTTPHeadersLock); mutableRequest.allHTTPHeaderFields = self.HTTPHeaders; SD_UNLOCK(_HTTPHeadersLock);Copy the code

Pipelining (Pipelining) is introduced in HTTP1.1 to reduce the wait time required for each request to be processed before the next request is processed, as shown below.

Keep going

/ / if the context is not spread requestModifier use itself requestModifier id < SDWebImageDownloaderRequestModifier > requestModifier; if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) { requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier]; } else { requestModifier = self.requestModifier; } NSURLRequest *request; / / if requestModifier call modifiedRequestWithRequest if (requestModifier) {NSURLRequest * modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]]; if (! modifiedRequest) { return nil; } else { request = [modifiedRequest copy]; } } else { request = [mutableRequest copy]; }Copy the code

RequestModifier is a great design that gives outsiders the opportunity to modify the request a second time.

requestModifier = [SDWebImageDownloaderRequestModifier requestModifierWithBlock:^NSURLRequest * _Nullable(NSURLRequest *  _Nonnull request) { if ([request.URL.absoluteString isEqualToString:kTestPNGURL]) { NSMutableURLRequest *mutableRequest  = [request mutableCopy]; [mutableRequest setValue:@"Bar" forHTTPHeaderField:@"Foo"]; NSURLComponents *components = [NSURLComponents componentsWithURL:mutableRequest.URL resolvingAgainstBaseURL:NO]; components.query = @"text=Hello+World"; mutableRequest.URL = components.URL; return mutableRequest; } }]; downloader.requestModifier = requestModifier;Copy the code

Here is a brief explanation of the order of execution using a test case from SDWebImage.

The ResponseModifier and DownloaderDecryptor below do the same, throwing the timing of the processing to the caller so that they can do personalized processing.

Class operationClass = self.config.operationClass;
if (operationClass && 
    [operationClass isSubclassOfClass:[NSOperation class]] && 
    [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
        // Custom operation class
} else {
    operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
Copy the code

The operationClass design is also very clever, through operationClass outside can switch to their own ability to download, just need to inherit the NSOperation and follow SDWebImageDownloaderOperation agreement. If you want to replace SDWebImageDownloaderOperation SDWebImageTestDownloadOperation to refer to.

So how does SDWebImage work?

The diagram for these classes is as follows:

First of all, SD willSDWebImageDownloaderIf you useOperationIs abstracted as<SDWebImageDownloaderOperation>.SDWebImageDownloaderDo not use implementation classes directlySDWebImageDownloaderOperation, in need of useOperationPlace through useNSOperation <SDWebImageDownloaderOperation>To complete. For example, one of the operations:

NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
Copy the code

You only need to specify which implementation class to use when you create Operation.

NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
Copy the code

Method in the rest of the NSURLSessionTaskDelegate and NSURLSessionDataDelegate are distributed to operation agent will receive a callback, this part of the resolution to SDWebImageDownloaderOperation.

SDWebImageDownloaderOperation

initWithRequest

_request = [Request copy] _request = [Request copy] _request = [Request copy]; _options = options; _context = [context copy]; _context = [context copy]; _callbackBlocks = [NSMutableArray new]; _responseModifier = context[SDWebImageContextDownloadResponseModifier]; _decryptor = context[SDWebImageContextDownloadDecryptor]; Session _unownedSession = session; // Create a serial encoding queue _coderQueue = [NSOperationQueue new]; _coderQueue.maxConcurrentOperationCount = 1; _backgroundTaskId = UIBackgroundTaskInvalid;Copy the code

Making good use of copy when retrieving input parameters reduces the number of nameless problems.

// If cancelled, set Finish to YES, call back and reset if (self.iscancelled) {if (! self.isFinished) self.finished = YES; // Operation cancelled by user before sending the request [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]]; [self reset]; return; } Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak typeof(self) wself = self; UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; Self. BackgroundTaskId = [app beginBackgroundTaskWithExpirationHandler: ^ {/ / cancellation task [wself cancel];}]. }Copy the code
// If unownedSession is null NSURLSession *session = self.unownedSession; if (! The session) {/ / create ownedSession NSURLSessionConfiguration * sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest = 15; session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; self.ownedSession = session; }Copy the code

When is unownedSession null? I’ll talk about that later.

If (self. The options & SDWebImageDownloaderIgnoreCachedResponse) {/ / according to request access to cache the response NSURLCache * URLCache = session.configuration.URLCache; if (! URLCache) { URLCache = [NSURLCache sharedURLCache]; } NSCachedURLResponse *cachedResponse; // cachedResponseForRequest is non-thread-safe @synchronized (URLCache) {cachedResponse = [URLCache cachedResponseForRequest:self.request]; If (cachedResponse) {self.cachedData = cachedresponse.data; }} / / SDWebImageDownloader code in NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;Copy the code

System default set up memory and disk space to cache our network request, by default caching policy is NSURLRequestUseProtocolCachePolicy, this represents the HTTP caching strategies. But why cache data in IgnoreCache? Let’s look at this in conjunction with the cache.

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { ... if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) { self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}]; // call completion block with not modified error [self callCompletionBlocksWithError:self.responseError]; [self done]; }... }Copy the code

If the cache is ignored, an error is reported if the requested data is the same as the cached data.

HTTPThe abbreviated logic of the protocol caching policy is as follows.

/ / set up according to the configuration dataTask and decoding the priority queue if (self. The options & SDWebImageDownloaderHighPriority) {self. DataTask. Priority = NSURLSessionTaskPriorityHigh; self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive; } else if (self.options & SDWebImageDownloaderLowPriority) { self.dataTask.priority = NSURLSessionTaskPriorityLow; self.coderQueue.qualityOfService = NSQualityOfServiceBackground; } else { self.dataTask.priority = NSURLSessionTaskPriorityDefault; self.coderQueue.qualityOfService = NSQualityOfServiceDefault; } // dataTask start request [self.datatask resume]; // No new downloads will be opened for images from the same URL. Just add a new monitor / / trigger all listen callback for (SDWebImageDownloaderProgressBlock progressBlock in [the self callbacksForKey:kProgressCallbackKey]) { progressBlock(0, NSURLResponseUnknownLength, self.request.URL); } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^ {/ / notifications [[NSNotificationCenter defaultCenter] postNotificationName: SDWebImageDownloadStartNotification object:strongSelf]; });Copy the code

Each NSURLSession object has its own socket reuse pool. Tasks managed by the same NSURLSession are limited in concurrent requests, and priority controls the priority of these tasks.

QualityOfService specifies the service level applied to the operation object added to the queue. The service level determines the priority of the operation object accessing system resources, such as CPU time, network resources, and disk resources. The operation with a higher service level has a higher priority. MainQueue service level is at the highest NSQualityOfServiceUserInteractive, don’t abuse SDWebImageDownloaderHighPriority, because with the home team competition resources.

Notification is forwarded in the thread where it is posted, not necessarily in the thread where the observer is registered. In this case, the main thread is cut to prevent problems with external UI operations

URLSession:dataTask:didReceiveResponse:completionHandler:

Callback time: when the server first receives the Resopnse header data.

// If the caller needs to modify response, If (self.responsemodifier && Response) {response = [self.responsemodifier modifiedResponseWithResponse:response]; // Prevent callers from returning nil if (! response) { valid = NO; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}]; }} / / record data NSInteger expected = (NSInteger) response. ExpectedContentLength; expected = expected > 0 ? expected : 0; self.expectedSize = expected; self.response = response; NSInteger statusCode = [Response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200; BOOL statusCodeValid = statusCode >= 200 && statusCode < 400; if (! statusCodeValid) { valid = NO; / / the self assembly failure information. ResponseError = [NSError errorWithDomain: SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}]; }Copy the code

URLSession:dataTask:didReceiveData:

/ / only set up to support distributed load and are not encrypted image can step by step loading BOOL supportProgressive = (self. The options & SDWebImageDownloaderProgressiveLoad) &&! self.decryptor; if (supportProgressive && ! NSData *imageData = [self.imageData copy]; / / if you have any task executing decoding the queue is skipped if (self) coderQueue) operationCount = = 0) {@ weakify (self); [self.coderQueue addOperationWithBlock:^{@stronGify (self); if (! Self) {return;} // Decode the image UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], The self. The context); the if (image) {/ / callback the picture to the upper [self callCompletionBlocksWithImage: image imageData: nil error: nil finished: NO]; }}]; }}Copy the code

This article only aims at the analysis of the download module, and will write the part of picture decoding separately later.

URLSession:task:didCompleteWithError:

Triggered when data transfer is complete.

@synchronized(self) {
    self.dataTask = nil;
    __block typeof(self) strongSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
        if (!error) {
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
        }
    });
}
Copy the code

When using self.dataTask, the @synchronized block is used for protection. Although no new thread is displayed, the system automatically specifies a serial queue when creating an NSURLSession if the queue for its proxy callback is not specified. In this case, the thread on which the proxy callback is invoked may not be the same thread on which the Operation task is executed, so @synchronized is required to protect self.datatask.

Talk about the design

Config

We are encapsulating separate function modules, providing constructors to receive arguments from callers when needed, but it becomes extremely complicated when there are many arguments. In this case, we can use the Config mode, we can pass some mandatory parameters using the constructor, other non-mandatory parameters in Config, and if you need to set the default value can be done by providing defaultConfig, Android is through the builder mode to solve.

Dependency injection

The idea of dependency injection is used in many places in SDWebImage, such as the implementation of operationClass mentioned above, which is common in Android and rarely done in iOS. Can return to the interpretation of specific requestModifier and SDWebImageDownloaderOperation above part.

SDWebImage design does make me learn more things, SDWebImageDownloader to download all the callback were distributed to SDWebImageDownloaderOperation to specific processing, Such are the benefits of SDWebImageDownloader can easily use other Customer Operation, and SDWebImageDownloaderOperation also can be used alone, In order to ensure that it can be used alone, the ownedSession and other corresponding processing is added.

Why use NSOperation?

NSURLSession’s proxy call-back is in a serial queue, which itself is in a new child thread, and a queue is created to handle image encoding. There is no other time-consuming task that needs to be done by a word thread. After looking at the code, I guessed that it was probably to reuse the queue management of NSOperation, which is really a stable choice because of the various priority Settings. Reflect on yourself when doing projects, for their own interests, repeated many wheels.

NS_OPTIONS and NS_ENUM

NS_ENUM is used for single selection, NS_OPTIONS is used for multiple selection.

These are my thoughts when I read the download part of SDWebImage. I hope I can provide some different ideas to students who read this article.