美文网首页
IOS框架:SDWeblmage(下)

IOS框架:SDWeblmage(下)

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-26 06:24 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、简介
      • 1、设计目的
      • 2、特性
      • 3、常见问题
      • 4、使用方法
    • 二、实现原理
      • 1、架构图(UML 类图)
      • 2、流程图(方法调用顺序图)
      • 3、目录结构
      • 4、核心逻辑
    • 三、反思和拓展
    • 四、实现细节
      • 1、SDWebImageDownloader
      • 2、SDWebImageDownloaderOperation
      • 3、SDImageCache
      • 4、SDWebImageManager
      • 5、UIImageView+WebCache
    • Demo
    • 参考文献

    IOS框架:SDWeblmage(上)

    四、实现细节

    SDWebImage 最核心的功能
    • 下载(SDWebImageDownloader
    • 缓存(SDImageCache
    • 将缓存和下载的功能组合起来(SDWebImageManager
    • 封装成 UIImageView 等类的分类方法(UIImageView+WebCache 等)

    1、SDWebImageDownloader

    SDWebImageDownloader 继承于 NSObject,主要承担了异步下载图片和优化图片加载的任务。

    a、问题
    • 如何实现异步下载,也就是多张图片同时下载?
    • 如何处理同一张图片(同一个 URL)多次下载的情况?
    b、源码

    枚举定义

    // 下载选项
    typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
        SDWebImageDownloaderLowPriority = 1 << 0,
        SDWebImageDownloaderProgressiveDownload = 1 << 1,
        SDWebImageDownloaderUseNSURLCache = 1 << 2,
        SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
        SDWebImageDownloaderContinueInBackground = 1 << 4,
        SDWebImageDownloaderHandleCookies = 1 << 5,
        SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
       SDWebImageDownloaderHighPriority = 1 << 7,
    };
    
    // 下载任务执行顺序
    typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
        SDWebImageDownloaderFIFOExecutionOrder, // 先进先出
        SDWebImageDownloaderLIFOExecutionOrder  // 后进先出
    };
    

    SDWebImageDownloader.h 文件中的属性

    @property (assign, nonatomic) BOOL shouldDecompressImages;  // 下载完成后是否需要解压缩图片,默认为 YES
    @property (assign, nonatomic) NSInteger maxConcurrentDownloads;
    @property (readonly, nonatomic) NSUInteger currentDownloadCount;
    @property (assign, nonatomic) NSTimeInterval downloadTimeout;
    @property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
    
    @property (strong, nonatomic) NSString *username;
    @property (strong, nonatomic) NSString *password;
    @property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
    

    SDWebImageDownloader.m 文件中的属性

    @property (strong, nonatomic) NSOperationQueue *downloadQueue; // 图片下载任务是放在这个 NSOperationQueue 任务队列中来管理的
    @property (weak, nonatomic) NSOperation *lastAddedOperation;
    @property (assign, nonatomic) Class operationClass;
    @property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
    @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
    @property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // 图片下载的回调 block 都是存储在这个属性中,该属性是一个字典,key 是图片的 URL,value 是一个数组,包含每个图片的多组回调信息。 
    

    SDWebImageDownloader.h 文件中方法

    + (SDWebImageDownloader *)sharedDownloader;
    
    - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
    - (NSString *)valueForHTTPHeaderField:(NSString *)field;
    
    - (void)setOperationClass:(Class)operationClass; // 创建 operation  
    
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageDownloaderOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
                                           
    - (void)setSuspended:(BOOL)suspended;
    

    SDWebImageDownloader.m文件中的方法

    // Lifecycle
    + (void)initialize;
    + (SDWebImageDownloader *)sharedDownloader;
    - init;
    - (void)dealloc;
    
    // Setter and getter
    - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
    - (NSString *)valueForHTTPHeaderField:(NSString *)field;
    - (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads;
    - (NSUInteger)currentDownloadCount;
    - (NSInteger)maxConcurrentDownloads;
    - (void)setOperationClass:(Class)operationClass;
    
    // Download
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageDownloaderOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
    - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
              andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                         forURL:(NSURL *)url
                 createCallback:(SDWebImageNoParamsBlock)createCallback;
    
    // Download queue            
    - (void)setSuspended:(BOOL)suspended;
    
    c、实现思路

    先看看 +initialize方法,这个方法中主要是通过注册通知让SDNetworkActivityIndicator 监听下载事件,来显示和隐藏状态栏上的 network activity indicator。为了让 SDNetworkActivityIndicator 文件可以不用导入项目中来(如果不要的话),这里使用了 runtime 的方式来实现动态创建类以及调用方法。

    + (void)initialize {
        if (NSClassFromString(@"SDNetworkActivityIndicator")) {
            id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
    
            // Remove observer in case it was previously added.
            // 先移除通知观察者 SDNetworkActivityIndicator
            [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
            [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];
    
            // 再添加通知观察者 SDNetworkActivityIndicator
            [[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                                     selector:NSSelectorFromString(@"startActivity")
                                                         name:SDWebImageDownloadStartNotification object:nil];
            [[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                                     selector:NSSelectorFromString(@"stopActivity")
                                                         name:SDWebImageDownloadStopNotification object:nil];
        }
    }
    

    +sharedDownloader方法中调用了 -init方法来创建一个单例。

    + (SDWebImageDownloader *)sharedDownloader {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }
    

    -init方法中做了一些初始化设置和默认值设置,包括设置最大并发数(6)、下载超时时长(15s)等。

    - (id)init {
        if ((self = [super init])) {
            _operationClass = [SDWebImageDownloaderOperation class];
            _shouldDecompressImages = YES;
            // 设置下载 operation 的默认执行顺序(先进先出还是先进后出)
            _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
            // 初始化 _downloadQueue(下载队列)
            _downloadQueue = [NSOperationQueue new];
            // 初始化 _barrierQueue(GCD 队列)最大并发数(6)
            _downloadQueue.maxConcurrentOperationCount = 6;
            // 初始化 _URLCallbacks(下载回调 block 的容器)
            _URLCallbacks = [NSMutableDictionary new];
            // 设置 _HTTPHeaders 默认值
            _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
            _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
            // 设置默认下载超时时长 15s 
            _downloadTimeout = 15.0;
        }
        return self;
    }
    

    这个类中最核心的方法就是 - downloadImageWithURL: options: progress: completed:方法,这个方法中首先通过调用-addProgressCallback: andCompletedBlock: forURL: createCallback:方法来保存每个url对应的回调block

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
        __block SDWebImageDownloaderOperation *operation;
        __weak __typeof(self)wself = self;
    
        [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ......
    

    -addProgressCallback: ...方法先进行错误检查,判断 URL是否为空,然后再将 URL 对应的 progressBlockcompletedBlock 保存到 URLCallbacks 属性中去。

    - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
        // 判断 url 是否为 nil,如果为 nil 则直接回调 completedBlock,返回失败的结果,然后 return
        // 因为 url 会作为存储 callbacks 的 key
        if (url == nil) {
            if (completedBlock != nil) {
                completedBlock(nil, nil, nil, NO);
            }
            return;
        }
    
        // MARK: 使用 dispatch_barrier_sync 函数来保证同一时间只有一个线程能对 URLCallbacks 进行操作
        dispatch_barrier_sync(self.barrierQueue, ^{
            // 从属性 URLCallbacks(一个字典) 中取出对应 url 的 callBacksForURL
            // 这是一个数组,因为可能一个 url 不止在一个地方下载
            BOOL first = NO;
            if (!self.URLCallbacks[url]) {
                // 如果没有取到,也就意味着这个 url 是第一次下载
                // 那就初始化一个 callBacksForURL 放到属性 URLCallbacks 中
                self.URLCallbacks[url] = [NSMutableArray new];
                first = YES;
            }
    
            // 处理同一个 URL 的多次下载请求
            NSMutableArray *callbacksForURL = self.URLCallbacks[url];
            // 往数组 callBacksForURL 中添加 包装有 callbacks(progressBlock 和 completedBlock)的字典
            NSMutableDictionary *callbacks = [NSMutableDictionary new];
            if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
            if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
            [callbacksForURL addObject:callbacks];
            // 更新 URLCallbacks 存储的对应 url 的 callBacksForURL
            self.URLCallbacks[url] = callbacksForURL;
    
            // 如果这个 url 是第一次请求下载,就回调 createCallback
            if (first) {
                createCallback();
            }
        });
    }
    

    URLCallbacks 属性是一个 NSMutableDictionary 对象,key 是图片的 URLvalue 是一个数组,包含每个图片的多组回调信息 。用 JSON 格式表示的话,就是下面这种形式:

    {
        "callbacksForUrl1": [
            {
                "kProgressCallbackKey": "progressCallback1_1",
                "kCompletedCallbackKey": "completedCallback1_1"
            },
            {
                "kProgressCallbackKey": "progressCallback1_2",
                "kCompletedCallbackKey": "completedCallback1_2"
            }
        ],
        "callbacksForUrl2": [
            {
                "kProgressCallbackKey": "progressCallback2_1",
                "kCompletedCallbackKey": "completedCallback2_1"
            },
            {
                "kProgressCallbackKey": "progressCallback2_2",
                "kCompletedCallbackKey": "completedCallback2_2"
            }
        ]
    }
    

    这里有个细节需要注意,因为可能同时下载多张图片,所以就可能出现多个线程同时访问 URLCallbacks 属性的情况。为了保证线程安全,所以这里使用了 dispatch_barrier_sync 来分步执行添加到 barrierQueue 中的任务,这样就能保证同一时间只有一个线程能对URLCallbacks 进行操作。

    如果这个URL是第一次被下载,就要回调 createCallbackcreateCallback 主要做的就是创建并开启下载任务,下面是 createCallback 的回调处理

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
        __block SDWebImageDownloaderOperation *operation;
        __weak __typeof(self)wself = self;
    
        // 1. 把入参 url、progressBlock 和 completedBlock 传进该方法,并在第一次下载该 URL 时回调 createCallback
        [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
            NSTimeInterval timeoutInterval = wself.downloadTimeout;
            if (timeoutInterval == 0.0) {
                timeoutInterval = 15.0;
            }
    
            // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
            // 1.1 创建下载 request ,设置 request 的 cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining
            // 以及 allHTTPHeaderFields(这个属性交由外面处理,设计的比较巧妙)
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
            request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
            request.HTTPShouldUsePipelining = YES;
            if (wself.headersFilter) {
                request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
            }
            else {
                request.allHTTPHeaderFields = wself.HTTPHeaders;
            }
            // 1.2 创建 SDWebImageDownloaderOperation(继承自 NSOperation)
            operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                // 1.2.1 SDWebImageDownloaderOperation 的 progressBlock 回调处理
                // 这个 block 有两个回调参数:接收到的数据大小和预计数据大小
                
                // 这里用了 weak-strong dance,首先使用 strongSelf 强引用 weakSelf,目的是为了保住 self 不被释放
                SDWebImageDownloader *sself = wself;
                // 然后检查 self 是否已经被释放(这里为什么先“保活”后“判空”呢?因为如果先判空的话,有可能判空后 self 就被释放了)
                if (!sself) return;
                // 取出 url 对应的回调 block 数组(这里取的时候有些讲究,考虑了多线程问题,而且取的是 copy 的内容)
                __block NSArray *callbacksForURL;
                dispatch_sync(sself.barrierQueue, ^{
                    callbacksForURL = [sself.URLCallbacks[url] copy];
                });
                // 遍历数组,从每个元素(字典)中取出 progressBlock 进行回调
                for (NSDictionary *callbacks in callbacksForURL) {
                    SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                    if (callback) callback(receivedSize, expectedSize);
                }
            } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                // 1.2.2 SDWebImageDownloaderOperation 的 completedBlock 回调处理
                // 这个 block 有四个回调参数:图片 UIImage,图片数据 NSData,错误 NSError,是否结束 isFinished
                
                // 同样,这里也用了 weak-strong dance
                SDWebImageDownloader *sself = wself;
                if (!sself) return;
                
                // 接着,取出 url 对应的回调 block 数组
                __block NSArray *callbacksForURL;
                dispatch_barrier_sync(sself.barrierQueue, ^{
                    callbacksForURL = [sself.URLCallbacks[url] copy];
                    // 如果结束了(isFinished),就移除 url 对应的回调 block 数组(移除的时候也要考虑多线程问题)
                    if (finished) {
                        [sself.URLCallbacks removeObjectForKey:url];
                    }
                });
                // 遍历数组,从每个元素(字典)中取出 completedBlock 进行回调
                for (NSDictionary *callbacks in callbacksForURL) {
                    SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                    if (callback) callback(image, data, error, finished);
                }
            } cancelled:^{
                // SDWebImageDownloaderOperation 的 cancelBlock 回调处理
                
                // 同样,这里也用了 weak-strong dance
                SDWebImageDownloader *sself = wself;
                if (!sself) return;
                
                // 然后移除 url 对应的所有回调 block
                dispatch_barrier_async(sself.barrierQueue, ^{
                    [sself.URLCallbacks removeObjectForKey:url];
                });
            }];
            // 1.3 设置下载完成后是否需要解压缩
            operation.shouldDecompressImages = wself.shouldDecompressImages;
            
            // 1.4 如果设置了 username 和 password,就给 operation 的下载请求设置一个 NSURLCredential
            if (wself.username && wself.password) {
                operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
            }
            
            // 1.5 设置 operation 的队列优先级
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            }
    
            // 1.6 将 operation 加入到队列 downloadQueue 中,队列(NSOperationQueue)会自动管理 operation 的执行
            [wself.downloadQueue addOperation:operation];
            // 1.7 如果 operation 执行顺序是先进后出,就设置 operation 依赖关系(先加入的依赖于后加入的),并记录最后一个 operation(lastAddedOperation)
            if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
                // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
                [wself.lastAddedOperation addDependency:operation];
                wself.lastAddedOperation = operation;
            }
        }];
    
        // 2. 返回 createCallback 中创建的 operation(SDWebImageDownloaderOperation)
        return operation;
    }
    

    createCallback 方法中调用了- [SDWebImageDownloaderOperation initWithRequest: options: progress:]方法来创建下载任务 SDWebImageDownloaderOperation。那么,这个 SDWebImageDownloaderOperation 类究竟是干什么的呢?下一节再看。

    d、知识点

    SDWebImageDownloaderOptions 枚举使用了位运算:通过“与”运算符,可以判断是否设置了某个枚举选项,因为每个枚举选择项中只有一位是1,其余位都是 0,所以只有参与运算的另一个二进制值在同样的位置上也为 1,与 运算的结果才不会为 0.

    0101 (相当于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
    & 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache)
    = 0100 (> 0,也就意味着 option 参数中设置了 SDWebImageDownloaderUseNSURLCache)
    

    2、SDWebImageDownloaderOperation

    a、问题
    • 如何实现下载的网络请求?
    • 如何管理整个图片下载的过程?
    • 图片下载完成后需要做哪些处理?
    b、用途
    • 每张图片的下载都会发出一个异步的 HTTP 请求,这个请求就是由 SDWebImageDownloaderOperation 管理的。
    • 继承 NSOperation,遵守 SDWebImageOperationNSURLConnectionDataDelegate 协议
    • SDWebImageOperation 协议只定义了一个方法 -cancel,用来取消 operation
    c、源码

    SDWebImageDownloaderOperation.h文件中的属性:

    @property (strong, nonatomic, readonly) NSURLRequest *request; // 用来给 operation 中的 connection 使用的请求
    @property (assign, nonatomic) BOOL shouldDecompressImages; // 下载完成后是否需要解压缩
    @property (nonatomic, assign) BOOL shouldUseCredentialStorage; 
    @property (nonatomic, strong) NSURLCredential *credential;
    @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;
    @property (assign, nonatomic) NSInteger expectedSize;
    @property (strong, nonatomic) NSURLResponse *response;
    

    SDWebImageDownloaderOperation.m文件中的属性:

    @property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock;    
    @property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock;
    @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
    
    @property (assign, nonatomic, getter = isExecuting) BOOL executing; // 覆盖了 NSOperation 的 executing
    @property (assign, nonatomic, getter = isFinished) BOOL finished;  // 覆盖了 NSOperation 的 finished
    @property (assign, nonatomic) NSInteger expectedSize;
    @property (strong, nonatomic) NSMutableData *imageData;
    @property (strong, nonatomic) NSURLConnection *connection;
    @property (strong, atomic) NSThread *thread;
    
    @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; 
    
    // 成员变量
    size_t width, height;               // 图片宽高
    UIImageOrientation orientation;     // 图片方向
    BOOL responseFromCached;
    

    SDWebImageDownloaderOperation.h文件中的方法:

    - (id)initWithRequest:(NSURLRequest *)request
                  options:(SDWebImageDownloaderOptions)options
                 progress:(SDWebImageDownloaderProgressBlock)progressBlock
                completed:(SDWebImageDownloaderCompletedBlock)completedBlock
                cancelled:(SDWebImageNoParamsBlock)cancelBlock;    
    

    SDWebImageDownloaderOperation.m文件中的方法:

    // 覆盖了父类的属性,需要重新实现属性合成方法
    @synthesize executing = _executing;
    @synthesize finished = _finished;
    
    // Initialization
    - (id)initWithRequest:(NSURLRequest *)request
                  options:(SDWebImageDownloaderOptions)options
                 progress:(SDWebImageDownloaderProgressBlock)progressBlock
                completed:(SDWebImageDownloaderCompletedBlock)completedBlock
                cancelled:(SDWebImageNoParamsBlock)cancelBlock;
    // Operation
    - (void)start;
    - (void)cancel;
    - (void)cancelInternalAndStop;
    - (void)cancelInternal;
    - (void)done;
    - (void)reset;
    
    // Setter and getter
    - (void)setFinished:(BOOL)finished; 
    - (void)setExecuting:(BOOL)executing; 
    - (BOOL)isConcurrent; 
    
    // NSURLConnectionDataDelegate 方法
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; //  下载过程中的 response 回调
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; // 下载过程中 data 回调
    - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; // 下载完成时回调
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; // 下载失败时回调
    - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; // 在 connection 存储 cached response 到缓存中之前调用
    - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection; //  URL loader 是否应该使用 credential storage
    - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; // connection 发送身份认证的请求之前被调用
    
    // Helper
    + (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value;
    - (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;
    - (BOOL)shouldContinueWhenAppEntersBackground;
    
    d、具体实现

    首先来看看指定初始化方法 -initWithRequest:options:progress:completed:cancelled:,这个方法是保存一些传入的参数,设置一些属性的初始默认值。

    // 接受参数,设置属性
    - (id)initWithRequest:(NSURLRequest *)request
                  options:(SDWebImageDownloaderOptions)options
                 progress:(SDWebImageDownloaderProgressBlock)progressBlock
                completed:(SDWebImageDownloaderCompletedBlock)completedBlock
                cancelled:(SDWebImageNoParamsBlock)cancelBlock {
        if ((self = [super init])) {
            // 设置属性_shouldUseCredentialStorage、_executing、_finished、_expectedSize、responseFromCached 的默认值/初始值
            _request = request;
            _shouldDecompressImages = YES;
            _shouldUseCredentialStorage = YES;
            _options = options;
            _progressBlock = [progressBlock copy];
            _completedBlock = [completedBlock copy];
            _cancelBlock = [cancelBlock copy];
            _executing = NO;
            _finished = NO;
            _expectedSize = 0;
            responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
        }
        return self;
    }
    

    当创建的 SDWebImageDownloaderOperation对象被加入到downloaderdownloadQueue 中时,该对象的-start方法就会被自动调用。

    -start 方法中首先创建了用来下载图片数据的 NSURLConnection,然后开启 connection,同时发出开始图片下载的 SDWebImageDownloadStartNotification 通知,为了防止非主线程的请求被 kill 掉,这里开启runloop保活,直到请求返回。

    - (void)start {
        // 给 self 加锁
        @synchronized (self) {
            // 如果 self 被 cancell 掉的话
            if (self.isCancelled) {
                // finished 属性变为 YES
                self.finished = YES;
                // reset 下载数据和回调 block
                [self reset];
                // 然后直接 return
                return;
            }
    
            Class UIApplicationClass = NSClassFromString(@"UIApplication");
            BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
            // 如果允许程序退到后台后继续下载
            if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
                __weak __typeof__ (self) wself = self;
                UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
                // 就标记为允许后台执行,在后台任务过期的回调 block 中
                self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                    // 首先来一个 weak-strong dance
                    __strong __typeof (wself) sself = wself;
    
                    if (sself) {
                        // 调用 cancel 方法(这个方法里面又做了一些处理,反正就是 cancel 掉当前的 operation)
                        [sself cancel];
                        // 调用 UIApplication 的 endBackgroundTask: 方法结束任务
                        [app endBackgroundTask:sself.backgroundTaskId];
                        // 记录结束后的 taskId
                        sself.backgroundTaskId = UIBackgroundTaskInvalid;
                    }
                }];
            }
    
            // 标记 executing 属性为 YES
            self.executing = YES;
            // 创建 connection,赋值给 connection 属性
            self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
            // 获取 currentThread,赋值给 thread 属性
            self.thread = [NSThread currentThread];
        }
    
        // 启动 connection
        [self.connection start];
    
        // 因为上面初始化 connection 时可能会失败,所以这里我们需要根据不同情况做处理
        if (self.connection) {// A.如果 connection 不为 nil
            if (self.progressBlock) {
                // 回调 progressBlock(初始的 receivedSize 为 0,expectSize 为 -1)
                self.progressBlock(0, NSURLResponseUnknownLength);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                // 发出 SDWebImageDownloadStartNotification 通知(SDWebImageDownloader 会监听到)
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
            });
    
            if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
                // Make sure to run the runloop in our background thread so it can process downloaded data
                // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
                //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
            }
            else {
                // 开启 runloop
                CFRunLoopRun();
            }
            
            // runloop 结束后继续往下执行(也就是 cancel 掉或者 NSURLConnection 请求完毕代理回调后调用了 CFRunLoopStop)
            if (!self.isFinished) {
                [self.connection cancel];
                [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
            }
        }
        else {// B.如果 connection 为 nil
            if (self.completedBlock) {
                // 回调 completedBlock,返回 connection 初始化失败的错误信息
                self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
            }
        }
    
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
            return;
        }
        if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
            UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
            // 下载完成后,调用 endBackgroundTask: 标记后台任务结束
            [app endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    }
    

    NSURLConnection 请求图片数据时,服务器返回的的结果是通过 NSURLConnectionDataDelegate 的代理方法回调的,其中最主要的是以下三个方法:

    - connection:didReceiveResponse: // 下载过程中的 response 回调,调用一次
    - connection:didReceiveData:     // 下载过程中 data 回调,调用多次
    - connectionDidFinishLoading:    // 下载完成时回调,调用一次
    

    前两个方法是在下载过程中回调的,第三个方法是在下载完成时回调的。

    第一个方法 - connection:didReceiveResponse:被调用:

    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
        
        // A. 返回 code 不是 304 Not Modified
        if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
            // 1. 获取 expectedSize
            NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
            self.expectedSize = expected;
            if (self.progressBlock) {
                // 回调 progressBlock
                self.progressBlock(0, expected);
            }
    
            // 2. 初始化 imageData 属性
            self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
            self.response = response;
            dispatch_async(dispatch_get_main_queue(), ^{
                // 3. 发送 SDWebImageDownloadReceiveResponseNotification 通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
            });
        }
        // B. 针对 304 Not Modified 做处理,直接 cancel operation,并返回缓存的 image
        else {
            NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
            
            //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
            //In case of 304 we need just cancel the operation and return cached image from the cache.
            if (code == 304) {
                // 1. 取消连接
                [self cancelInternal];
            } else {
                [self.connection cancel];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                // 2. 发送 SDWebImageDownloadStopNotification 通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            });
    
            if (self.completedBlock) {
                // 3. 回调 completedBlock
                self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
            }
            // 4. 停止 runloop
            CFRunLoopStop(CFRunLoopGetCurrent());
            [self done];
        }
    }
    

    接着会多次调用 - connection:didReceiveData:方法来更新进度、拼接图片数据:

    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
        // 1.拼接图片数据
        [self.imageData appendData:data];
    
        // 2.针对 SDWebImageDownloaderProgressiveDownload 做的处理
        if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
    
            // Get the total bytes downloaded
            const NSInteger totalSize = self.imageData.length;
    
            // 2.1 根据更新的 imageData 创建 CGImageSourceRef 对象
            CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
    
            // 2.2 首次获取到数据时,读取图片属性:width, height, orientation
    
            // 2.3 图片还没下载完,但不是第一次拿到数据,使用现有图片数据 CGImageSourceRef 创建 CGImageRef 对象
            if (width + height > 0 && totalSize < self.expectedSize) {
                // Create the image
                CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
    
                if (partialImageRef) {
                    UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    // 2.4 对图片进行缩放
                    UIImage *scaledImage = [self scaledImageForKey:key image:image];
                    if (self.shouldDecompressImages) {
                        // 对图片进行解码
                        image = [UIImage decodedImageWithImage:scaledImage];
                    }
                    else {
                        image = scaledImage;
                    }
                    CGImageRelease(partialImageRef);
                    dispatch_main_sync_safe(^{
                        if (self.completedBlock) {
                            // 回调 completedBlock
                            self.completedBlock(image, nil, nil, NO);
                        }
                    });
                }
            }
    
            CFRelease(imageSource);
        }
    
        // 3.回调 progressBlock
        if (self.progressBlock) {
            self.progressBlock(self.imageData.length, self.expectedSize);
        }
    }
    

    当图片数据全部下载完成时,- connectionDidFinishLoading:方法就会被调用:

    - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
        // 1. 下载结束
        SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
        @synchronized(self) {
            // 停止 runloop
            CFRunLoopStop(CFRunLoopGetCurrent());
            self.thread = nil;
            self.connection = nil;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                // 发送 SDWebImageDownloadStopNotification 通知和 SDWebImageDownloadFinishNotification 通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            });
        }
        
        if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
            responseFromCached = NO;
        }
        
        // 2. 回调 completionBlock
        if (completionBlock) {
            // 2.1 如果是返回的结果是 URL Cache,就回调图片数据为 nil 的 completionBlock
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
                completionBlock(nil, nil, nil, YES);
            } else if (self.imageData) {// 2.2 如果有图片数据
                // 2.2.1 针对不同图片格式进行数据转换 data -> image
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                // 2.2.2 据图片名中是否带 @2x 和 @3x 来做 scale 处理
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                image = [self scaledImageForKey:key image:image];
                
                // 2.2.3 如果需要解码,就进行图片解码(如果不是 GIF 图)
                if (!image.images) {
                    if (self.shouldDecompressImages) {
                        image = [UIImage decodedImageWithImage:image];
                    }
                }
                
                // 2.2.4 判断图片尺寸是否为空
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    // 如果没有图片数据,回调带有错误信息的 completionBlock
                    completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
                }
                else {
                    // 回调成功拿到图片的 completionBlock
                    completionBlock(image, self.imageData, nil, YES);
                }
            } else {
                // 2.3 如果没有图片数据,回调带有错误信息的 completionBlock
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
            }
        }
        // 3. 将 completionBlock 置为 nil
        self.completionBlock = nil;
        // 4. 重置
        [self done];
    }
    

    当图片的所有数据下载完成后,SDWebImageDownloader传入的 completionBlock被调用,至此,整个图片的下载过程就结束了。

    从上面的解读中我们可以看到,一张图片的数据下载是由一个 NSConnection 对象来完成的,这个对象的整个生命周期(从创建到下载结束)又是由 SDWebImageDownloaderOperation 来控制的,将 operation 加入到operation queue中就可以实现多张图片同时下载了。

    简单概括成一句话就是,NSConnection负责网络请求,NSOperation 负责多线程。

    e、反思
    • NSOperation-start 方法、-main 方法和 -cancel 方法
    • -start 方法中为什么要调用 CFRunLoopRun() 或者 CFRunLoopRunInMode() 函数?
    • SDWebImageDownloaderOperation 中是什么时候开启异步线程的?
    • NSURLConnection 的几个代理方法分别在什么时候调用?
      -NSURLCache 是什么?
    • 下载完成后,为什么需要对图片进行解压缩操作?
    • WebP 图片的解码

    3、SDImageCache

    a、原因
    • 以空间换时间,提升用户体验:加载同一张图片,读取缓存是肯定比远程下载的速度要快得多的。
    • 减少不必要的网络请求,提升性能,节省流量:一般来讲,同一张图片的 URL 是不会经常变化的,所以没有必要重复下载。另外,现在的手机存储空间都比较大,相对于流量来,缓存占的那点空间算不了什么
    b、问题
    • 从读取速度和保存时间上来考虑,缓存该怎么存?key 怎么定?
    • 内存缓存怎么存?
    • 磁盘缓存怎么存?路径、文件名怎么定?
    • 使用时怎么读取缓存?
    • 什么时候需要移除缓存?怎么移除?
    • 文件操作和 NSDirectoryEnumerator
    • 如何判断一个图片的格式是PNG还是 JPG
    c、属性和方法

    SDImageCache 管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,所以不会阻塞主线程,影响用户体验。

    SDImageCache.h文件中的属性:

    @property (assign, nonatomic) BOOL shouldDecompressImages;  // 读取磁盘缓存后,是否需要对图片进行解压缩
    
    @property (assign, nonatomic) NSUInteger maxMemoryCost; // 其实就是 NSCache 的 totalCostLimit,内存缓存总消耗的最大限制,cost 是根据内存中的图片的像素大小来计算的
    @property (assign, nonatomic) NSUInteger maxMemoryCountLimit; // 其实就是 NSCache 的 countLimit,内存缓存的最大数目
    
    @property (assign, nonatomic) NSInteger maxCacheAge;    // 磁盘缓存的最大时长,也就是说缓存存多久后需要删掉
    @property (assign, nonatomic) NSUInteger maxCacheSize;  // 磁盘缓存文件总体积最大限制,以 bytes 来计算
    

    SDImageCache.m文件中的属性:

    @property (strong, nonatomic) NSCache *memCache;
    @property (strong, nonatomic) NSString *diskCachePath;
    @property (strong, nonatomic) NSMutableArray *customPaths; // 只读的路径,比如 bundle 中的文件路径,用来在 SDWebImage 下载、读取缓存之前预加载用的
    @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
    
    NSFileManager *_fileManager;
    

    枚举

    typedef NS_ENUM(NSInteger, SDImageCacheType) {
        SDImageCacheTypeNone,   // 没有读取到图片缓存,需要从网上下载
        SDImageCacheTypeDisk,   // 磁盘中的缓存
        SDImageCacheTypeMemory  // 内存中的缓存
    };
    

    SDImageCache.h文件中的方法:

    + (SDImageCache *)sharedImageCache;
    - (id)initWithNamespace:(NSString *)ns;
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
    
    -(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
    
    
    - (void)addReadOnlyCachePath:(NSString *)path;
    
    
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key;
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
    
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
    
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
    
    
    - (void)removeImageForKey:(NSString *)key;
    - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
    
    - (void)clearMemory;
    
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)clearDisk;
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
    - (void)cleanDisk;
    
    
    - (NSUInteger)getSize;
    - (NSUInteger)getDiskCount;
    - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
    
    
    - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
    - (BOOL)diskImageExistsWithKey:(NSString *)key;
    
    
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
    - (NSString *)defaultCachePathForKey:(NSString *)key;
    

    SDImageCache.m文件中的函数:

    NSUInteger SDCacheCostForImage(UIImage *image);
    BOOL ImageDataHasPNGPreffix(NSData *data);
    

    SDImageCache.m文件中的方法:

    // Lifecycle
    + (SDImageCache *)sharedImageCache;
    - (id)init;
    - (id)initWithNamespace:(NSString *)ns;
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
    - (void)dealloc;
    
    // Cache Path
    - (void)addReadOnlyCachePath:(NSString *)path; // 添加只读路径,比如 bundle 中的文件路径,用来在 SDWebImage 下载、读取缓存之前预加载用的
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
    - (NSString *)defaultCachePathForKey:(NSString *)key;
    - (NSString *)cachedFileNameForKey:(NSString *)key
    -(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
    
    // Store Image
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk 
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key;
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
    
    
    // Check if image exists
    - (BOOL)diskImageExistsWithKey:(NSString *)key;
    - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
    
    // Query the image cache
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
    - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key;
    - (UIImage *)diskImageForKey:(NSString *)key;
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
    - (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;
    
    // Remove specified image
    - (void)removeImageForKey:(NSString *)key;
    - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
    
    // Setter and getter
    - (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost;
    - (NSUInteger)maxMemoryCost;
    - (NSUInteger)maxMemoryCountLimit;
    - (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit;
    
    // Clear and clean
    - (void)clearMemory;
    - (void)clearDisk;
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)cleanDisk;
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
    - (void)backgroundCleanDisk;
    
    // Cache Size
    - (NSUInteger)getSize;
    - (NSUInteger)getDiskCount;
    - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
    
    d、具体实现

    SDImageCache的内存缓存是通过一个继承 NSCacheAutoPurgeCache类来实现的。

    NSCache 是一个类似于 NSMutableDictionary存储 key-value 的容器,主要有以下特点:

    • 自动删除机制:当系统内存紧张时,NSCache会自动删除一些缓存对象
    • 线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
    • 不同于 NSMutableDictionaryNSCache存储对象时不会对 key 进行 copy 操作
    // SDImageCache
    _memCache = [[AutoPurgeCache alloc] init];
    
    // AutoPurgeCache
    @interface AutoPurgeCache : NSCache
    @end
    
    @implementation AutoPurgeCache
    
    - (id)init
    {
        self = [super init];
        if (self) {
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        }
        return self;
    }
    
    - (void)dealloc
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    
    }
    
    @end
    

    SDImageCache 的磁盘缓存是通过异步操作 NSFileManager 存储缓存文件到沙盒来实现的。

    1、初始化方法调用栈

    -init
        -initWithNamespace:
            -makeDiskCachePath:
            -initWithNamespace:diskCacheDirectory:
    

    -init 方法中默认调用了 -initWithNamespace:方法,-initWithNamespace:方法又调用了 -makeDiskCachePath:方法来初始化缓存目录路径, 同时还调用了 -initWithNamespace:diskCacheDirectory:方法来实现初始化。

    - (id)init {
        return [self initWithNamespace:@"default"];
    }
    
    - (id)initWithNamespace:(NSString *)ns {
        NSString *path = [self makeDiskCachePath:ns];
        return [self initWithNamespace:ns diskCacheDirectory:path];
    }
    

    -initWithNamespace: diskCacheDirectory:是一个 Designated Initializer,这个方法中主要是初始化实例变量、属性,设置属性默认值,并根据 namespace 设置完整的缓存目录路径,除此之外,还针对 iOS 添加了通知观察者,用于内存紧张时清空内存缓存,以及程序终止运行时和程序退到后台时清扫磁盘缓存。

    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
        if ((self = [super init])) {
            // 根据 namespace 设置完整的缓存目录路径
            NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
    
            // 初始化实例变量、属性,设置属性默认值
            
            // initialise PNG signature data
            kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
    
            // Create IO serial queue
            _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    
            // Init default values
            _maxCacheAge = kDefaultCacheMaxCacheAge;
    
            // Init the memory cache
            _memCache = [[AutoPurgeCache alloc] init];
            _memCache.name = fullNamespace;
    
            // Init the disk cache
            if (directory != nil) {
                _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
            } else {
                NSString *path = [self makeDiskCachePath:ns];
                _diskCachePath = path;
            }
    
            // Set decompression to YES
            _shouldDecompressImages = YES;
    
            dispatch_sync(_ioQueue, ^{
                _fileManager = [NSFileManager new];
            });
    
            // 添加了通知观察者,用于内存紧张时清空内存、磁盘缓存
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(clearMemory)
                                                         name:UIApplicationDidReceiveMemoryWarningNotification
                                                       object:nil];
    
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(cleanDisk)
                                                         name:UIApplicationWillTerminateNotification
                                                       object:nil];
            // 以及程序终止运行时和程序退到后台时清扫磁盘缓存
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(backgroundCleanDisk)
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
        }
    
        return self;
    }
    

    2、写入缓存
    步骤一:写入缓存的操作主要是由 - storeImage:recalculateFromImage:imageData:forKey:toDisk:方法处理的,在存储缓存数据时,先计算图片像素大小,并存储到内存缓存中去,然后如果需要存到磁盘(沙盒)中,就开启异步线程将图片的二进制数据存储到磁盘(沙盒)中。

    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
        if (!image || !key) {
            return;
        }
    
    // 1. 添加内存缓存
        // 1.1 先计算图片像素大小
        NSUInteger cost = SDCacheCostForImage(image);
        // 1.2 并存储到内存缓存中去
        [self.memCache setObject:image forKey:key cost:cost];
    
    // 2. 如果需要存储到沙盒的话,就异步执行磁盘缓存操作
        // 如果需要存到磁盘(沙盒)中
        if (toDisk) {
            // 就开启异步线程
            dispatch_async(self.ioQueue, ^{
                // 将图片的二进制数据存储到磁盘(沙盒)中
                NSData *data = imageData;
    
                // 2.1 如果需要 recalculate (重新转 data)或者传进来的 imageData 为空的话,就再转一次 data,因为存为文件的必须是二进制数据
                if (image && (recalculate || !data)) {
                    ......
                }
    
                if (data) {
                    // 2.2 借助 NSFileManager 将图片二进制数据存储到沙盒
                    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                    }
                    // 存储的文件名是对 key 进行 MD5 处理后生成的字符串
                    [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
                }
            });
        }
    }
    

    步骤二:如果需要在存储之前将传进来的 image 转成 NSData,而不是直接使用传入的 imageData,那么就要针对 iOS 系统下,按不同的图片格式来转成对应的 NSData 对象。那么图片格式是怎么判断的呢?这里是根据是否有 alpha 通道以及图片数据的前 8 位字节来判断是不是 PNG 图片,不是 PNG 的话就按照 JPG 来处理。

    // 将图片的二进制数据存储到磁盘(沙盒)中
    NSData *data = imageData;
    
    // 2.1 如果需要 recalculate (重新转 data)或者传进来的 imageData 为空的话,就再转一次 data,因为存为文件的必须是二进制数据
    if (image && (recalculate || !data)) {
        // 2.1.1 如果 imageData 为 nil,就根据 image 是否有 alpha 通道来判断图片是否是 PNG 格式的
        int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
        BOOL imageIsPng = hasAlpha;
        
        // 2.1.2 如果 imageData 不为 nil,就根据 imageData 的前 8 位字节来判断是不是 PNG 格式的
        // 因为 PNG 图片有一个唯一签名,前 8 位字节是(十进制): 137 80 78 71 13 10 26 10
        if ([imageData length] >= [kPNGSignatureData length]) {
            imageIsPng = ImageDataHasPNGPreffix(imageData);
        }
        
        // 2.1.3 根据图片格式将 UIImage 转为对应的二进制数据 NSData
        if (imageIsPng) {
            data = UIImagePNGRepresentation(image);
        }
        else {
            data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
        }
        data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    }
    

    步骤三:将图片数据存储到磁盘(沙盒)时,需要提供一个包含文件名的路径,这个文件名是一个对 key 进行 MD5 处理后生成的字符串。

    // 存储的文件名是对 key 进行 MD5 处理后生成的字符串
    [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
    
    - (NSString *)defaultCachePathForKey:(NSString *)key {
        return [self cachePathForKey:key inPath:self.diskCachePath];
    }
    
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
        NSString *filename = [self cachedFileNameForKey:key];
        return [path stringByAppendingPathComponent:filename];
    }
    
    #define CC_MD5_DIGEST_LENGTH    16          /* digest length in bytes */
    
    - (NSString *)cachedFileNameForKey:(NSString *)key {
        const char *str = [key UTF8String];
        if (str == NULL) {
            str = "";
        }
        unsigned char r[CC_MD5_DIGEST_LENGTH];
        CC_MD5(str, (CC_LONG)strlen(str), r);
        NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
                                                        r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
    
        return filename;
    }
    

    3、读取缓存
    步骤一:SDWebImage 在给 UIImageView 加载图片时首先需要查询缓存,查询缓存的操作主要是 -queryDiskCacheForKey:done: 方法来实现的,该方法首先会调用-imageFromMemoryCacheForKey方法来查询内存缓存,也就是从 memCache 中去找,如果找到了对应的图片(一个 UIImage 对象),就直接回调 doneBlock,并直接返回。 如果内存缓存中没有找到对应的图片,就开启异步队列,调用 -diskImageForKey 读取磁盘缓存,读取成功之后,再保存到内存缓存,最后再回到主队列,回调 doneBlock

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
        // 1.先检查内存缓存,如果找到了对应的图片就回调 doneBlock,并直接返回
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
    
        // 2.如果内存缓存中没有找到对应的图片,开启异步队列,读取硬盘缓存
        NSOperation *operation = [NSOperation new];
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                return;
            }
    
            @autoreleasepool {
                // 2.1 读取磁盘缓存
                UIImage *diskImage = [self diskImageForKey:key];
                // 2.2 如果有磁盘缓存,就保存到内存缓存
                if (diskImage) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
                // 2.3 回到主队列,回调 doneBlock
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
    
        return operation;
    }
    

    步骤二:其中读取磁盘缓存并不是一步就完成了的,读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths (也就是bundle)中去找,找到之后,再对数据进行转换,后面的图片处理步骤跟图片下载成功后的图片处理步骤一样——先将 data 转成 image,再根据文件名中的 @2x@3x 进行缩放处理,如果需要解压缩,最后再解压缩一下。

    - (UIImage *)diskImageForKey:(NSString *)key {
        // 读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths (也就是bundle)中去找
        NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
        // 跟图片下载成功后的图片处理步骤一样
        if (data) {
            // 先将 data 转成 image
            UIImage *image = [UIImage sd_imageWithData:data];
            // 再根据文件名中的 @2x、@3x 进行缩放处理
            image = [self scaledImageForKey:key image:image];
            // 如果需要解压缩,最后再解压缩一下
            if (self.shouldDecompressImages) {
                image = [UIImage decodedImageWithImage:image];
            }
            return image;
        }
        else {
            return nil;
        }
    }
    
    - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
        // 取磁盘缓存时,会先从沙盒中去找
        NSString *defaultPath = [self defaultCachePathForKey:key];
        NSData *data = [NSData dataWithContentsOfFile:defaultPath];
        if (data) {
            return data;
        }
    
        // 如果沙盒中没有,再从 customPaths (也就是bundle)中去找
        NSArray *customPaths = [self.customPaths copy];
        for (NSString *path in customPaths) {
            NSString *filePath = [self cachePathForKey:key inPath:path];
            NSData *imageData = [NSData dataWithContentsOfFile:filePath];
            if (imageData) {
                return imageData;
            }
        }
    
        return nil;
    }
    
    4、清扫磁盘缓存

    每新加载一张图片,就会新增一份缓存,时间一长,磁盘上的缓存只会越来越多,所以我们需要定期清除部分缓存。值得注意的是,清扫磁盘缓存(clean)和清空磁盘缓存(clear)是两个不同的概念,清空是删除整个缓存目录,清扫只是删除部分缓存文件。

    清扫磁盘缓存有两个指标:一是缓存有效期,二是缓存体积最大限制。SDImageCache中的缓存有效期是通过 maxCacheAge 属性来设置的,默认值是 1 周,缓存体积最大限制是通过 maxCacheSize 来设置的,默认值为 0。

    SDImageCache 在初始化时添加了通知观察者,所以在应用即将终止时和退到后台时,都会调用 -cleanDiskWithCompletionBlock: 方法来异步清扫缓存。

    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
    .......
            // 添加了通知观察者,用于内存紧张时清空内存、磁盘缓存
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(clearMemory)
                                                         name:UIApplicationDidReceiveMemoryWarningNotification
                                                       object:nil];
    
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(cleanDisk)
                                                         name:UIApplicationWillTerminateNotification
                                                       object:nil];
            // 以及程序终止运行时和程序退到后台时清扫磁盘缓存
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(backgroundCleanDisk)
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
    ......
    }
    
    - (void)dealloc {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        SDDispatchQueueRelease(_ioQueue);
    }
    
    // 清空内存缓存
    - (void)clearMemory {
        [self.memCache removeAllObjects];
    }
    
    // 清空磁盘缓存
    - (void)clearDisk {
        [self clearDiskOnCompletion:nil];
    }
    
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
    {
        dispatch_async(self.ioQueue, ^{
            [_fileManager removeItemAtPath:self.diskCachePath error:nil];
            [_fileManager createDirectoryAtPath:self.diskCachePath
                    withIntermediateDirectories:YES
                                     attributes:nil
                                          error:NULL];
    
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    }
    
    // 清扫只是删除部分缓存文件
    - (void)cleanDisk {
        [self cleanDiskWithCompletionBlock:nil];
    }
    
    - (void)backgroundCleanDisk {
        ......
        [self cleanDiskWithCompletionBlock:^{
            [application endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }];
    }
    

    清扫磁盘缓存的逻辑是,先遍历所有缓存文件,并根据文件的修改时间来删除过期的文件,同时记录剩下的文件的属性和总体积大小,如果设置了 maxCacheAge 属性的话,接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面),最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。

    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    
        dispatch_async(self.ioQueue, ^{
            // 先遍历所有缓存文件
            for (NSURL *fileURL in fileEnumerator) {
                NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
                // Skip directories.
                if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // Remove files that are older than the expiration date;
                // 并根据文件的修改时间来删除过期的文件
                NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
                if ([[modificationDate 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 setObject:resourceValues forKey:fileURL];
            }
            
            for (NSURL *fileURL in urlsToDelete) {
                [_fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // 如果设置了 maxCacheAge 属性的话
            if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
                // Target half of our maximum cache size for this cleanup pass.
                const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
                // Sort the remaining cache files by their last modification time (oldest first).
                // 接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面)
                NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                    return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                }];
    
                // Delete files until we fall below our desired cache size.
                // 最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。
                for (NSURL *fileURL in sortedFiles) {
                    if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                        NSDictionary *resourceValues = cacheFiles[fileURL];
                        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                        currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
    
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    

    4、SDWebImageManager

    a、问题
    • 读取磁盘缓存操作和下载操作都是异步的,如何管理这两个操作(operation)?
    • 对于下载失败过的 URL,如何处理重试?
    b、属性和方法

    SDWebImageManager.h文件中的属性

    @property (weak, nonatomic) iddelegate;
    @property (strong, nonatomic, readonly) SDImageCache *imageCache;              // 缓存器
    @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 下载器
    @property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;      // 用来自定义缓存 key 的 block
    

    SDWebImageManager.m文件中的属性

    @property (strong, nonatomic, readwrite) SDImageCache *imageCache;
    @property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
    @property (strong, nonatomic) NSMutableSet *failedURLs;             // 下载失败过的 URL 
    @property (strong, nonatomic) NSMutableArray *runningOperations;    // 正在执行中的任务
    
    c、具体实现

    SDWebImageManager 的核心任务是由 -downloadImageWithURL:options:progress:completed:方法来实现的,这个方法中先会从SDImageCache 中读取缓存,如果有缓存,就直接返回缓存,如果没有就通过 SDWebImageDownloader 下载,下载成功后再保存到缓存中去,然后再回调 completedBlock。其中 progressBlock的回调是直接交给了 SDWebImageDownloaderprogressBlock 来处理的。

    SDWebImageManager 在读取缓存和下载之前会创建一个 SDWebImageCombinedOperation 对象,这个对象是用来管理缓存读取操作和下载操作的,SDWebImageCombinedOperation 对象有 3 个属性:

    • cancelled:用来取消当前加载任务的
    • cancelBlock:用来移除当前加载任务和取消下载任务的
    • cacheOperation:用来取消读取缓存操作
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        // 1. 对 completedBlock 和 url 进行检查
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
        // 2. 创建 SDWebImageCombinedOperation 对象
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
        // 3. 判断是否是曾经下载失败过的 url
        BOOL isFailedUrl = NO;
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    
        // 4. 如果这个 url 曾经下载失败过,并且没有设置 SDWebImageRetryFailed,就直回调 completedBlock,并且直接返回
        if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            dispatch_main_sync_safe(^{
                NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
                completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
            });
            return operation;
        }
    
        // 5. 添加 operation 到 runningOperations 中
        @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
    
        // 6. 计算缓存用的 key,读取缓存
        NSString *key = [self cacheKeyForURL:url];
    
        // 7. 处理缓存查询结果回调
        operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
            // 7.1 判断 operation 是否已经被取消了,如果已经取消了就直接移除 operation
            if (operation.isCancelled) {
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
    
                return;
            }
    
            // 7.2.A 如果缓存中没有图片或者图片每次都需要更新
            if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                // 7.2.A.1 如果有缓存图片,先回调 completedBlock,回传缓存的图片
                if (image && options & SDWebImageRefreshCached) {
                    dispatch_main_sync_safe(^{
                        completedBlock(image, nil, cacheType, YES, url);
                    });
                }
    
                // download if no image or requested to refresh anyway, and download allowed by delegate
                // 7.2.A.2 开始下载图片,获得 subOperation
                SDWebImageDownloaderOptions downloaderOptions = 0;
                .......
                id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                    // 7.2.A.2.1.A 操作被取消,什么都不干
                    if (weakOperation.isCancelled) {}
                    // 7.2.A.2.1.B 下载失败
                    else if (error) {
                        // 7.2.A.2.B.1 没有被取消的话,回调 completedBlock
                        dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                            }
                        });
                        .......
                        if (shouldBeFailedURLAlliOSVersion || shouldBeFailedURLiOS7) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
                        }
                    }
                    // 7.2.A.2.1.C 下载成功
                    else {
                        // 7.2.A.2.1.C.1 将 URL 从下载失败的黑名单中移除
                        if ((options & SDWebImageRetryFailed)) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
                        }
    
                        // 7.2.A.2.1.C.2 缓存图片
                        if (downloadedImage && finished) {
                            
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                        
                        // 7.2.A.2.1.C.3 回调 completedBlock
                        dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
    
                    // 7.2.A.2.2 将 operation 从 runningOperations 中移除
                    if (finished) {
                        @synchronized (self.runningOperations) {
                            [self.runningOperations removeObject:operation];
                        }
                    }
    
                // 7.2.1.3 设置 SDWebImageCombinedOperation 的 cancelBlock——cancel 掉 subOperation,并移除 operation
                operation.cancelBlock = ^{
                    [subOperation cancel];
                    
                    @synchronized (self.runningOperations) {
                        [self.runningOperations removeObject:weakOperation];
                    }
                };
            // 7.2.B 如果有缓存图片且不需要每次更新
            else if (image) {
                // 7.2.B.1 回调 completedBlock
                dispatch_main_sync_safe(^{
                    if (!weakOperation.isCancelled) {
                        completedBlock(image, nil, cacheType, YES, url);
                    }
                });
                // 7.2.B.2 流程结束,从 runningOperations 中移除 operation
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
            }
            // 7.2.C 如果没有缓存图片而且不允许下载
            else {
                // Image not in cache and download disallowed by delegate
                // 7.2.B.1 回调 completedBlock
                dispatch_main_sync_safe(^{
                    if (!weakOperation.isCancelled) {
                        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                    }
                });
                // 7.2.B.2 流程结束,从 runningOperations 中移除 operation
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
            }
        }];
    
        return operation;
    }
    

    5、UIImageView+WebCache

    a、问题
    • 如何处理 UIImageView 连续多次加载图片的情况,比如在UITableView 的 ...cellForRow... 方法中加载 cell 上的图片?
    • 如何处理placeholder image 的显示逻辑?
    b、具体实现

    实际上是将 SDWebImageManager 封装了一层

    • 占位图设置
    • 自动管理图片加载任务
    • 图片成功获取后,自动设置图片显示

    为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,该方法中首先通过调用 -sd_cancelCurrentImageLoad 方法取消这个 UIImageView 当前的下载任务,然后设置了占位图,如果 url 不为 nil,接着就调用 SDWebImageManager-downloadImage... 方法开始加载图片,并将这个加载任务 operation 保存起来,用于后面的 cancel 操作。图片获取成功后,再重新设置 imageViewimage,并回调 completedBlock

    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
        
        // 1. 取消当前正在进行的加载任务
        [self sd_cancelCurrentImageLoad];
        
        // 2. 通过 Associated Object 将 url 作为成员变量存起来
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
        // 3. 设置占位图
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                self.image = placeholder;
            });
        }
        
        // 4. 根据 url 是否为 nil 做处理
        if (url) {// A. 如果 url 不为 nil
            __weak __typeof(self)wself = self;
            //  A.1 调用 SDWebImageManager 的 -downloadImage... 方法开始加载图片,并获得一个 operation
            id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    
                dispatch_main_sync_safe(^{
                    if (!wself) return;
                    
                    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                    {
                        //  A.1.1.A 如果不需要自动设置 image,直接 return
                        completedBlock(image, error, cacheType, url);
                        return;
                    }
                    else if (image) {
                        // A.1.1.B 图片下载成功,设置 image
                        wself.image = image;
                        [wself setNeedsLayout];
                    } else {
                        // A.1.1.C 图片下载失败,设置 placeholder
                        if ((options & SDWebImageDelayPlaceholder)) {
                            wself.image = placeholder;
                            [wself setNeedsLayout];
                        }
                    }
                    // A.1.2 回调 completedBlock
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
            // A.2 借助 UIView+WebCacheOperation 将获得的 operation 保存到成员变量中去
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } else {
            // B. URL 为空时,直接回调 completedBlock,返回错误信息
            dispatch_main_async_safe(^{
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                if (completedBlock) {
                    completedBlock(nil, error, SDImageCacheTypeNone, url);
                }
            });
        }
    }
    

    值得注意的是,为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,每个UIImageView的图片加载任务都会保存成一个 Associated Object,方便需要时取消任务。这个Associated Object的操作是在 UIView+WebCacheOperation 中实现的,因为除了 UIImageView 用到图片加载功能之外,还有 UIButton 等其他类也用到了加载远程图片的功能,所以需要进行同样的处理,这样设计实现了代码的复用。

    #import "UIView+WebCacheOperation.h"
    #import "objc/runtime.h"
    
    static char loadOperationKey;
    
    @implementation UIView (WebCacheOperation)
    
    - (NSMutableDictionary *)operationDictionary {
        NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        if (operations) {
            return operations;
        }
        operations = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
    
    - (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
        [self sd_cancelImageLoadOperationWithKey:key];
        NSMutableDictionary *operationDictionary = [self operationDictionary];
        [operationDictionary setObject:operation forKey:key];
    }
    
    - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
        // Cancel in progress downloader from queue
        NSMutableDictionary *operationDictionary = [self operationDictionary];
        id operations = [operationDictionary objectForKey:key];
        if (operations) {
            if ([operations isKindOfClass:[NSArray class]]) {
                for (id <SDWebImageOperation> operation in operations) {
                    if (operation) {
                        [operation cancel];
                    }
                }
            } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
                [(id<SDWebImageOperation>) operations cancel];
            }
            [operationDictionary removeObjectForKey:key];
        }
    }
    
    - (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
        NSMutableDictionary *operationDictionary = [self operationDictionary];
        [operationDictionary removeObjectForKey:key];
    }
    
    @end
    

    Demo

    Demo在我的Github上,欢迎下载。
    SourceCodeAnalysisDemo

    参考文献

    相关文章

      网友评论

          本文标题:IOS框架:SDWeblmage(下)

          本文链接:https://www.haomeiwen.com/subject/ooiomktx.html