IOS框架:SDWeblmage(上)

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

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

目录

一、简介

1、设计目的

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

2、特性

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、目录结构

类名 功能
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);
            }
        });
    }
}

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

步骤四

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

参考文献

上一篇下一篇

猜你喜欢

热点阅读