这段时间研究了一下SDWebImage源码,因个人能力有限,如有不对的地方,请指出或留言,谢谢!

SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:

1.一个异步的图片加载器。

2.一个异步的内存+磁盘图片缓存

3.支持GIF、WebP图片

4.后台图片解压缩处理

5.确保同一个URL的图片不被多次下载

6.确保非法的URL不会被反复加载

7.确保下载及缓存时,主线程不被阻塞。

SDWebImage大致流程如下图所示:


SDWebImage是一个成熟而且比较庞大的框架,但使用非常方便。最常用的是给UIImageView设置图片的接口,SDWebImage有提供多个接口给UIImageview设置图片,

最常用的接口为-(void)sd_setImageWithURL:(NSURL *)url  placeholderImage:(UIImage *)placeholder,代码如下所示:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

但所有的接口,最终都会调用下面代码所示的方法:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    //1.取消先前的下载任务
    [self sd_cancelCurrentImageLoad];
    //动态的添加属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            //设置占位图
            self.image = placeholder;
        });
    }
    
    if (url) {

        // check if activityView is enabled or not
        //是否显示进度条
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }

        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            //移除进度条
            [wself removeActivityIndicator];
            //self是否被释放
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                //不要自动设置图片,调用Block传入UIImage对象
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    //图片不为空并且不自动设置图片
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //将生成的加载操作赋给UIView的自定义属性,并赋值
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

1.取消先前的下载任务。开始加载之前图片先取消对应的UIImageView先前的图片下载操作。给UIImageView设置一张新图片时,用户不会关心UIImageView先前加载了哪张图片。

//取消图片加载操作
- (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];
    }
}

SDWebImageManager

在上面讲到的UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。UIImageView+WebCache提供的-(void)sd_setImageWithURL:(NSURL *)url  placeholderImage:(UIImage *)placeholder方法中,图片的下载操作最终是调用SDWebImageManager的方法,如下所示:

[SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)</span>
该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,如下代码所示:

@interface SDWebImageManager : NSObject 
@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate; 
@property (strong, nonatomic, readonly) SDImageCache *imageCache; 
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; 
... 
@end

在上面的代码中可以看到有一个delegate属性,该代理声明了两个可选实现的方法:

// 控制当图片在缓存中没有找到时,应该下载哪个图片
 - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL; 
// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换 
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;</span>
在两个代理方法会在SDWebImageManager的核心方法-downloadImageWithURL:options:progress:completed:中调用,具体实现如下所示:

//SDWebImageManager核心代码
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    //1.判断URL的合法性
    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    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为nil,或者在不可重试的情况下是一个下载失败的url,则直接返回操作对象并调用完成回调
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    NSString *key = [self cacheKeyForURL:url];

    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            //互斥锁防止冲突
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }

        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                    // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }

            ...
            //下载
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                }
                //如果出错,则调用完成回调,并将url放入下载失败url数组中
                else if (error) {
                    dispatch_main_sync_safe(^{
                        if (strongOperation && !strongOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });

                    ...
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                        .....
                    }
                    else {
                        if (downloadedImage && finished) { //下载完成,缓存图片
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }

                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }

                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];
           ...
        }
        else if (image) {
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !strongOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
            // Image not in cache and download disallowed by delegate
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
    }];

    return operation;
}


SDWebImageManager核心方法中涉及到两个重要的类SDWebImageDownloader 
SDImageCache,下面将分别进行讲解: 

SDWebImageDownloader

 在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的。它是一个异步下载器,并对图像加载做了优化处理。

下载选项

在下载过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举SDWebImageDownloaderOptions定义,具体如下:

<span style="font-size:12px;">typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {  
SDWebImageDownloaderLowPriority = 1 <<0, 
 SDWebImageDownloaderProgressiveDownload = 1 << 1,  
// 默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache  
SDWebImageDownloaderUseNSURLCache = 1 << 2,  
// 如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block  
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,  
// 在iOS4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消  
SDWebImageDownloaderContinueInBackground = 1 << 4,  
// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie  
SDWebImageDownloaderHandleCookies = 1 << 5,  
// 允许不受信任的SSL证书。主要用于测试目的。   
<pre name="code" class="objc">SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, </span>
// 将图片下载放到高优先级队列中 SDWebImageDownloaderHighPriority = 1 << 7,};
 

下载顺序

SDWebImage的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示:

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {  
// 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序  
SDWebImageDownloaderFIFOExecutionOrder,  
// 以栈的方式,按照后进先出的顺序下载。  
SDWebImageDownloaderLIFOExecutionOrder
};
下载管理器

SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个NSOperationQueue操作队列中来完成的,其声明如下:

@property (strong, nonatomic) NSOperationQueue *downloadQueue;
默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader类的maxConcurrentDownloads属性来修改所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理,其声明及定义如下:
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调,这些回调操作是以block形式来呈现,如下所示:

//下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
//下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);

图片下载的这些回调信息存储在SDWebImageDownloader类的URLCallbacks属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。由于允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证URLCallbacks操作(添加、删除)的线程安全,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并通过同步机制设置屏障来确保同一时间只有一个线程操作URLCallbacks属性。

dispatch_barrier_sync(sself.barrierQueue, ^{
 callbacksForURL = [sself.URLCallbacks[url] copy];
//如果finished为YES,则将该url对象的回调信息从URLCallbacks中删除
 if (finished) {
   [sself.URLCallbacks removeObjectForKey:url];
   }
 });
整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的如下方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    //URLCallbacks属性,key是图片的URL地址,value是一个数组,包含每个图片的多组回调信息
    //url为空的话,immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    /**
     *  保证同一时间只有一个线程对URLCallbacks进行操作
     */
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作。

- (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 completedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        
        //1.创建请求对象,并根据options参数设置其属性
        //为了避免潜在的重复缓存,如果没有明确告知需要缓存,则禁用图片请求的缓存操作

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        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;
        }
        //2.创建SDWebImageDownloaderOperation操作对象,并进行配置
        //配置信息包括是否需要认证、优先级
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             //3.从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                        //4.从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                    //如果finished为YES,则将该url对象的回调信息从URLCallbacks中删除
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                     //5.取消操作将该url对应的回调信息从URLCallbacks中删除
                                                        cancelled:^{
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //6.将操作加入到操作队列downloadQueue中
        //如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
        [wself.downloadQueue addOperation:operation];
        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;
        }
    }];

    return operation;
}
下载操作

每个图片的下载都是一个Operation操作。在上面分析过这个操作的创建及加入操作队列的过程。现在来看看单个操作的具体实现,SDWebImage定义了一个协议,即SDWebImageOperation作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

SDWebImage自定义了一个Operation类,即SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled。

对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLConnection类(并未使用7.0以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLConnection各代理方法的实现。

首先,SDWebimageDownloaderOperation在分类中采用了NSURLConnectionDataDelegate协议,并实现了该协议的以下几个方法:

- connection:didReceiveResponse:
- connection:didReceiveData:
- connectionDidFinishLoading:
- connection:didFailWithError:
- connection:willCacheResponse:
- connectionShouldUseCredentialStorage:
- connection:willSendRequestForAuthenticationChallenge:
我们在此不逐一分析每个方法的实现,就重点分析一下-connection:didReceiveData:方法。该方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象来做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。

//接收数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    //1.附加数据
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf

        // Get the total bytes downloaded
        //获取已下载数据总大小
        const NSInteger totalSize = self.imageData.length;

        // Update the data source, we must pass ALL the data, not just the new bytes
        //3.更新数据源,我们需要传入所有数据,而不仅仅是新数据
        //用CGImageSourceRef对象
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        //4.首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值
        if (width + height == 0) {
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                //获取图像信息的数据中取出图像的长、宽、方向等信息
                NSInteger orientationValue = -1;
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                CFRelease(properties);
                //5.绘制到Core Graphics时,我们会丢弃方向信息
                // When we draw to Core Graphics, we lose orientation information,
                // which means the image below born of initWithCGIImage will be
                // oriented incorrectly sometimes. (Unlike the image born of initWithData
                // in connectionDidFinishLoading.) So save it here and pass it on later.
                //方向不对,在这边我们先保存这个信息并在后面使用
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }

        //6.图片还未下载完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            //7.使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            //8.适合于手机iOS变形图像的解决方案。
            // Workaround for iOS anamorphic image
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    CGImageRelease(partialImageRef);
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif
            
            //9.对图片进行缩放,解码操作
            if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                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) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }

        CFRelease(imageSource);
    }

    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

前面说过SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态。

在start方法中,创建了我们下载所使用的NSURLConnection对象,开启了图片的下载,同时抛出一个下载开始的通知。当然,如果我们期望下载在后台处理,则只需要配置下载选项,使其包含SDWebImageDownloaderContinueInBackground选项。start方法的具体实现如下:

- (void)start {

    //同步锁
    //管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        //1.如果设置了在后台执行,则进行后台执行
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
    }

    [self.connection start];

    if (self.connection) {
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        //2.在主线程中抛出下载通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });

        //3.启动run loop
        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 {
            CFRunLoopRun();
        }

        //4.如果未完成,则取消连接
        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

为什么会重写start方法而不重写main方法可能是为了控制operation的生命周期

当然,在下载完成或下载失败后,需要停止当前线程的run loop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。

SDImageCache

为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。
SDWebImage提供了对图片缓存的支持,而该功能是由SDImageCache类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。
内存缓存及磁盘缓存
内存缓存的处理是使用NSCache对象来实现的。NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。
磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache还定义了一个串行队列,来异步存储图片。
内存缓存与磁盘缓存相关变量的声明及定义如下:
@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache;
@property (strong, nonatomic) NSString *diskCachePath;
@property (strong, nonatomic) NSMutableArray *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
@end
- (id)initWithNamespace:(NSString *)ns {
    if ((self = [super init])) {
  NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
  ...
  _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
  ...
  // Init the memory cache
  _memCache = [[NSCache alloc] init];
  _memCache.name = fullNamespace;
  // Init the disk cache
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace];
  dispatch_sync(_ioQueue, ^{
      _fileManager = [NSFileManager new];
  });
  ...
    }
    return self;
}

SDImageCache提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个key值来索引它。在内存中,我们将其作为NSCache的key值,而在磁盘中,我们用这个key作为图片的文件名。对于一个远程服务器下载的图片,其url是作为这个key的最佳选择了。我们在后面会看到这个key值的重要性。
存储图片
我们先来看看图片的缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task放到串行队列中处理。在iOS中,会先检测图片是PNG还是JPEG,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5摘要后的串)。缓存操作的基础方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk,它的具体实现如下:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
  ...
  // 1. 内存缓存,将其存入NSCache中,同时传入图片的消耗值
  [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
  if (toDisk) {
    // 2. 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入ioQueue中
    dispatch_async(self.ioQueue, ^{
      NSData *data = imageData;
      if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
        // 3. 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
        // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型
        BOOL imageIsPng = YES;
        if ([imageData length] >= [kPNGSignatureData length]) {
          imageIsPng = ImageDataHasPNGPreffix(imageData);
        }
        if (imageIsPng) {
          data = UIImagePNGRepresentation(image);
        }
        else {
          data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
        }
#else
        data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
      }
      // 4. 创建缓存文件并存储图片
      if (data) {
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
          [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
      }
    });
  }
}

查询图片
如果我们想在内存或磁盘中查询是否有key指定的图片,则可以分别使用以下方法:
- (UIImage *) imageFromMemoryCacheForKey:(NSString *) key;
- (UIImage *) imageFromDiskCacheForKey:(NSString *) key;
而如果只是想查看本地是否在key指定的图片,则不管是在内存还是在磁盘上,则可以使用以下方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
  ...
  // 1. 首先查看内存缓存,如果查找到,则直接回调doneBlock并返回
  UIImage *image = [self imageFromDiskCacheForKey:key];
  if (image) {
    doneBlock(image, SDImageCacheTypeMemory);
    return nil;
  }
  // 2. 如果内存中没有,则在磁盘中查找。如果找到,则将其放到内存缓存,并调用doneBlock回调
  NSOperation *operation = [NSOperation new];
  dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
      return;
    }
    @autoreleasepool {
      UIImage *diskImage = [self diskImageForKey:key];
      if (diskImage) {
        CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
        [self.memCache setObject:diskImage forKey:key cost:cost];
      }
      dispatch_async(dispatch_get_main_queue(), ^{
        doneBlock(diskImage, SDImageCacheTypeDisk);
      });
    }
  });
  return operation;
}

移除图片
图片的移除操作则可以使用以下方法:
- (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)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
而部分清理则是根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。清理的操作在-cleanDiskWithCompletionBlock:方法中,其实现如下:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
  dispatch_async(self.ioQueue, ^{
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    // 1. 该枚举器预先获取缓存文件的有用的属性
    NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                           includingPropertiesForKeys:resourceKeys
                                    options:NSDirectoryEnumerationSkipsHiddenFiles
                                 errorHandler:NULL];
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
    NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
    NSUInteger currentCacheSize = 0;
    // 2. 枚举缓存文件夹中所有文件,该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
      NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
      // 3. 跳过文件夹
      if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
        continue;
      }
      // 4. 移除早于有效期的老文件
      NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
      if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue; 
      }
      // 5. 存储文件的引用并计算所有文件的总大小,以备后用
      NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
      currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
      [cacheFiles setObject:resourceValues forKey:fileURL];
    }
    for (NSURL *fileURL in urlsToDelete) {
      [_fileManager removeItemAtURL:fileURL error:nil];
    }
    // 6.如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,我们首先删除最老的文件
    if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
      // 7. 以设置的最大缓存大小的一半作为清理目标
      const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
      // 8. 按照最后修改时间来排序剩下的缓存文件
      NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                              usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                              }];
      // 9. 删除文件,直到缓存总大小降到我们期望的大小
      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();
      });
    }
  });
}

小结
以上分析了图片缓存操作,当然,除了上面讲的几个操作,SDImageCache类还提供了一些辅助方法。如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片。另外,SDImageCache类提供了一个单例方法的实现,所以我们可以将其当作单例对象来处理。

参考文献:http://southpeak.github.io/blog/2015/02/07/sourcecode-sdwebimage/ 



Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐