SDWebImage通过对优秀源码的分析

本文是对优秀三方开源库源码学习的第一篇,通过对优秀源码的分析,帮助我们提升自己的“内功”。

优秀的*,比如SDWebImage我们开发中使用了多次,再熟悉不过了。除了对其功能的使用之外,他优秀的设计模式和封装思想也值得我们学习。以及前面几篇文章中分享的多线程,runloop,锁等基础知识,在这些源码中也有不同程度的使用,也是对我们学习基础知识的一个很好的巩固。

下面就开始说一说SDWebImage中值得我们反复咀嚼学习的东西。

SDWebImage源码分析

简介

截止到我写这篇文章的时间,SDWebImage已经更新到v5.9.4版本,该库提供了具有缓存功能的图片下载器,并为了方便开发者使用,为常用的UI元素比如UIImageView,UIButton等添加了category。

还有下面几个我们经常用到的功能:

  • 异步图片下载器
  • 自动缓存(内存 + 磁盘),以及过期自动处理缓存
  • 可扩展的图片编码器,以支持海量的图片格式,目前支持JPEG,PNG,WebP...,动态图片支持GIF/APNG/HEIC
  • 保证不会多次下载相同的URL
  • 对Objective-C和Swift都有很好的支持
  • 构建了一个SDWebImageSwiftUI的全新框架来支持SwiftUI
  • 可以与其他三方库进行集成使用,比如YYCache、Lottie-iOS等

简介就简单说几句,比较我们作为一个iOS工程师,哪怕是初级工程师也都对SDWebImage的功能很熟悉了。

从使用开始-学习的第一步

  • Objective-C
#import <SDWebImage/SDWebImage.h>

[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
             placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
复制代码
  • Swift
import SDWebImage

imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
复制代码

sd_setImageWithURL中的调用流程,我建议大家谨记于心,可以手动画一画流程图,帮助记忆。当然不是说这里非要硬背这个流程,因为这个核心的流程也是SDWebImage的核心工作原理。下面是一幅我自己手画的流程图,忽略我这个丑丑的字,希望你也可以动手画一画。 SDWebImage通过对优秀源码的分析

可以看到SDWebImageManger、SDWebImageDownloader和SDWebImageCache是SDWebImage的三个核心类,通过类名也知道他们是各自负责什么功能的,下面我们也是主要围绕这三个核心类来学习。

sd_setImageWithURL

入口方法sd_setImageWithURL是UIKit分类中的方法,所有 View Category 的 setImageWithURL() 最终都会调用到下面这个方法:

- (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;
复制代码

这个方法的实现挺长,总体来说就做了下面几件事:

  1. 根据key取消当前的操作
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
复制代码

跟踪代码可以看到是从SDOperationsDictionary中根据key获取到operation执行cancle,并从SDOperationsDictionaryremove这个key对应的operationSDOperationsDictionary是一个NSMapTable<NSString *, id<SDWebImageOperation>>类型的NSMapTable

这个操作,针对于比如cell中的UIImageView被复用的时候,首先需要根据key取消当前imageView上的下载或者缓存操作。 2. 将url作为属性绑定到UIView上

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
复制代码
  1. 根据URL,通过SDWebImageManager的loadImageWithURL方法加载图片
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
	...
}
复制代码
  1. 将上一步得到的operation,存入SDOperationsDictionary中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
复制代码

上面的步骤中最主要的是第三步中的是loadImageWithURL方法加载图片的过程,也是我们要重点学习的地方。

loadImageWithURL

loadImageWithURLSDWebImageManager中的方法,SDWebImageManager是一个单例,在初始化的时候,还初始化了SDImageCacheSDWebImageDownloader。SDWebImageManager的作用就是调度SDImageCache和SDWebImageDownloader进行缓存和下载操作的。

信号量实现锁操作

@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
复制代码

这里注释也说的很明白了,两个信号量分别控制操作failedURLsrunningOperations的线程安全。具体使用也比较简单,在我之前分享的多线程的文章中也介绍了信号量实现锁操作的具体用法,信号量的初始值可以用来控制线程并发访问的最大数量,当设置为1的时候,则代表同时只允许一条线程访问资源,保证了线程同步。

#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);
    }
复制代码

继续往下说loadImageWithURL中的工作流程,下面一个重要的步骤就是queryCacheOperationForKey(),在SDImageCache里查询是否存在缓存的图片.

queryCacheOperationForKey

到这一步骤就要细细的去品味一下,作者在缓存方面的设计思路了。

这里可以先从NSCache开始去学习缓存方面的知识,这里我先对其做一个简单的介绍。NSCache是系统提供的一个做缓存的类,他的基本操作类似于NSMutableDictionary,但是他的操作是线程安全的,不需要使用者去考虑加锁和释放锁。当内存不足时,会自动释放存储对象,如果我们需要自定义的时候,需要监听 UIApplicationDidReceiveMemoryWarningNotification这个内存警告的通知然后再做删除的操作。

下面先来看看 SDImageCacheConfig类,可以看得出来就是一个配置类,保存一些缓存策略的信息,比如默认的最长缓存时间是一周,默认是根据上次的修改时间去清除缓存。

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;
}
复制代码

继续看一下SDImageCache中关于缓存策略定义的几个枚举

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    //从网上下载的
    SDImageCacheTypeNone,
    //从磁盘中获取的
    SDImageCacheTypeDisk,
    //从内存中获取的
    SDImageCacheTypeMemory
};
复制代码

还有一些和NSCache差不多的属性,再就是存储和查询的方法定义了,下面就简单贴两个方法的定义,全量API可以查看源码

- (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;
复制代码

接下来继续看SDImageCache类内部的实现细节:

内部定义了一个SDMemoryCache类继承自NSCache,做后续的内存缓存操作

@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>

复制代码

下面就进入SDImageCache中的初始化操作,SDImageCache是在SDWebImageManager初始化的时候就完成了初始化,咱们看看初始化具体都做了什么操作。

- (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;
}
复制代码

主要是做了这样几件事:

  1. 创建了一个执行IO操作的串行队列_ioQueue,以及NSFileManager的初始化
  2. 构造SDImageCacheConfig配置对象
  3. 设置内存缓存的name 和 磁盘缓存的文件夹路径
  4. 添加应用即将终止和进入后台的通知

为缓存做了充足的准备,下面就开始真正存储和查找的流程了:

- (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;
}

复制代码

这里的流水线操作大致分为下面几步:

  1. 先查内存,根据缓存策略判断是否还要继续查找磁盘,如果不需要直接返回
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}
复制代码
  1. 再查询磁盘缓存,这里可能会造成内存峰值,所以使用了@autoreleasepool,并在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;
}

复制代码

磁盘查找在默认路径没查找到之后,还进行了去后缀再查找的操作,如果还是没有结果,再判断用户是否添加了自定义路径,在自定义路径中再继续查找。

这里还有一个对线程比较巧妙的使用,创建了一个NSOperation,但并没有使用它进行复杂的多线程操作,而是更像是一个标志位,使用它的cancel方法和isCancelled属性,来取消磁盘查询。

到这里,存储和查找的流程就说完了,还有一个比较重要的点是删除缓存的机制。就是第一步初始化操作中,对系统通知监听做出的操作,之前在config中配置的数据到这里也就发挥了作用。

- (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();
            });
        }
    });
}
复制代码

这里我们需要注意的几个点是:

  • 需要一个文件的迭代器fileEnumerator去获取需要删除的图片文件
  • 根据前面设置的清除缓存的策略中最大时间限制(默认一周),计算出文件(创建或上一次修改)需要被清除的时间范围
  • 根据这个时间范围遍历出需要删除的图片,然后从磁盘中删除
  • 再根据缓存策略中配置的最大缓存大小,判断时候需要进一步清除缓存
  • 按创建的先后顺序继续删除缓存,直到缓存大小是最大值的一半
  1. 磁盘查找成功就根据缓存策略判断是否要写入缓存
if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
复制代码
  1. 最后执行完成的doneBlock回调,当然这里会有没查到的情况存在,就会回到SDWebImageManager中进入后面下载的流程
if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
复制代码

downloadImageWithURL

对于下载图片部分,老版本的SDWebImage是基于NSURLConnection的,后面的新版本都是基于NSURLSession,这个是苹果自己的发展史导致的,这里就不多做解释了。后面我们主要以新版本的代码来分析。

downloadImageWithURL方法返回的是一个SDWebImageDownloadToken类型的token,这么做的目的是,可以在取消的回调中及时取消下载操作。

@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
复制代码

downloadImageWithURL方法中真正执行下载操作的是下面这段代码:

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];
    }
复制代码

看到这里你可能有些迷糊,这个if条件语句中,并没有看到网络请求相关的代码呀,只是在createDownloaderOperationWithUrl方法中,创建了一个request,最后返回了一个operation,是不是很迷,那就继续去看看这个operation,这里为什么需要一个operation呢。

SDWebImageDownloaderOperation

上面说到的operation其实是一个SDWebImageDownloaderOperation的实例,这个在初始化sessionConfiguration的时候就设置了。

- (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;
}
复制代码

SDWebImageDownloaderOperation继承自NSOperation,并且重写了NSOperation的start()方法,这里才是网络请求真正开始的地方。这里可以回想一下,NSOperation的start()方法的调用机制,是在被addOperation:到一个NSOperationQueue的时候,上面迷糊的地方是不是就清晰了,downloadImageWithURL[self.downloadQueue addOperation:operation];这句代码就是触发网络请求的关键所在。

- (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];
    }
}

复制代码

到这里,下载的主要流程就说完,当然源码中还有很多细节值得我们去详细的阅读,比如下载的顺序,可以设置为FIFO也可以是LIFO,再比如下载的优先级等等,但是这篇文章篇幅已经太长了,就不展开说了。

callCompletionBlockForOperation

最后一个步骤,就是拿到结果,这个结果可能是查询缓存查到的缓存图片数据,也可能是网络下载的图片数据,最后都要回到主线程去给imageView.image设置图片。

dispatch_main_async_safe(^{
        if (operation && !operation.isCancelled && completionBlock) {
            completionBlock(image, data, error, cacheType, finished, url);
        }
    });
复制代码

总结:

对于SDWebImage的源码学习,今天先告一段落。希望通过本文的分享,可以引起你对源码学习的兴趣,以及我自己对源码学习的一点儿方式方法,首先要熟悉主流程,然后再逐个流程,往深了去探究。

在学习的过程中,你也可以看到作者对多线程,锁,缓存等等功能的妙用,是不是可以更加巩固你对基础知识的理解。

上一篇:解决Web部署 svg/woff/woff2字体 404错误 iis 解决Web部署 svg/woff/woff2字体 404错误


下一篇:黄聪:解决Web部署 svg/woff/woff2字体 404错误