原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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
提供了 UIImageView
、UIButton
、MKAnnotationView
的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。
2、特性
- 提供
UIImageView
、UIButton
、MKAnnotationView
的分类,用来显示网络图片,以及缓存管理 - 异步下载图片
- 异步缓存(内存+磁盘),并且自动管理缓存有效性
- 后台图片解压缩
- 同一个
URL
不会重复下载 - 自动识别无效
URL
,不会反复重试 - 不阻塞主线程
- 高性能
- 使用
GCD
和ARC
- 支持多种图片格式
- 支持动图(
GIF
),基于 FLAnimatedImage加载动图
3、常见问题
问题 1:使用 UITableViewCell
中的 imageView
加载不同尺寸的网络图片时会出现尺寸缩放问题。
解决方案:自定义 UITableViewCell
,重写-layoutSubviews
方法,调整位置尺寸;或者直接弃用 UITableViewCell
的imageView
,自己添加一个 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 | 用于获取图片数据的格式(JPEG 、PNG 等) |
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
单例开启图片加载任务operation
,SDWebImageManager
的图片加载方法中会返回一个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:
方法去下载,先将传入的 progressBlock
和 completedBlock
保存起来,并在第一次下载该 URL
的图片时,创建一个 NSMutableURLRequest
对象和一个 SDWebImageDownloaderOperation
对象,并将该SDWebImageDownloaderOperation
对象添加到 SDWebImageDownloader
的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 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
来保持 NSURLConnection
在start
后、收到响应前不被干掉,下载图片时,监听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
再回调给 SDWebImageManager
,SDWebImageManager
中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageView
,UIImageView
中再回到主线程设置 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
图的?
四、实现细节
Demo
Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo
网友评论