IOS框架:SDWeblmage(下)

2020-10-26  本文已影响0人  时光啊混蛋_97boy

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

目录

IOS框架:SDWeblmage(上)

四、实现细节

SDWebImage 最核心的功能

1、SDWebImageDownloader

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

a、问题
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、用途
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、反思

3、SDImageCache

a、原因
b、问题
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 的容器,主要有以下特点:

// 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、问题
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 个属性:

- (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、问题
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

参考文献

上一篇下一篇

猜你喜欢

热点阅读