美文网首页
SDWebImage

SDWebImage

作者: 学而不思则罔思而不学则殆 | 来源:发表于2016-09-27 19:55 被阅读26次

    简介:

    一个类库,提供一个UIImageView类别以支持加载来自网络的远程图. 具有缓存管理、异步下载、同一个URL下载次数控制和优化等特点.

    流程:

    流程图

    每个文件的作用是什么?

    UIView+WebCache.h

    这个文件是其他视图加载的关键, 最后方法都会跳转到这里面, 它是在 UIView 上面的扩展, 实现视图加载图片的方式. 它和 UIView+WebCacheOperation.h 配合使用. 这个类主要提供了加载图片和不加载图片的 loading.

    • 方法主要是下面
    - (void)sd_internalSetImageWithURL:(nullable NSURL *)url 
                      placeholderImage:(nullable UIImage *)placeholder 
                               options:(SDWebImageOptions)options 
                          operationKey:(nullable NSString *)operationKey 
                         setImageBlock:(nullable SDSetImageBlock)setImageBlock
                              progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDExternalCompletionBlock)completedBlock;
    
    • 参数说明
      url: 图片在服务器上的路径
      placeholder: 图片加载时显示的默认图
      options: 控制图片加载的方式, 之后会详解
      operationKey: 操作 (operation) 的key, 如果为空时, 将使用类名. 这个主要使用来取消一个operation, 结合 UIView+WebCacheOperation.h 使用
      setImageblock: 如果不想使用 SD 加载完图片显示在视图上, 可以使用这个 Blcok 自定义加载图片, 这样就可以在调用加载图片的方法中加载图片. 它的完整定义是:

      typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);
      

      progress: 进度回调, 它的完整意义是下面(注意: 这里有一个 targetURL )

      typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);
      

      completed: 图片加载完成后的回调.

        typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);
      

      例子主要是暂时直接使用 UIView 的扩展加载图片, 且使用 setImageBlock 加载图片. 只要理解这个方法, 那么关于 UIView 加载图片就基本掌握了.

      UIView+WebCacheOperation.h

      这个类用来记录 UIView 加载 Operation 操作, 大多数情况下一个 View 仅拥有一个 Operation. 默认 key 是当前类的类名, 如果设置了不同的 key, 将保存不同的 Operation, 默认的 key 是当前类的类名, 如果设置不同状态下的图片, 那么我需要记录多个 Operation. 它主要采用一个支点来保存所有的 Operation.

      operations = [NSMutableDictionary dictionary];
      objc_setAssociatedObject(self, &loadOperationKey, operations,   OBJC_ASSOCIATION_RETAIN_NONATOMIC);
      

      取消一个 Operation, 这里需要注意 SDWebImageOperation. 取消当前正在进行的 Operation. 这几个类主要是基于以下方法的进一步封装, 这里就不多做介绍了.

      UIImage+GIF.h

      可以根据 NSData 生成一个 GIF 图片和一个判断是否是 GIF 图片的方法.

      UIImage+MultiFormat.h

      可以根据 NSData 生成不同格式的图片, 这里可能我们需要用到的是, 根据 Data 判断图片的格式.

    下载操作

    SDWebImageDownLoaderOperation: NSOperation
    @interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    

    这个文件是整个 SD 的灵魂, 控制着图片下载的过程, 它和 NSOperationQueue 配合使用.
    SDWebImageDownloaderOperationInterface: 这是一个协议, 可以自定义自己的 NSOperation, 只要实现协议中的方法, 并且继承自 NSOperation.
    初始化

    -(nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                                  inSession:(nullable NSURLSession *)session
                                    options:(SDWebImageDownloaderOptions)options 
    NS_DESIGNATED_INITIALIZER;
    

    使用这个方法来创建一个 SDWebImageDownloaderOperation, NS_DESIGNATED_INITIALIZER 这个宏说明所有的初始化方法最终都要调用这个方法, request 就是网络请求的 request, session 当前 Operation 所在的 Session, option: SDWedImageDownLoaderOperation, 如何来下载任务, 有一些枚举值.

    SDWebImageDownloader

    这个类主要负责下载图片, 他是一个单例哦它内部有 SDWebImageDownloadToken, 用来表示一个下载任务, 这样根据 token 来取消对应的任务. 可以使用以下方法对 SDWebImageDownloader 进行初始化.

    - (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
    
    下载方法:
    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
    

    这个方法只要是用来下载一个任务, 下载任务使用的是 NSOperation+NSOperationQueue, 来控制瞎子啊. 也就是说这个方法主要就是生产一个 NSOperation, 并添加到 NSOperationQueue 中, 这样 NSOperationQueue 将自动管理下载任务. 使用 NSOperation 的好处就是可以控制下载的过程, 并且不需要管理线程的创建. 当然他的优点也就是他的缺点, 具体看使用场景.

    • 参数说明:
      url: 参数说明
      options: 图片下载的选项
      progress: 进度回调, 注意这个进度是在后台线程执行, 刷新 UI 需要回到主线程中
      completed: 下载完成后的回掉
      SDWebImageDownloadToken: 返回值用来表示想一个下载任务, 取消的时候使用.
      使用上面的这个方法下载的时候, 前提需要了解下面的这个方法的实现. 它使用一个支点缓存了所有的下载. 使用 SDWebImageDownloadToken 来标记一个下载任务.

    SDImageCache

    SD 中的缓存主要采用了内存缓存(NSCache)加磁盘缓存(保存在沙盒名录中的 Cache 目录下),
    SDImageCacheConfig 主要负责配置缓存
    初始化

    - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                           diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
    
    • 参数说明
      directory:文件说要保存到的沙盒目录, 默认的是 Cache 目录
      ns: 文件的域名, 最终的路径是:.../cache/om.hackemist.SDWebImageCache.ns.
      需要注意的是是所有的 I/O 操作都是在一个串行队列中执行. 这里主要用到了文件的一些操作, 比如文件大小, 保存文件, 文件路径等. 文件保存是主要是以文件的下载路径, MD5后, 加上文件后缀作为文件名, 保存到本地和 NSCache 中.
    • 他监听了3个通知在初始化的时候:
    - UIApplicationDidReceiveMemoryWarningNotification:有内存警告时清除所有的缓存
    - UIApplicationWillTerminateNotification:删除已过期的文件
    - UIApplicationDidEnterBackgroundNotification:在后台删除已过期的文件
    
    • 也可以使用单利创建, 使用默认的配置
    + (nonnull instancetype)sharedImageCache;
    
    SDWebImageManager

    主要用管理 SDImageCache 和 SDWebImageDownloader. 也就是它把缓存和下载结合起来
    初始化:

    - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;
    

    这个是SDWebImageManager 最终的初始化方法, 也就是说所有的初始化方法最终都会调用这个方法, 方便使用自定义 SDWebImageManager, 当然通常情况下使用单利初始化

     + (nonnull instancetype)sharedManager;
    

    实现方法

    - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
        if ((self = [super init])) {
            _imageCache = cache;
            _imageDownloader = downloader;
            // 下载失败的 URL 缓存,注意它使用的是集合,这样保证缓存中没有重复的 URL
            _failedURLs = [NSMutableSet new];
            // 正在运行的操作
            _runningOperations = [NSMutableArray new];
        }
        return self;
    }
    

    下载一个图片的主要方法
    这个方法很长, 慢慢来看.

    - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        // 使用断言来保证完成的 Block 不能为空, 也就是说如果你不需要完成回调, 直接使用 SDWebImagePrefetcher 就行
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        // 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.
        // 保证 URL 是 NSString 类型, 转换成 NSURL 类型
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        // Prevents app crashing on argument type error like sending NSNull instead of NSURL
        // 保证 url 为 NSURL 类型
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
         // 对 url 做异常处理, 是否为不可使用的下载链接. "SDWebImageCombinedOperation" 是一个 NSObject 对象
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
        // 判断是否为下载失败的 url
        BOOL isFailedUrl = NO;
        if (url) {
            // 保证线程安全
            @synchronized (self.failedURLs) {
                isFailedUrl = [self.failedURLs containsObject:url];
            }
        }
        // 如果是失败的 url 且 Operations 不为 SDWebImageRetryFailed, 或者 url 为空直接返回错误
        if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
            return operation;
        }
        // 保存当前的 Operation 到缓存
        @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
        // 获取 url 对应的 key
        NSString *key = [self cacheKeyForURL:url];
        // 从 Cache 中获取图片, 他结合 option, 进行不同的操作
        operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
            // 如果 Operation 已经取消, 则移除, 并结束程序的执行.
            if (operation.isCancelled) {
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            // 如果未能在缓存中找到图片, 或者强制刷新缓存, 或者在代理中没有实现再强制下载图片, 那么它就需要下载.
            if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                if (cachedImage && options & SDWebImageRefreshCached) {
                    // 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.
                    [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                }
    
                // download if no image or requested to refresh anyway, and download allowed by delegate
                // SDWebImageDownloaderOptions 根据不同的选项做不同的操作,根据 SDWebImageOptions 转换成对应的 SDWebImageDownloaderOptions。这里需要注意位运算,根据位运算可以计算出不同的选项。那么使用位定义的枚举和用普通定义的枚举值有什么优缺点?需要读者考虑。比如下面这两种定义方法个的优缺点。
                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 (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
                
                if (cachedImage && 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;
                }
                // 使用 "imageDownloader" 下载图片, 下载完成后保存到缓存, 并移除 Operation. 如果发生错误, 需要将失败的 Url 保存到 failedURLs, 避免失效的 url 多次下载. 这里需要注意一个 delegate (`[self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]`),它需要调用者自己实现,这样缓存中将保存转换后的图片。
                SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, 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
                    } else if (error) {
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
    
                        if (   error.code != NSURLErrorNotConnectedToInternet
                            && error.code != NSURLErrorCancelled
                            && error.code != NSURLErrorTimedOut
                            && error.code != NSURLErrorInternationalRoamingOff
                            && error.code != NSURLErrorDataNotAllowed
                            && error.code != NSURLErrorCannotFindHost
                            && error.code != NSURLErrorCannotConnectToHost) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
                        }
                    }
                    else {
                        if ((options & SDWebImageRetryFailed)) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
                        }
                        
                        BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
    
                        if (options & SDWebImageRefreshCached && cachedImage && !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), ^{
                                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
    
                                if (transformedImage && finished) {
                                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                    // pass nil if the image was transformed, so we can recalculate the data from the image
                                    [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                                }
                                
                                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                            });
                        } else {
                            if (downloadedImage && finished) {
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        }
                    }
    
                    if (finished) {
                        [self safelyRemoveOperationFromRunning:strongOperation];
                    }
                }];
                operation.cancelBlock = ^{
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            } else if (cachedImage) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                [self safelyRemoveOperationFromRunning:operation];
            } else {
                // Image not in cache and download disallowed by delegate
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
                [self safelyRemoveOperationFromRunning:operation];
            }
        }];
    
        return operation;
    }
    
    SDWebImagePrefetcher

    它是一个图片预加载的类, 你可以设置多个 URL. 这种更适合那些, 在 wifi 情况下提前加载一些图片缓存起来, 用户使用的时候, 直接从本地缓存中读取. 实现起来也很简单, 使用一个递归来执行每一个下载. 他的本质使用的是 SDWebImageManager 处理下载, 没有使用单例, 而新创建一个 manager

    初始化:

    -(nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager {
        if ((self = [super init])) {
            _manager = manager;
            _options = SDWebImageLowPriority;
            _prefetcherQueue = dispatch_get_main_queue();
            self.maxConcurrentDownloads = 3;
        }
        return self;
    }
    
    SDWebImagePrefetcherDelegate

    每下载完一个后, 走一次回调

    - (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;
    
    SDWebImageCompat

    SD 会用到不同的平台, 需要做一些兼容性的处理.

    NSData+ImageContentRype

    根据 Data 来解析图片的格式


    SD 使用的知识点总结

    GCD 和 NSOperation

    SD 中的思想

    • 耦合度低, 每个类负责不同的操作, 相互之间可以独立使用
    • 使用扩展, 方便使用者
    • 异步下载图片, 并保存到内存与磁盘, 提高系统性能
    • 保证主线程不被卡顿, 提高性能
    • 通过一个 Manager 来控制不同的操作.

    SD 类图

    SD类图
    • 所有的操作都围绕在 SDWebImageManager;
    • SDWebImageManager 中包含了 SDImageCache 和 SDWebImageDownloader, 来处理图片的下载和缓存
    • SDWebImageDownloader 使用 SDWebImageDownloaderOperation 执行下载操作:
    • SDImageCache 使用 SDImageConfig 来配置缓存
    • 从 SDWebImageManager 衍生出一个预加载图片的类 SDWebImagePrefetcher, 负责多个图片的预先加载
    • 底层封装好通过扩展 UIView 让视图可以加载图片
      看图需要了解 UML (Unified Modeling Language) 类图:
    • 依赖关系 (dependency):
      依赖关系是用一套带箭头的虚线标示的, UIButton (WebCache) 依赖于 UIView (WeCache);
      它是一种临时的关系, 通常在运行期间产生, 并且随着运行时变化: 迎来广西也可能发生变化.显然, 依赖也是有方向性的, 双向依赖是一种非常糟糕的结构, 我们总是应该保持单项依赖, 杜绝双向依赖的产生;
    • 聚合关系 (aggregation):
      聚合关系用一条空心菱形箭头的直线标示, 聚合关系用于标示实体对象之间的关系, 表示整体有部分构成的语义: 列如一个部分由多个员工组成; SDWebImagePrefetcher 由SDWebImageManager 组成;
    • 实现关系 (realize):
      实现关系用一条带空心箭头的虚线表示:比如SDWebImageManager 组成.
    • 实现关系 (realize):
      实现管理用一条带空心箭头的虚线表示: 比如 SDWebImageDownloaderOperation 实现协议 SDWebImageOperation
    • 泛化关系 (generalization):
      泛化关系用一条带空心箭头的实现表示, 他是一种继承关系.

    整体架构

    SDWebImageClassDiagram.png

    使用实例

    • 实例一:使用 UIView 的扩展加载图片,并外部自动设置图片
    [cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
         cell.sdimageView.image = image;
    } progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                
    }];
    
    • 实例二:预加载图片
    [SDWebImagePrefetcher sharedImagePrefetcher].delegate = self;
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:resultUrl progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
            
    } completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
            
    }];
    

    总结

    解耦:模块之间一定不要有太多的关联,我们往往对项目中的某个类做增量操作,不断的给某个类添加新的代码,导致这个类越来越重,我们试着把一个类拆分为不同的功能模块;
    思路明确:从图片的下载到图片显示到视图上,要有明确的思路,先有一个大致的流程,然后逐步细化,逐步实现;
    层次明确:应用层的使用不会印象到底层的设计;
    GCD 和 NSOperation:各有利弊,要合理的使用;
    注意性能:一定要注意性能,结合多线程,提升性能,比如 SD 读取文件时会在一条线程中读取;
    方便使用者:写三方库时,要让用户使用起来超级方便,比如在自己项目中写项目组中公用的模块时,要有明确的注释,让使用这更方便的使用;

    相关文章

      网友评论

          本文标题:SDWebImage

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