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

IOS框架:SDWeblmage(上)

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

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

    目录

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

    一、简介

    1、设计目的

    SDWebImage提供了 UIImageViewUIButtonMKAnnotationView的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。

    2、特性

    • 提供 UIImageViewUIButtonMKAnnotationView的分类,用来显示网络图片,以及缓存管理
    • 异步下载图片
    • 异步缓存(内存+磁盘),并且自动管理缓存有效性
    • 后台图片解压缩
    • 同一个 URL 不会重复下载
    • 自动识别无效 URL,不会反复重试
    • 不阻塞主线程
    • 高性能
    • 使用 GCDARC
    • 支持多种图片格式
    • 支持动图(GIF),基于 FLAnimatedImage加载动图

    3、常见问题

    问题 1:使用 UITableViewCell中的 imageView 加载不同尺寸的网络图片时会出现尺寸缩放问题。

    解决方案:自定义 UITableViewCell,重写-layoutSubviews方法,调整位置尺寸;或者直接弃用 UITableViewCellimageView,自己添加一个 imageView 作为子控件。

    问题 2:图片刷新问题。SDWebImage 在进行缓存时忽略了所有服务器返回的 caching control 设置,并且在缓存时没有做时间限制,这也就意味着图片 URL 必须是静态的了,要求服务器上一个URL 对应的图片内容不允许更新。但是如果存储图片的服务器不由自己控制,也就是说图片内容更新了,URL 却没有更新,这种情况怎么办?

    解决方案:在调用sd_setImageWithURL: placeholderImage: options:方法时设置options参数为SDWebImageRefreshCached,这样虽然会降低性能,但是下载图片时会照顾到服务器返回的 caching control

    问题 3:在加载图片时,如何添加默认的 progress indicator

    解决方案:在调用-sd_setImageWithURL:方法之前,先调用下面的方法:

    [imageView sd_setShowActivityIndicatorView:YES];
    [imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];
    

    4、使用方法

    a: UITableView 中使用 UIImageView+WebCache

    UITabelViewCell 中的UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可。

    b: 使用回调 blocks

    block中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调。

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                             completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                    ... completion code here ...
                                 }];
    
    c: SDWebImageManager 的使用

    UIImageView(WebCache)分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。

        SDWebImageManager *manager = [SDWebImageManager sharedManager];
        [manager loadImageWithURL:imageURL
                          options:0
                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                // progression tracking code
                         }
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                            if (image) {
                                // do something with image
                            }
                         }];
    

    SDWebImageManager也可以单独使用。单独使用 SDWebImageDownloader 异步下载图片:

        SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
        [downloader downloadImageWithURL:imageURL
                                 options:0
                                progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                    // progression tracking code
                                }
                               completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                    if (image && finished) {
                                        // do something with image
                                    }
                                }];
    

    SDImageCache 支持内存缓存和异步的磁盘缓存(可选),如果你想单独使用 SDImageCache 来缓存数据的话,可以使用单例:

    // 添加缓存的方法:
        [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
        
    // 默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:
        [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
        
    // 读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。
        SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
        [imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
            // image is not nil if image was found
        }];
    
    d: 自定义缓存 key

    有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的key

        SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
                url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
                return [url absoluteString];
            };
    

    二、实现原理

    1、架构图(UML 类图)

    架构图(UML 类图)

    2、流程图(方法调用顺序图)

    流程图(方法调用顺序图)

    3、目录结构

    • Downloader
      • SDWebImageDownloader
      • SDWebImageDownloaderOperation
    • Cache
      • SDImageCache
    • Utils
      • SDWebImageManager
      • SDWebImageDecoder
      • SDWebImagePrefetcher
    • Categories
      • UIView+WebCacheOperation
      • UIImageView+WebCache
      • UIImageView+HighlightedWebCache
      • UIButton+WebCache
      • MKAnnotationView+WebCache
      • NSData+ImageContentType
      • UIImage+GIF
      • UIImage+MultiFormat
      • UIImage+WebP
    • Other
      • SDWebImageOperation(协议)
      • SDWebImageCompat(宏定义、常量、通用函数)
    类名 功能
    SDWebImageDownloader 是专门用来下载图片和优化图片加载的,跟缓存没有关系
    SDWebImageDownloaderOperation 继承于 NSOperation,用来处理下载任务的
    SDImageCache 用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程
    SDWebImageManager 作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来
    SDWebImageDecoder 图片解码器,用于图片下载完成后进行解码
    SDWebImagePrefetcher 预下载图片,方便后续使用,图片下载的优先级低,其内部由 SDWebImageManager来处理图片下载和缓存
    UIView+WebCacheOperation 用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
    UIImageView+WebCache 集成 SDWebImageManager的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用
    UIImageView+HighlightedWebCache UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片
    UIButton+WebCache UIImageView+WebCache 类似,集成 SDWebImageManager的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用
    MKAnnotationView+WebCache UIImageView+WebCache 类似
    NSData+ImageContentType 用于获取图片数据的格式(JPEGPNG等)
    UIImage+GIF 用于加载 GIF 动图
    UIImage+MultiFormat 根据不同格式的二进制数据转成 UIImage 对象
    UIImage+WebP 用于解码并加载 WebP图片

    4、核心逻辑

    步骤一

    运行pod install,然后打开 SDWebImage.xcworkspace,先run 起来感受一下。

    这里有个很恶心的问题,执行 pod install 报错Error installing libwebp,需要翻墙来安装。但我平时用不上VPN,买一个又贵纯属浪费。网上的解决办法是这样的:
    查看cocoapods 本地库路径: pod repo

    cocoapods
    - Type: git (master)
    - URL:  https://github.com/CocoaPods/Specs.git
    - Path: /Users/xiejiapei/.cocoapods/repos/cocoapods
    

    找到libwebp的文件夹:find ~/.cocoapods/repos/cocoapods -iname libwebp

    /Users/xiejiapei/.cocoapods/repos/cocoapods/Specs/1/9/2/libwebp
    

    查看libwebp下版本:

    xiejiapei@xiejiapeis-iMac ~ % cd /Users/xiejiapei/.cocoapods/repos/cocoapods/Specs/1/9/2/libwebp
    
    xiejiapei@xiejiapeis-iMac libwebp % ls -l
    total 0
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.4.1
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.4.2
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.4.3
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.4.4
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.5.0
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.5.1
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.5.2
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.6.0
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 0.6.1
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 1.0.0
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 1.0.1
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 1.0.2
    drwxr-xr-x  3 xiejiapei  staff   96 Dec 24  2019 1.0.3
    drwxr-xr-x  4 xiejiapei  staff  128 Aug  5 14:44 1.1.0
    drwxr-xr-x  3 xiejiapei  staff   96 Aug  5 15:57 1.1.0-rc2
    

    cd到1.1.0版本,修改libwebp.podspec.json文件中的homepage,source->git

    xiejiapei@xiejiapeis-iMac libwebp % cd 1.1.0
    xiejiapei@xiejiapeis-iMac 1.1.0 % ls -l
    total 8
    -rw-r--r--@ 1 xiejiapei  staff  1840 Aug  5 14:30 libwebp.podspec.json
    xiejiapei@xiejiapeis-iMac 1.1.0 % sudo vim libwebp.podspec.json 
    

    homepage改为https://github.com/webmproject/,而source->git改为https://github.com/webmproject/libwebp.git

    "name": "libwebp",
      "version": "1.0.0",
      "summary": "Library to encode and decode images in WebP format.",
      "homepage": "https://developers.google.com/speed/webp/",
      "authors": "Google Inc.",
      "license": {
        "type": "BSD",
        "file": "COPYING"
      },
      "source": {
        "git": "https://chromium.googlesource.com/webm/libwebp",
        "tag": "v1.0.0"
      },
    

    修改完就大功告成,接下来cd到工程目录下,执行pod install

    呵呵,经我证实,该方法无效。折腾我一个小时。

    步骤二

    MasterViewController 中的 [cell.imageView sd_setImageWithURL:url placeholderImage:placeholderImage];开始看起。

        [cell.imageView sd_setImageWithURL:[NSURL URLWithString:[_objects objectAtIndex:indexPath.row]]
                          placeholderImage:[UIImage imageNamed:@"placeholder"] options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];
    
    步骤三

    经过层层调用:

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

    直到UIImageView+WebCache中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:

    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
        [self sd_cancelCurrentImageLoad];
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                self.image = placeholder;
            });
        }
        
        if (url) {
            __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) {
                if (!wself) return;
                dispatch_main_sync_safe(^{
                    if (!wself) return;
                    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);
                    }
                });
            }];
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } else {
            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);
                }
            });
        }
    }
    

    该方法中,主要做了以下几件事:

    • 取消当前正在进行的加载任务 operation,设置 placeholder
    • 如果URL 不为nil,就通过SDWebImageManager单例开启图片加载任务 operationSDWebImageManager 的图片加载方法中会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock
    步骤四

    SDWebImageManager的图片加载方法 downloadImageWithURL:options:progress:completed: 中会先拿图片缓存的 key (这个 key 默认是图片 URL)去 SDImageCache 单例中读取内存缓存,如果有,就返回给 SDWebImageManager

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

    如果内存缓存没有,就开启异步线程,拿经过 MD5 处理的 key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存中去,然后再返回给 SDWebImageManager

    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
    
    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:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
    }
    
    
    步骤五

    如果内存缓存和磁盘缓存中都没有,SDWebImageManager 就会调用 SDWebImageDownloader 单例的 -downloadImageWithURL: options: progress: completed:方法去下载,先将传入的 progressBlockcompletedBlock 保存起来,并在第一次下载该 URL的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloaderdownloadQueue 来启动异步下载任务。

    - (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:^{
            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
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
            operation = [[wself.operationClass alloc] initWithRequest:request
                                                              options:options
                                                             progress:^(NSInteger receivedSize, 
            [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;
    }
    
    步骤六

    SDWebImageDownloaderOperation 中包装了一个 NSURLConnection 的网络请求,并通过runloop来保持 NSURLConnectionstart 后、收到响应前不被干掉,下载图片时,监听NSURLConnection 回调的 -connection:didReceiveData:方法中会负责 progress相关的处理和回调,- connectionDidFinishLoading:方法中会负责将data 转为image,以及图片解码操作,并最终回调completedBlock

    - (void)start {
            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);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
            });
    
         CFRunLoopRun();
    
            if (!self.isFinished) {
                [self.connection cancel];
                [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
            }
    }
    
    步骤七

    SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManagerSDWebImageManager 中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageViewUIImageView 中再回到主线程设置 image 属性。至此,图片的下载和缓存操作就圆满结束了。

    三、反思和拓展

    1、使用-[UIApplication beginBackgroundTaskWithExpirationHandler:]方法使 app 退到后台时还能继续执行任务, 不再执行后台任务时,需要调用 -[UIApplication endBackgroundTask:] 方法标记后台任务结束。

    2、文件的缓存有效期及最大缓存空间大小
    默认有效期:

    maxCacheAge = 60 * 60 * 24 * 7; // 1 week
    

    默认最大缓存空间:

    maxCacheSize = <#unlimited#>
    

    3、MKAnnotationView 是用来干嘛的?
    MKAnnotationView 是属于 MapKit 框架的一个类,继承自UIView,是用来展示地图上的annotation 信息的,它有一个用来设置图片的属性 image

    4、图片下载完成后,为什么需要用 SDWebImageDecoder 进行解码?

    5、SDWebImage 中图片缓存的 key是按照什么规则取的?

    6、SDImageCache 清除磁盘缓存的过程?

    7、md5 是什么算法?是用来干什么的?除此之外,还有哪些类似的加密算法?

    8、SDImageCache 读取磁盘缓存是不是就是指从沙盒中查找并读取文件?

    9、UIImageView 是如何通过SDWebImage 加载图片的?

    10、SDWebImage 在设计上有哪些巧妙之处?

    11、假如自己来实现一个图片下载工具,该怎么写?

    图片读写:以图片URL的单向Hash值作为Key
    淘汰策略:以队列先进先出的方式淘汰,LRU算法(如30分钟之内是否使用过)
    磁盘设计:存储方式、大小限制(如100MB )、淘汰策略(如某图片存储时间距今已超过7天)
    网络设计:图片请求最大并发量、请求超时策略、请求优先级
    图片解码:对于不同格式的图片,解码采用什么方式来做? 在哪个阶段做图片解码处理?(磁盘读取后网络请求返回后)

    设计一个简单的图片缓存器

    12、SDWebImage 的性能怎么看?

    13、SDWebImage是如何处理 gif 图的?


    四、实现细节

    见下文 IOS框架:SDWeblmage(下)


    Demo

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

    参考文献

    相关文章

      网友评论

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

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