This article is the first study of excellent tripartite open source library source code, through the analysis of excellent source code, to help us improve their “internal power”.
Good wheels, like SDWebImage, which we’ve used many times in development, are very familiar. In addition to the use of its functions, his excellent design patterns and encapsulation ideas are also worth learning. As well as the previous several articles to share the multithreading, runloop, lock and other basic knowledge, there are varying degrees of use in these source code, is also a good consolidation of our learning the basic knowledge.
Here’s what SDWebImage is worth chewing over and over.
SDWebImage source code analysis
Introduction to the
As of this writing, SDWebImage has been updated to v5.9.4, which provides a cached image downloader and adds categories for common UI elements such as UIImageView and UIButton for developers’ convenience.
Here are some other features we use a lot:
- Asynchronous picture downloader
- Automatic cache (memory + disk), and automatic expiration processing cache
- Extensible picture encoder to support massive image formats, currently support JPEG,PNG,WebP… , dynamic picture support GIF/APNG/HEIC
- Make sure you don’t download the same URL multiple times
- Good support for both Objective-C and Swift
- A new framework for SDWebImageSwiftUI was built to support SwiftUI
- It can be integrated with other three-party libraries, such as YYCache, Lottie-ios, etc
Just a quick introduction and comparison. We as iOS engineers, even junior engineers, are familiar with SDWebImage’s capabilities.
Start with use – the first step in learning
- Objective-C
#import <SDWebImage/SDWebImage.h>
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
Copy the code
- Swift
import SDWebImage
imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
Copy the code
The call flow in sd_setImageWithURL, I recommend you to keep it in mind, you can draw a flow diagram by hand to help you remember. Of course, it is not necessary to memorize this process, because this core process is also the core working principle of SDWebImage. The following is a flow chart drawn by my own hand, ignoring my ugly character, I hope you can also draw by hand.
It can be seen that SDWebImageManger, SDWebImageDownloader and SDWebImageCache are the three core classes of SDWebImage. Through the class name, we can also know what functions they are responsible for. We will also focus on these three core classes to learn.
sd_setImageWithURL
The entry method sd_setImageWithURL is a UIKit class method, and setImageWithURL() of all View categories will eventually call this method:
- (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;
Copy the code
The implementation of this method is quite long and generally does the following things:
- Cancel the current operation based on the key
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
Copy the code
The trace code can see that cancle is executed from the SDOperationsDictionary by retrieving the operation from the key and removing the operation corresponding to this key from the SDOperationsDictionary. SDOperationsDictionary is an NSMapTable
>.
This operation, for example, when a UIImageView in a cell is reused, first unloads or caches the current imageView based on the key. 2. Bind the URL as a property to UIView
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Copy the code
- Based on the URL, the image is loaded through SDWebImageManager’s loadImageWithURL method
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
}
Copy the code
- Store the operation obtained in the previous step into the SDOperationsDictionary
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
Copy the code
In the above steps, the most important step is the process of loading images by loadImageWithURL method in the third step, which is also the focus of our study.
loadImageWithURL
LoadImageWithURL is a method in SDWebImageManager, which is a singleton that also initializes SDImageCache and SDWebImageDownloader during initialization. SDWebImageManager is used to schedule SDImageCache and SDWebImageDownloader for cache and download operations.
The semaphore implements the lock operation
@property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe
@property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe
Copy the code
As the comments make clear, two semaphores control thread safety for failedURLs and runningOperations, respectively. Concrete is easy to use, before I share multithreaded article also introduces the semaphore the concrete usage of the lock operation, the initial value of the semaphore thread can be used to control the maximum number of concurrent access, when set to 1, represents only allows a thread to access a resource at the same time, guarantee the thread synchronization.
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
_failedURLsLock = dispatch_semaphore_create(1);
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
Copy the code
Continued said loadImageWithURL, work in process, the following is an important step queryCacheOperationForKey (), in SDImageCache query whether there is a picture of cache.
queryCacheOperationForKey
To this step will be fine to taste, the author in the cache aspect of the design ideas.
Here you can start with NSCache to learn about caching, and I’ll give you a brief introduction. NSCache is a caching class provided by the system. Its basic operation is similar to that of NSMutableDictionary, but its operation is thread-safe and does not require the user to consider locking and releasing locks. When there is insufficient memory, will automatically release the stored objects, if we need to customize, need to monitor UIApplicationDidReceiveMemoryWarningNotification this memory warning notice then delete operation.
The SDImageCacheConfig class is a configuration class that stores some information about the cache policy. For example, the default maximum cache time is one week, and the default is to clear the cache according to the last modification time.
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week @implementation SDImageCacheConfig - (instancetype)init { if (self = [super init]) { _shouldDecompressImages = YES; _shouldDisableiCloud = YES; _shouldCacheImagesInMemory = YES; _shouldUseWeakMemoryCache = YES; _diskCacheReadingOptions = 0; _diskCacheWritingOptions = NSDataWritingAtomic; _maxCacheAge = kDefaultCacheMaxCacheAge; _maxCacheSize = 0; _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate; } return self; }Copy the code
Take a look at several enumerations of cache policy definitions in SDImageCache
Typedef NS_ENUM(NSInteger, SDImageCacheType) {// SDImageCacheTypeNone from the net, // SDImageCacheTypeDisk from the disk, // SDImageCacheTypeMemory};Copy the code
There are some attributes similar to NSCache, and then the storage and query method definition, the following is a simple paste two method definition, the full API can view the source code
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;
Copy the code
Moving on to the implementation details inside the SDImageCache class:
An internal SDMemoryCache class inherits from NSCache to perform subsequent memory caching operations
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
Copy the code
Now enter the initialization operation in SDImageCache, SDImageCache is completed when SDWebImageManager initialization, let’s take a look at the initialization of the specific operation.
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nonnull NSString *)directory { if ((self = [super init])) { NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; // Create IO serial queue _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); _config = [[SDImageCacheConfig alloc] init]; // Init the memory cache _memCache = [[SDMemoryCache alloc] initWithConfig:_config]; _memCache.name = fullNamespace; // Init the disk cache if (directory ! = nil) { _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace]; } else { NSString *path = [self makeDiskCachePath:ns]; _diskCachePath = path; } dispatch_sync(_ioQueue, ^{ self.fileManager = [NSFileManager new]; }); #if SD_UIKIT // Subscribe to app events [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles) name:UIApplicationWillTerminateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles) name:UIApplicationDidEnterBackgroundNotification object:nil]; #endif } return self; }Copy the code
We mainly did the following things:
- Creates a serial queue _ioQueue to perform IO operations, and initializes NSFileManager
- Construct the SDImageCacheConfig configuration object
- Set the name of the memory cache and the folder path of the disk cache
- Add notifications that the application is about to terminate and go into the background
With caching in place, the actual process of storing and finding begins:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock { if (! key) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; BOOL shouldQueryMemoryOnly = (image && ! (options & SDImageCacheQueryDataWhenInMemory)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil, SDImageCacheTypeMemory); } return nil; } NSOperation *operation = [NSOperation new]; void(^queryDiskBlock)(void) = ^{ if (operation.isCancelled) { // do not call the completion if cancelled return; } @autoreleasepool { NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage; SDImageCacheType cacheType = SDImageCacheTypeNone; if (image) { // the image is from in-memory cache 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]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = diskImage.sd_memoryCost; [self.memCache setObject:diskImage forKey:key cost:cost]; } } if (doneBlock) { if (options & SDImageCacheQueryDiskSync) { doneBlock(diskImage, diskData, cacheType); } else { dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, cacheType); }); }}}}; if (options & SDImageCacheQueryDiskSync) { queryDiskBlock(); } else { dispatch_async(self.ioQueue, queryDiskBlock); } return operation; }Copy the code
The assembly line operation here can be roughly divided into the following steps:
- Check the memory first and determine whether to continue searching for disks based on the cache policy. If no, return to the cache
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
Copy the code
- Query the disk cache, which may cause memory spikes, so @Autoreleasepool is used and executed asynchronously in ioQueue
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { NSString *defaultPath = [self defaultCachePathForKey:key]; NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } // fallback because of https://github.com/SDWebImage/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } NSArray<NSString *> *customPaths = [self.customPaths copy]; for (NSString *path in customPaths) { NSString *filePath = [self cachePathForKey:key inPath:path]; NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; if (imageData) { return imageData; } // fallback because of https://github.com/SDWebImage/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; if (imageData) { return imageData; } } return nil; }Copy the code
Disk search After the default path is not found, the system removes the suffix and searches again. If no result is found, the system determines whether the user has added a custom path and searches again in the custom path.
Another clever use of threads is to create an NSOperation, but instead of using it for complex multithreading, it acts more like a flag bit, using its cancel method and isCancelled property to cancel disk queries.
This concludes the process of storing and searching, and one more important point is the mechanism for removing the cache. This is what the system notification listener does in the first initialization operation, where the data previously configured in config comes into play.
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; // Compute content date key to be used for tests NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey; switch (self.config.diskCacheExpireType) { case SDImageCacheConfigExpireTypeAccessDate: cacheContentDateKey = NSURLContentAccessDateKey; break; case SDImageCacheConfigExpireTypeModificationDate: cacheContentDateKey = NSURLContentModificationDateKey; break; default: break; } NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey]; // This enumerator prefetches useful properties for our cache files. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge]; NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0; // Enumerate all of the files in the cache directory. This loop has two purposes: // // 1. Removing files that are older than the expiration date. // 2. Storing file attributes for the size-based cleanup pass. NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSError *error; NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error]; // Skip directories and errors. if (error || ! resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } // Remove files that are older than the expiration date; NSDate *modifiedDate = resourceValues[cacheContentDateKey]; if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } // Store a reference to this file and account for its total size. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += totalAllocatedSize.unsignedIntegerValue; cacheFiles[fileURL] = resourceValues; } for (NSURL *fileURL in urlsToDelete) { [self.fileManager removeItemAtURL:fileURL error:nil]; } // If our remaining disk cache exceeds a configured maximum size, perform a second // size-based cleanup pass. We delete the oldest files first. if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) { // Target half of our maximum cache size for this cleanup pass. const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2; // Sort the remaining cache files by their last modification time or last access time (oldest first). NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]]; }]; // Delete files until we fall below our desired cache size. for (NSURL *fileURL in sortedFiles) { if ([self.fileManager removeItemAtURL:fileURL error:nil]) { NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; if (currentCacheSize < desiredCacheSize) { break; } } } } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); }}); }Copy the code
A few points to note here are:
- A file iterator, fileEnumerator, is required to retrieve the image file to be deleted
- Based on the maximum time limit (one week by default) set in the policy to clear the cache, calculate the time range in which the file (created or last modified) needs to be cleared
- Iterate through the images to be deleted based on this time range and then delete them from the disk
- Further clear the cache based on the maximum cache size configured in the cache policy
- Continue to drop caches in the order they were created until the cache size is half the maximum
- If the disk search succeeds, the cache policy determines whether to write to the cache
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
Copy the code
- The last execution is completed
doneBlock
The callback, of course, there will be something missing here, will go back to the SDWebImageManager and go through the download process
if (doneBlock) { if (options & SDImageCacheQueryDiskSync) { doneBlock(diskImage, diskData, cacheType); } else { dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, cacheType); }); }}Copy the code
downloadImageWithURL
As for downloading pictures, the old version of SDWebImage is based on NSURLConnection, while the later new version is based on NSURLSession. This is caused by apple’s own history, so there is no more explanation here. Later we mainly with the new version of the code to analyze.
The downloadImageWithURL method returns a token of type SDWebImageDownloadToken, so that the download can be cancelled in time for the cancelled callback.
@implementation SDWebImageCombinedOperation
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.downloadToken) {
[self.manager.imageDownloader cancel:self.downloadToken];
}
[self.manager safelyRemoveOperationFromRunning:self];
}
}
@end
Copy the code
The actual download in the downloadImageWithURL method is this code:
if (! operation || operation.isFinished || operation.isCancelled) { operation = [self createDownloaderOperationWithUrl:url options:options]; __weak typeof(self) wself = self; operation.completionBlock = ^{ __strong typeof(wself) sself = wself; if (! sself) { return; } LOCK(sself.operationsLock); [sself.URLOperations removeObjectForKey:url]; UNLOCK(sself.operationsLock); }; [self.URLOperations setObject:operation forKey:url]; // Add operation to operation queue only after all configuration done according to Apple's doc. // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. [self.downloadQueue addOperation:operation]; }Copy the code
You see here might be a bit confused, the if condition statement, did not see the network request related code, just in createDownloaderOperationWithUrl approach, to create a request, finally returned to a operation, isn’t it, So let’s go ahead and look at this operation, why we need an operation here.
SDWebImageDownloaderOperation
The above said the operation is a SDWebImageDownloaderOperation instance, this when initializing sessionConfiguration set up.
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration { if ((self = [super init])) { _operationClass = [SDWebImageDownloaderOperation class]; _shouldDecompressImages = YES; _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; _downloadQueue = [NSOperationQueue new]; _downloadQueue.maxConcurrentOperationCount = 6; _downloadQueue.name = @"com.hackemist.SDWebImageDownloader"; _URLOperations = [NSMutableDictionary new]; SDHTTPHeadersMutableDictionary *headerDictionary = [SDHTTPHeadersMutableDictionary dictionary]; . [self createNewSessionWithConfiguration:sessionConfiguration]; } return self; }Copy the code
SDWebImageDownloaderOperation inherited from NSOperation and rewrite the NSOperation start () method, here is the real network request the place to start. Recall that the start() method of NSOperation is called by addOperation: to an NSOperationQueue, and the confusion is clear. In downloadImageWithURL [self downloadQueue addOperation: operation]. This code is the key to triggering the network request.
- (void)start { @synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } #if SD_UIKIT 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:^{ [wself cancel]; }]; } #endif NSURLSession *session = self.unownedSession; if (! session) { NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest = 15; /** * Create the session for this task * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate * method calls and completion handler calls. */ session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; self.ownedSession = session; } if (self.options & SDWebImageDownloaderIgnoreCachedResponse) { // Grab the cached data for later check NSURLCache *URLCache = session.configuration.URLCache; if (! URLCache) { URLCache = [NSURLCache sharedURLCache]; } NSCachedURLResponse *cachedResponse; // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483 @synchronized (URLCache) { cachedResponse = [URLCache cachedResponseForRequest:self.request]; } if (cachedResponse) { self.cachedData = cachedResponse.data; } } self.dataTask = [session dataTaskWithRequest:self.request]; self.executing = YES; } if (self.dataTask) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" if ([self.dataTask respondsToSelector:@selector(setPriority:)]) { if (self.options & SDWebImageDownloaderHighPriority) { self.dataTask.priority = NSURLSessionTaskPriorityHigh; } else if (self.options & SDWebImageDownloaderLowPriority) { self.dataTask.priority = NSURLSessionTaskPriorityLow; } } #pragma clang diagnostic pop [self.dataTask resume]; for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { progressBlock(0, NSURLResponseUnknownLength, self.request.URL); } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf]; }); } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]]; [self done]; }}Copy the code
Here, download the main process on the finish, of course, there are many details in the source code worth us to read in detail, such as the download order, can be set to FIFO can also be LIFO, such as the download priority and so on, but this article has been too long, will not expand to say.
callCompletionBlockForOperation
The final step is to get the result, which may be the cached image data from the query cache, or the downloaded image data from the network, and then go back to the main thread to set the image for imageView.image.
dispatch_main_async_safe(^{
if (operation && !operation.isCancelled && completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
Copy the code
Conclusion:
For SDWebImage source code learning, let’s stop today. Hope that through the sharing of this article, can arouse your interest in source code learning, as well as my own a little way of learning the source code, first to be familiar with the main process, and then one by one process, to explore the depth.
In the process of learning, you can also see the author’s clever use of multi-threading, locking, caching and other functions, can consolidate your understanding of the basic knowledge.
If you still don’t understand the points I shared, please leave a comment.
If you are interested in the points I haven’t written, you can also study and discuss them together.