概述:

SDWebImage类库提供一个UIImageView类别以支持加载来自网络的远程图片。具有缓存管理、异步下载、图片解码、同一个URL下载次数控制和优化等特征。方便使用,为我们节省了大量开发工作。

 

核心方法一:

 1 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
 2     [self sd_cancelCurrentImageLoad];
 3     objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 4 5     if (!(options & SDWebImageDelayPlaceholder)) {
 6         self.image = placeholder;
 7     }
 8     
 9     if (url) {
10         __weak UIImageView *wself = self;
11         id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL, NSData* data) {
12             if (!wself) return;
13             dispatch_main_sync_safe(^{
14                 if (!wself) return;
15                 if (image) {
16                     wself.image = image;
17                     [wself setNeedsLayout];
18                 } else {
19                     if ((options & SDWebImageDelayPlaceholder)) {
20                         wself.image = placeholder;
21                         [wself setNeedsLayout];
22                     }
23                 }
24                 if (completedBlock && finished) {
25                     completedBlock(image, error, cacheType, url, data);
26                 }
27             });
28         }];
29         [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
30     } else {
31         dispatch_main_async_safe(^{
32             NSError *error = [NSError errorWithDomain:@"SDWebImageErrorDomain" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
33             if (completedBlock) {
34                 completedBlock(nil, error, SDImageCacheTypeNone, url, nil);
35             }
36         });
37     }
38 }

解析如下:

1. [self sd_cancelCurrentImageLoad]

取消正在下载的队列,防止重复下载

2.objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

相当于给当前对象增加一个imageURLKey属性,并设置值为url。这句话对于功能上来说不是必须的,只是为了方便获取正在下载图片的url这个值。

3.设置图像的占位符,除非选项里标明延迟设置

4.关键部分了,检测url是否正常。如果不正常,很简单,报错。如果正常的话,开始下载,这里为了防止主线程卡顿,采用了队列。

并将队列加入缓存中:[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad”]。这行代码和第一行相对应,可以在需要取消时进行取消。待下载完成后,进行图像设置,并通知设用方下载结果。这里做了一些安全检查,比如检测自身是否为空。

5. dispatch_main_sync_safe是自己定义的一个宏,从字面意思上也可以看出来,为了安全地跳到主线程上操作,宏定义如下:

#define dispatch_main_sync_safe(block)\

    if ([NSThread isMainThread]) {\

        block();\

    } else {\

        dispatch_sync(dispatch_get_main_queue(), block);\

    }

很简单,判断当前是否在主线程,如果在的话,直接调用,不在的话,先指到主线程上再调用。

 

核心代码二:图片下载器

上面的代码,已经基本满足了我们的使用需要,我们可以不用关心内部的实现细节了,但如果想深入理解的话,可以再研究一下这段代码。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    NSParameterAssert(completedBlock);


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


    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;


    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }


    if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url, nil);
        });
        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, NSData* data) {
        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 bug 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, data);
                });
            }


            // download if no image or requested to refresh anyway, and download allowed by delegate
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (image && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                if (weakOperation.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
                }
                else if (error) {
                    dispatch_main_sync_safe(^{
                        if (!weakOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url, data);
                        }
                    });


                    if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);


                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                            // NOTE: We don't call transformDownloadedImage delegate method on animated images as most transformation code would mangle it
                    else if (downloadedImage && !downloadedImage.images && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];


                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
                            }


                            dispatch_main_sync_safe(^{
                                if (!weakOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url, data);
                                }
                            });
                        });
                    }
                    else {
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }


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


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


    return operation;
}

解析如下:

1.首先做了一堆容错处理,作为被广泛使用的地个框架,良好的容错性是必须要考虑的

2.针对曾经下载失败的图片地址做了缓存(请求超时的除外),下次请求时不再真正发送请求,而是直接报错,这也算是一个优化吧。这个仅在内存中缓存,下次启动app后会清空。

3.根据不同的下载选项,做了对应的下载策略

4.在block处做了weak处理

 

题外话:

从这个框架中,我们可以学到:动态添加属性,二级图片缓存,队列读取或下载等等。平时在一些候选人的简历里,会看到罗列一大堆开源类库,能熟练使用。但这证明不了这个人的能力,恰好说明这些开源类库在设计和实现上都比较不错,有许多值得学习的地方。能够运用是最基本的要求,更多的时候我们还是要多看看里面的代码,了解一下实现原理,这样能够帮我们更好的运用,遇到问题也能够抛根问题找到原因。再深入一些的话,想想从这些源码里有什么收获,是不是可以把里面的一些知识运用到我们自己的开发中。

 

延伸:

https://github.com/rs/SDWebImage

http://blog.cnbang.net/tech/2578/

 

posted on 2016-07-07 15:44  bluev  阅读(809)  评论(0编辑  收藏  举报