This article is based on SDWebImage 5.6. The reason for reread was also to find that the API was constantly iterating, with many structures different from earlier versions, and to keep a note. The reading sequence is also based on the API execution sequence, so it’s less detailed and more about how the framework works.

5.x Migration Guid

If you are interested, you are strongly encouraged to check out the official recommended migration document, which mentions the new features required for 5.x. It details the new features and the motivation for the changes.

  • New Animated Image View (4.0 forFLAnimatedImageView);
  • Image Transform is provided to facilitate users to add scale, rotate, rounded corner and other operations after downloading images;
  • Customization, can say everything protocol, can custom Cache, loader, coder;
  • Add the View Indicator to indicate the loading state of the Image.

It can be said that the change of 5.x lies in the protocolization of the core class in the whole SDWebImage, and the pluginization of the operation such as the request, loading, decoding and caching of the image as far as possible, so as to make it easy to extend and replace.

There are many types of protocol, but only a few are listed here:

4.4 5.x
SDWebImageCacheSerializerBlock id<SDWebImageCacheSerializer>
SDWebImageCacheKeyFilterBlock id<SDWebImageCacheKeyFilter>
SDWebImageDownloader id<SDImageLoader>
SDImageCache id<SDImageCache>
SDWebImageDownloaderProgressBlock id<SDWebImageIndicator>
FLAnimatedImageView id<SDAnimatedImage>

View Category

As the upper API call is implemented by providing convenience methods on top of UIView + WebCache, including the following:

  • UIImageView+HighlightedWebCache
  • UIImageView+WebCache
  • UIView+WebCacheOperation
  • UIButton+WebCache
  • NSButton+WebCache

To get started, let’s take a look at sdwebImagecompat. h, which defines three macros, SD_MAC, SD_UIKIT, and SD_WATCH, which are used to distinguish between the apis of different systems to satisfy the requirements of compilation, and also to erase the differences between the apis of different platforms. So for example, you can use #define UIImage NSImage to unify the NSImage on the MAC into UIImage. Another thing to note:

#ifndef dispatch_main_async_safe #define dispatch_main_async_safe(block)\ if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\ block(); \ } else {\ dispatch_async(dispatch_get_main_queue(), block); \ } #endifCopy the code

Different from earlier versions of the implementation:

#define dispatch_main_async_safe(block)\ if ([NSThread isMainThread]) {\ block(); \ } else {\ dispatch_async(dispatch_get_main_queue(), block); \ } #endifCopy the code
  • #ifndefImproves code rigor and prevents duplicate definitionsdispatch_main_async_safe
  • Change the criterion from isMainThread to Dispatch_queue_T Label

On the second point, there is a discussion of SD and another on GCD’s Main Queue vs. Main Thread

Calling an API from a non-main queue that is executing on the main thread will lead to issues if the library (like VektorKit) relies on checking for execution on the main queue.

The difference is from deciding whether to execute on the main thread to whether to schedule on the main queue. Because tasks in the main queue must be executed on the main thread.

Compared to THE classification of UIImageView, UIButton needs to store images under different UIControlState and backgrounImage, Associate creates an internal dictionary (NSMutableDictionary

*)sd_imageURLStorage to store images.

SetImageUrl: finally closes all View categories to this method:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;
Copy the code

This method implementation is very long, simple to explain the process:

  1. willSDWebImageContextCopy and convert to immutablevalidOperationKeyThe default value is the class name of the current view.
  2. performsd_cancelImageLoadOperationWithKeyCancel the last task to ensure that no asynchronous download operation is currently in progress and will not conflict with the upcoming operation.
  3. Set the placeholder map;
  4. Initialize theSDWebImageManagerSDImageLoaderProgressBlockReset,NSProgress,SDWebImageIndicator;
  5. Open the downloadloadImageWithURL:And will returnSDWebImageOperationdepositsd_operationDictionaryThe key tovalidOperationKey;
  6. When you get the image, callsd_setImage:Add a Transition animation to the new image;
  7. Stop indicator when animation is over.

SDWebImageOperation is a ** strong-weak ** NSMapTable, which is also added with the associated value:

// key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
Copy the code

Weak is used because the operation instance is stored in the SDWebImageManager’s runningOperations, where only the reference is saved to facilitate cancel.

SDWebImageContext

A SDWebImageContext object which hold the original context options from top-level API.

ImageContext runs through the entire process of image processing. It brings data into each processing task step by step. There are two types of ImageContext:

typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
typedef NSMutableDictionary<SDWebImageContextOption, id>SDWebImageMutableContext;
Copy the code

SDWebImageContextOption is an extensible String enumeration that currently has 15 types. Basically, you can guess by just looking at the name, and the document is simply classified as follows:

Its importance can be seen in terms of its participation.

ImagePrefetcher

Prefetcher has little to do with the entire PROCESSING flow of SD. It mainly uses imageManger to download images in batches. The core method is as follows:

- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
Copy the code

It saves the downloaded URLs as a transaction in the SDWebImagePrefetchToken to avoid the problem that previous versions cancel the last fetching operation on each prefetchURLs:.

Each download task is in the AutoReleasesepool environment, and SDAsyncBlockOperation is used to wrap the actual download task to achieve cancelable operations:

@autoreleasepool { @weakify(self); SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) { @strongify(self); if (!self || asyncOperation.isCancelled) { return; } /// load Image ... }];  @synchronized (token) { [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation]; } [self.prefetchQueue addOperation:prefetchOperation]; }Copy the code

The task is then stored in the prefetchQueue, where the maximum number of downloads is 3 by default. While URLs download the real task in token.loadOperations:

NSPointerArray *operations = token.loadOperations; id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options context:self.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { /// progress handler }]; NSAssert(operation ! = nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic"); @synchronized (token) { [operations addPointer:(__bridge void *)operation]; }Copy the code

Use NSPointerArray loadOperations and prefetchOperations, here with the help of its NSPointerFunctionsWeakMemory features, and can be stored Null values, although its performance is not very good, See also: Basic Collection classes

Another notable feature is the thread-safe management of download state by PrefetchToken, using c++11 memory_order_relaxed.

atomic_ulong _skippedCount;
atomic_ulong _finishedCount;
atomic_flag  _isAllFinished;
    
unsigned long _totalCount;
Copy the code

That is, through memory sequence and atomic operations to achieve lockless concurrency, so as to improve efficiency. Students who are interested in the specific principles can consult the materials by themselves.

ImageLoader

SDWebImageDownloader is the default implementation of the <SDImageLoader> protocol within SD. It provides the ability to obtain images from HTTP/HTTPS/FTP or local URL NSURLSession sources. It also maximizes the configurability of the entire download process. The main properties:

@interface SDWebImageDownloader : NSObject

@property (nonatomic, copy, readonly, nonnull) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderDecryptor> decryptor;
/* ... */

-(nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
    options:(SDWebImageDownloaderOptions)options
    context:(nullable SDWebImageContext *)context
   progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
  completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

@end
Copy the code

DownloaderConfig supports the NSCopy protocol and provides the following main configurations:

/// Defaults to 6. @property (nonatomic, assign) NSInteger maxConcurrentDownloads; // Defaults to 15.0s. @property (nonatomic, assign) NSTimeInterval downloadTimeout; /// Custom Session Configuration does not support dynamic replacement type during use. @property (nonatomic, strong, nullable) NSURLSessionConfiguration *sessionConfiguration; // extend class dynamically, You need to follow ` NSOperation < SDWebImageDownloaderOperation > ` SDImageLoader customization to @ property (nonatomic, assign, nullable) Class operationClass; / / / download sequence images, the default FIFO @ property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder;Copy the code

Request modifier, provide to modify the request before downloading,

/// Modify the original URL request and return a new one instead. You can modify the HTTP header, cachePolicy, etc for this URL.

@protocol SDWebImageDownloaderRequestModifier <NSObject>
   
- (nullable NSURLRequest *)modifiedRequestWithRequest:(nonnull NSURLRequest *)request;

@end
Copy the code

Similarly, a Response modifier provides modifications to the returned value,

/// Modify the original URL response and return a new response. You can use this to check MIME-Type, mock server response, etc.

@protocol SDWebImageDownloaderResponseModifier <NSObject>

- (nullable NSURLResponse *)modifiedResponseWithResponse:(nonnull NSURLResponse *)response;

@end
Copy the code

The last decryptor is used to decrypt the image, which by default provides a Base64 conversion to imageData,

/// Decrypt the original download data and return a new data. You can use this to decrypt the data using your perfereed algorithm. @protocol SDWebImageDownloaderDecryptor <NSObject> - (nullable NSData *)decryptedDataWithData:(nonnull NSData  *)data response:(nullable NSURLResponse *)response; @endCopy the code

Processing data through this protocolized object can be said to utilize the policy pattern or dependency injection in the design pattern. When the protocol object is obtained through configuration, the caller only needs to care about the methods provided by the protocol object, and does not need to care about its internal implementation, so as to achieve the purpose of decoupling.

###DownloadImageWithURL

Before downloading, check whether the URL exists. If not, throw the error and return. After retrieving the URL, try to reuse the generated operation:

NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
Copy the code

If operation exists, call

@synchronized (operation) {
    downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
Copy the code

And set queuePriority. We’re using @synchronized(operation) here, and we’re using @synchronized(self) inside the operation, just to keep the operation thread safe between two different classes, This is because it is possible for an operation to be passed to a queue of decoders or agents. Here addHandlersForProgress: It stores the progressBlock with the completedBlock in the NSMutableDictionary

SDCallbacksDictionary then returned.

In addition, Operation does not clear the previously stored callbacks when addHandlersForProgress:. This means that multiple callbacks will be executed after completion.

If the operation does not exist, the task was cancelled, the task has been completed, call createDownloaderOperationWithUrl: options: context: create a new operation and storage in URLOperations. At the same time, the completionBlock is configured so that the URLOperations can be cleaned up in time after the task is completed. Save the progressBlock and completedBlock; Commit the operation to the downloadQueue.

The final operation, url, request, downloadOperationCancelToken are packaged together into SDWebImageDownloadToken, download method.

###CreateDownloaderOperation

With the download over, let’s talk about how operation is created. The first is to generate URLRequest:

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
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(self.HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(self.HTTPHeadersLock);
Copy the code

Mainly through SDWebImageDownloaderOptions gain parameters configuration, the timeout is by the downloader config. DownloadTimeout decision, defaults to 15 s. Then from the imageContext id < SDWebImageDownloaderRequestModifier > requestModifier to modify the request.

// Request Modifier
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
    requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
    requestModifier = self.requestModifier;
}
Copy the code

It is important to note that requestModifier is acquired with a priority, and imageContext has a higher priority than downloader. Through this method, the interface caller can be controlled and the global configuration can be supported, which is suitable for all ages. By the same token, the id < SDWebImageDownloaderResponseModifier > responseModifier, id < SDWebImageDownloaderDecryptor > the decryptor.

The confirmed responseModifier and decryptor are then stored in imageContext again for later use.

Finally, fetch the operationClass from downloaderConfig to create the operation:

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

Set its Credential, minimumProgressInterval, queuePriority, and pendingOperation.

By default, each task is added to the downloadQueue in FIFO order. If the user sets LIFO, the priority of the existing task in the queue will be changed before being added to the queue:

if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder for (NSOperation *pendingOperation in self.downloadQueue.operations) { [pendingOperation addDependency:operation]; }}Copy the code

By traversing the queue, the new task is changed to the dependency of all tasks in the current queue to reverse the priority.

The data processing

SDWebImageDownloaderOperation is after the agreement of the type, the agreement itself following NSURLSessionTaskDelegate, NSURLSessionDataDelegate, it’s really handle URL request data classes, Support background download, support to responseData modification (by responseModifier), support to download ImageData decryption (by decryptor). The main internal properties are as follows:

@property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
@property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

@property (strong, nonatomic, nullable) NSMutableData *imageData;
@property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
@property (assign, nonatomic) NSUInteger expectedSize; // may be 0
@property (assign, nonatomic) NSUInteger receivedSize;

@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier; // modifiy original URLResponse
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor; // decrypt image data
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;

@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding
#if SD_UIKIT
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context;
Copy the code

There is nothing special about the initialization. Note that the Nullable Session passed in here is saved in unownedSessin, as opposed to the ownedSession generated internally by default. If session is empty at initialization, ownedSession will be created at start.

So the problem is, because we need to look at the various states of the session, we need to set the delegate to do that,

[NSURLSession sessionWithConfiguration:delegate:delegateQueue:];
Copy the code

The ownedSession delegate is within the operation, and the delegate that initializes the incoming session is the downloader. It performs uniform processing and forwarding of callbacks by extracting the corresponding implementation of the operation call from taskID, for example:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { // Identify the operation that runs this task and pass it the delegate method NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task]; if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) { [dataOperation URLSession:session task:task didCompleteWithError:error]; }}Copy the code

The download task is then started as a true consumer operation, and notifications are sent throughout the download process including start, end, and cancel.

  1. When didReceiveResponse, will save the response. ExpectedContentLength as expectedSize. Then call modifiedResponseWithResponse: save the edited reponse.

  2. Each didReceiveData appends data to imageData: [the self imageData appendData: data], update receivedSizeself. ReceivedSize = self. ImageData. Length. Finally, when receivedSize > expectedSize determines that the download is complete, perform subsequent processing. If you support the SDWebImageDownloaderProgressiveLoad, when receiving data, will enter the download while decoding coderQueue:

// progressive decode the image in coder queue dispatch_async(self.coderQueue, ^{ @autoreleasepool { UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); if (image) { // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; }}});Copy the code

Otherwise, will be done when didCompleteWithError decoding operation: SDImageLoaderDecodeImageData, but need to decrypt before decoding:

if (imageData && self.decryptor) {
    imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}
Copy the code

3. Handle the complete callback;

We’ll talk about the logic of decode at the end.

ImageCache

The Cache class is designed in the same way as ImageLoader. There is a copy of SDImageCacheConfig to configure Cache expiration time, size, read/write permissions, and dynamically extensible MemoryCache/DiskCache.

The main attributes of SDImageCacheConfig are as follows:

@property (assign, nonatomic) BOOL shouldDisableiCloud;
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
@property (assign, nonatomic) NSTimeInterval maxDiskAge;
@property (assign, nonatomic) NSUInteger maxDiskSize;
@property (assign, nonatomic) NSUInteger maxMemoryCost;
@property (assign, nonatomic) NSUInteger maxMemoryCount;
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
/// Defaults to built-in `SDMemoryCache` class.
@property (assign, nonatomic, nonnull) Class memoryCacheClass;
/// Defaults to built-in `SDDiskCache` class.
@property (assign ,nonatomic, nonnull) Class diskCacheClass;
Copy the code

SDImageCacheConfig is required for MemoryCache and DiskCache instantiation:

/// SDMemoryCache
- (nonnull instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
/// SDDiskCache
- (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config;
Copy the code

MemoryCache Protocl operates on the ID type (restricted by the NSCache API), while DiskCache operates on NSData.

Let’s take a look at their default implementation.

SDMemoryCache

/**
 A memory cache which auto purge the cache on memory warning and support weak cache.
 */
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>

@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;

@end
Copy the code

Internally, NSCache is extended to SDMemoryCache protocol, and *NSMapTable

weakCache is added, and semaphore lock is added to it to ensure thread safety. Weak-cache is a feature added only on iOS/tvOS because NSCache does not clear the cache on macOS despite system memory warnings. WeakCache uses a strong-weak reference that has no extra memory overhead and does not affect the life cycle of the object.
,>

WeakCache is used to restore the cache. It is controlled by the CacheConfig shouldUseWeakMemoryCache switch. For details, see Cacheconfig.h. Here’s how it works:

- (id)objectForKey:(id)key { id obj = [super objectForKey:key]; if (! self.config.shouldUseWeakMemoryCache) { return obj; } if (key && ! obj) { // Check weak cache SD_LOCK(self.weakCacheLock); obj = [self.weakCache objectForKey:key]; SD_UNLOCK(self.weakCacheLock); if (obj) { // Sync cache NSUInteger cost = 0; if ([obj isKindOfClass:[UIImage class]]) { cost = [(UIImage *)obj sd_memoryCost]; } [super setObject:obj forKey:key cost:cost]; } } return obj; }Copy the code

Because NSCache follows the NSDiscardableContent policy to store temporary objects, cache objects may be cleared by the system when memory is tight. In this case, if an application fails to access the MemoryCache, the diskCache query operation will be performed, which may cause image flickering. When shouldUseWeakMemoryCache is enabled, because weakCache holds the weak reference of the object (in the case that the object is cleared by NSCache and not released), we can get the cache by weakCache. Plug it into the NSCache. This reduces disk I/O.

SDDiskCache

This is more simple, internal use NSFileManager manage image data read and write, call SDDiskCacheFileNameForKey will key MD5 treated as a fileName, stored in diskCachePath directory. Another is to clear expired caches:

  1. According to the SDImageCacheConfigExpireType sortNSDirectoryEnumerator *fileEnumerator, start filtering;
  2. CacheConfig. MaxDiskAage is used to compare the validity and save the expired URL to urlsToDelete.
  3. call[self.fileManager removeItemAtURL:fileURL error:nil];
  4. Delete the data in the disk cache based on the cacheConfig. MaxDiskSize until it is half of the maxDiskSize.

Another point is that SDDiskCache, like YYKVStorage, also supports adding extendData for UIImage to store additional information, such as image scaling, URL rich link, time and other data.

However, YYKVStorage itself is stored with extended_data field of the manifest table in the database. SDDiskCache takes a different approach. Use setXattr, getXattr, listXattr of system API

to save extendData. You can say it’s up again. By the way, it corresponds to the key is to use SDDiskCacheExtendedAttributeName.

SDImageCache

It is a protocol class that is responsible for scheduling SDMemoryCache and SDDiskCache. Its Properties are as follows:

@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
Copy the code

Note: memoryCache and diskCache instances are generated based on classes defined in CacheConfig. The default classes are SDMemoryCache and SDDiskCache.

Let’s look at the core approach:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
Copy the code
  1. Make sure the image and key exist;

  2. When shouldCacheImagesInMemory to YES, will call [self. MemoryCache setObject: image forKey: key cost: cost] to memoryCache write;

  3. The diskCache is written, and the operation logic is stored in the ioQueue and autoReleasepool.

    dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = ... / / to SDImageFormat to encode image access / / / data = [[SDImageCodersManager sharedManager] encodedDataWithImage: image format:format options:nil]; [self _storeImageDataToDisk:data forKey:key]; if (image) { // Check extended data id extendedObject = image.sd_extendedObject; / /... get extended data [self.diskCache setExtendedData:extendedData forKey:key]; } } // call completionBlock in main queue });Copy the code

Another important method is the Image Query, defined in the SDImageCache protocol:

- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
    if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
    if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
    if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass;
    
    return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}
Copy the code

It only do one thing, converts SDWebImageOptions SDImageCacheOptions, then call queryCacheOperationForKey: and its internal logic is as follows:

First, if the Query key exists, a Transformer is fetched from imageContext and converted to the Query key:

key = SDTransformedKeyForKey(key, transformerKey);
Copy the code

Try to get the image from the memory cache if it exists:

  1. Meet SDImageCacheDecodeFirstFrameOnly and follow SDAnimatedImage agreement, will remove the CGImage for conversion

    // Ensure static image
    Class animatedImageClass = image.class;
    if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
    #if SD_MAC
        image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
    #else
        image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
    #endif
    }
    Copy the code
  2. Meet SDImageCacheMatchAnimatedImageClass, will be forced to check the image type matches, otherwise the data to nil:

    // Check image class matching Class animatedImageClass = image.class; Class desiredImageClass = context[SDWebImageContextAnimatedImageClass]; if (desiredImageClass && ! [animatedImageClass isSubclassOfClass:desiredImageClass]) { image = nil; }Copy the code

When can get the image from the cache memory and for SDImageCacheQueryMemoryData, complete return directly, or to continue;

The diskCache starts to read data and determines whether the I/O operation is synchronized based on the read conditions.

// Check whether we need to synchronously query disk // 1. in-memory cache hit & memoryDataSync // 2. in-memory cache miss & diskDataSync BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) || (! image && options & SDImageCacheQueryDiskDataSync));Copy the code

The entire diskQuery is stored in queryDiskBlock wrapped in autoRelease:

void(^queryDiskBlock)(void) = ^{ if (operation.isCancelled) { // call doneBlock & return } @autoreleasepool { NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage; SDImageCacheType cacheType = SDImageCacheTypeNone; if (image) { // the image is from in-memory cache, but need image data diskImage = image; cacheType = SDImageCacheTypeMemory; } else if (diskData) { cacheType = SDImageCacheTypeDisk; // decode image data only if in-memory cache missed diskImage = [self diskImageForKey:key data:diskData options:options context:context]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = diskImage.sd_memoryCost; [self.memoryCache setObject:diskImage forKey:key cost:cost]; } } // call doneBlock if (doneBlock) { if (shouldQueryDiskSync) { doneBlock(diskImage, diskData, cacheType); } else { dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, cacheType); }); }}}}Copy the code

SD places a large number of temporary memory operations in the AutoReleasepool to ensure that memory is released in time.

Special emphasis on, if you execute code into this, there must be a disk read operation, therefore, if not to get imageData can improve query efficiency by SDImageCacheQueryMemoryData;

Finally, the transformation logic of SDTransformedKeyForKey is the transformerKey of SDImageTransformer splintered after the image key in sequence. Such as:

'image.png' |> flip(YES,NO) |> rotate(pi/4,YES) => 'image - SDImageFlippingTransformer (1, 0) - SDImageRotationTransformer (0.78539816339, 1). The PNG'Copy the code

SDImageManaer

SDImageManger acts as the scheduler of the entire library, the aggregator of all the above logic, and links the components in series. Go to View > Download > Decoder > Cache. The only core method it exposes is loadImage:

@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;
@property (strong, nonatomic, nullable) id<SDImageTransformer> transformer;
@property (nonatomic, strong, nullable) id<SDWebImageCacheKeyFilter> cacheKeyFilter;
@property (nonatomic, strong, nullable) id<SDWebImageCacheSerializer> cacheSerializer;
@property (nonatomic, strong, nullable) id<SDWebImageOptionsProcessor> optionsProcessor;

@property (nonatomic, class, nullable) id<SDImageCache> defaultImageCache;
@property (nonatomic, class, nullable) id<SDImageLoader> defaultImageLoader;

- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                 completed:(nonnull SDInternalCompletionBlock)completedBlock;
Copy the code

The cacheKeyFilter, cacheSerializer, and optionsProcessor apis are briefly covered here, and the rest are covered above.

SDWebImageCacheKeyFilter

By default, url.absoluteString is used as a cacheKey, and if fileter is set, the cacheKey is intercepted and modified through cacheKeyForURL:;

SDWebImageCacheSerializer

By default, ImageCache will cache downloadData directly, and when we use other image formats, such as WEBP, the disk will be stored in WEBP format. This creates a problem where we need to repeat the decoding operation every time we need to read the image from disk. CacheSerializer can directly convert downloadData to NSData cache in JPEG/PNG format to improve access efficiency.

SDWebImageOptionsProcessor

Parameters in SDWebImageOptions and SDWebImageContext to control the global. The following is an example:

SDWebImageManager.sharedManager.optionsProcessor = [SDWebImageOptionsProcessor optionsProcessorWithBlock:^SDWebImageOptionsResult * _Nullable(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context) { // Only do animation on `SDAnimatedImageView` if (!context[SDWebImageContextAnimatedImageClass]) { options |= SDWebImageDecodeFirstFrameOnly; } // Do not force decode for png url if ([url.lastPathComponent isEqualToString:@"png"]) { options |= SDWebImageAvoidDecodeImage; } // Always use  screen scale factor SDWebImageMutableContext *mutableContext = [NSDictionary dictionaryWithDictionary:context]; mutableContext[SDWebImageContextImageScaleFactor] = @(UIScreen.mainScreen.scale); context = [mutableContext copy]; return [[SDWebImageOptionsResult alloc] initWithOptions:options context:context]; }];Copy the code

LoadImage

The first parameter of the interface, the URL, is the connection core of the framework, but Nullable should be designed entirely for the convenience of callers. Internal by nil for URL and compatibility with NSString (forcing the conversion to NSURL) to ensure the subsequent flow, otherwise end the call. Once the download started, it was broken down into the following 6 methods:

  • callCacheProcessForOperation
  • callDownloadProcessForOperation
  • callStoreCacheProcessForOperation
  • callTransformProcessForOperation
  • callCompletionBlockForOperation
  • safelyRemoveOperationFromRunning

These are: cache query, download, store, transform, execute callback, and clean callback. You can see that each method is an operation against an operation, which will be ready in loadImage and then start caching the query.

SDWebImageCombinedOperation *operation = [SDWebImagCombinedOperation new]; operation.manager = self; /// 1 BOOL isFailedUrl = NO; if (url) { SD_LOCK(self.failedURLsLock); isFailedUrl = [self.failedURLs containsObject:url]; SD_UNLOCK(self.failedURLsLock); } if (url.absoluteString.length == 0 || (! (options & SDWebImageRetryFailed) && isFailedUrl)) { [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url]; return operation; } SD_LOCK(self.runningOperationsLock); [self.runningOperations addObject:operation]; SD_UNLOCK(self.runningOperationsLock); // 2. Preprocess the options and context arg to decide the final the result for manager SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];Copy the code

The loadImage method itself is not complicated. The core of the method is to generate the operation and then roll it into the cached query.

FailedURLs are checked to see if they contain the current URL after operation initialization:

  • If yes and options are SDWebImageRetryFailed, end the operation and return operation.
  • If the check passes, the operation is savedrunningOperationsIn the. And encapsulate options and imageContext into SDWebImageOptionsResult.

At the same time, a wave of imageContext will be updated. Transformer, cacheKeyFilter and cacheSerializer will be stored in imageContext as the global default Settings. The optionsProcessor is then called to provide the user with custom options to reprocess the imageContext. The requestModifer in ImageLoader has a similar priority logic, but the implementation is a little different. Finally, turn to the CacheProcess.

The loadImage process uses combineOperation, which combines cache and Loader operations to clear cache query and download tasks in one step. Its statement reads as follows:

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation> /// imageCache queryImageForKey: Operation@Property (strong, nonatomic, Nullable, readonly) id<SDWebImageOperation> cacheOperation; /// imageLoader requestImageWithURL: Operation @property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> loaderOperation; /// Cancel the current operation, including cache and loader process - (void)cancel; @endCopy the code

The cancel method checks the two types of opration and performs the cancel operation one by one.

####CallCacheProcessForOperation

Check the value of SDWebImageFromLoaderOnly to determine whether it is a direct download task.

If yes, go to the downloadProcess.

If no, create a query task using imageCache and save it to combineOperation’s cacheOperation:

operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
   if (!operation || operation.isCancelled) {
    	/// 1  
   }
  	/// 2
}];
Copy the code

There are two cases to handle with the results of cached queries:

  1. When the queue executes the task, if operaton is in the canceled state, the downloading task ends.
  2. Otherwise go to the downloadProcess.

####CallDownloadProcessForOperation

The implementation of download is complicated. First, it needs to decide whether to create a new download task, which is controlled by three variables:

BOOL shouldDownload = ! SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); shouldDownload &= (! cachedImage || options & SDWebImageRefreshCached); shouldDownload &= (! [self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); shouldDownload &= [self.imageLoader canRequestImageForURL:url];Copy the code
  • Check whether the options value is SDWebImageFromCacheOnly or SDWebImageRefreshCached
  • The agent determines whether to create a download task
  • ImageLoader controls whether the download task is supported
  1. If the shouldDownload is NO end to download and call callCompletionBlockForOperation safelyRemoveOperationFromRunning. If there is a cacheImage, it will be returned with the completionBlock.

  2. If shouldDownload is YES, create a new download task and save it in combineOperation’s loaderOperation. Before creating a new task, if the cacheImage is fetched and SDWebImageRefreshCached is YES, it is stored in imageContext (if not, imageContext is created).

  3. After the download is over, return to the callBack, which handles several cases first:

    • If operation is cancelled, the downloaded image and data are discarded, and the callCompletionBlock is finished.
    • Error caused by reqeUST being canceled. CallCompletionBlock download is complete.
    • If the request result still hits the NSURLCache cache after imageRefresh, the callCompletionBlock is not called;
    • Errro error, callCompletionBlockForOperation and add url to failedURLs;
    • If it is successfully retry, the URL will be removed from failedURLs and storeCacheProcess will be called.

    SafelyRemoveOperation is executed for finished;

####CallStoreCacheProcessForOperation

Remove storeCacheType, originalStoreCacheType, Transformer, and cacheSerializer from imageContext to determine whether converted image data and original data need to be stored, and wait until the cache storage is complete.

BOOL shouldTransformImage = downloadedImage && (! downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; BOOL shouldCacheOriginal = downloadedImage && finished; BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);Copy the code

If shouldCacheOriginal is NO, go straight to the transformProcess. Otherwise, check whether the storage type is raw data:

// normally use the store cache type, but if target image is transformed, use original store cache type instead
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
Copy the code

If cacheSerializer exists during storage, the data format of the cacheSerializer is converted and the self stroageImage:… is invoked. .

When the storage is finished, we move to the final step, the transformProcess.

####CallTransformProcessForOperation

Before the conversion starts, the system will routinely determine whether the conversion is required. If the value is false, the download of callCompletionBlock will end. The judgment is as follows:

id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer]; id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = originalImage && (! originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);Copy the code

If a transformation is required, it is entered into the global queue to begin processing:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    @autoreleasepool {
        UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
        if (transformedImage && finished) {
				/// 1
        } else {
				callCompletionBlock
        }
    }
});        
Copy the code

CacheData = [cacheSerializer cacheDataWithImage: originalData: imageURL:]; [self storageImage:…] Store pictures. CallCompletionBlock after the storage is complete.

conclusion

If you can see here, it’s very patient. I hope you can have a general understanding of THE WORK-flow of SD, as well as some details of processing and thinking. In SD 5.x, what I feel most is that its architectural design is worthy of reference.

  • How do you design a stable and extensible API that securely supports dynamically adding parameters?
  • How to design an architecture that is decoupled and dynamically pluggable?

Finally, this article is actually missing SDImageCoder, which will be left to the SDWebImage plugin and its extensions in the next article.