【原】SDWebImage源码阅读(一)
【原】SDWebImage源码阅读(一)
本文转载请注明出处 —— polobymulberry-博客园
1. 前言
一直没有系统地读过整套源码,就感觉像一直看零碎的知识点,没有系统读过一本专业经典书籍一样,会有点发虚,感觉知识体系不健全!废话少说,这次我决定好好阅读下SDWebImage的源码,我的阅读方式,是带着问题去阅读源码,然后强迫自己写博客。
2. SDWebImage是做什么的?
既然是要带着问题读,那么第一个问题就来了,SDWebImage是做什么的?SDWebImage是一个开源的代码库,我们可以在github上找到它 —> Github传送门。
Github上是这样介绍它的:
This library provides a category for UIImageView with support for remote images coming from the web.
所以我们大概知道SDWebImage就是一个库。这个库本质是UIImageView的category。为啥要做这个category呢?是为了从服务器端远程获取图片到UIImageView上显示。当然,看完代码后,就知道SDWebImage提供的功能远不止说的这么简单。
3. SDWebImage怎么用?
github上也给了一些例子,我们看一下最常用的一个例子:
#import <SDWebImage/UIImageView+WebCache.h>
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided sd_setImageWithURL: method to load the web image
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
cell.textLabel.text = @"My Text";
return cell;
}
这确实是一个很常见的需求,就是在一个tableView上,每一个cell都需要显示网络端获取的image。比如我们常用的新浪微博、网易新闻、知乎日报等等,都会用到。
这里最关键的一行代码就是:
// Here we use the new provided sd_setImageWithURL: method to load the web image
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
看到这里,我情不自禁地要赞叹两句,这个接口设计的真的很棒!你想想,我要从网络端获取图片,并显示到UIImageView上,其实我只要给你一个图片的url就ok啦,另外当前图片如果还未获取到,怎么办?弄个placeholderImage呗(当网络端图片还未加载完成,作为一个替代的图片,比如一些app如果网络不好的话,文章对应图片加载不出来,就会显示带有“暂无图片”的图片)。
其中的图片如何获取,如何缓存等等都屏蔽了。甚至没有暴露从网络端获取到的是什么图片,当然后面我们会提到SDWebImage中有其他的借口会暴露返回的图片image,允许在image上操作后再赋值给imageView。
细想下其中的过程,我大体有一个简单的实现概念(先自己想想怎么实现,然后对照实际源码,这样才能看到自己不足):
- 先将UIImageView的image设为placeholderImage
然后发出网络请求,获取图片image
如果图片获取成功,赋值给UIImageView
带着我这简陋的想法,我模仿SDWebImage写了如下代码:
首先我创建了一个UIImageView的category —— UIImageView+Extension.h
主要是模仿SDWebImage的 sd_setImageWithURL:placeholderImage:函数写了一个pjx_setImageWithURL:placeholderImage:
- UIImageView+Extension.h
#import <UIKit/UIKit.h> @interface UIImageView (Extension) - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage; @end
- UIImageView+Extension.m
#import "UIImageView+Extension.h" @implementation UIImageView (Extension) - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage
{
// 1.先将UIImageView的image设为placeholderImage
self.image = placeholderImage; // 2.然后发出网络请求,获取图片image
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
UIImage *image = [UIImage imageWithData:imageData]; // 3.如果图片获取成功,赋值给UIImageView
if (image) {
self.image = image;
}
} @end
ViewController调用代码:
#import "ViewController.h"
#import "UIImageView+Extension.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end @implementation ViewController #pragma mark - life cycle
- (void)viewDidLoad {
[super viewDidLoad]; NSString *baiduLogoString = @"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png"; [self.imageView pjx_setImageWithURL:[NSURL URLWithString:baiduLogoString] placeholderImage:[UIImage imageNamed:@"placeholderImage"]];
} @end
效果如下:
在没有网络(左)和有网络(右)情况下的对比图

然后我喜滋滋地去看SDWebImage的sd_setImageWithURL:placeholderImage:实现,结果~~他居然调用的是另外一个巨多参数的函数:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
大概猜下,除了url和placeholder两个参数懂是什么意思,options不明白,progress肯定表示的是下载的进度,也就是正在下载时候所要处理的事情(block),那么completed应该表示的是下载完成后所要做的事(block)。
于是我定位到了该函数,发现自己完全不是一个级别上的,看不懂。不过我还不死心,于是我全局搜索dataWithContentsOfURL,嗯,SDWebImage居然没有用!好吧,先不管了,不用就不用,那你总得给UIImageView的image赋值吧,而且肯定要赋值一次placeholderImage和网络请求得到的image吧。果然找到了,就在上面那个巨多参数的函数中。我只截取了部分代码
......
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
.....
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
...
可以看到这里实现了三处image的赋值。并且后面两处赋值后立即使用setNeedsLayout来进行刷新(我注释了刷新代码,好像没有发生什么问题,不过这里还是注意一下,肯定是某个情形下会发生无法自动刷新图片的情况,才要手动刷新)。好的,这里我们可以停一下,看看这些image赋值都是发生在什么情况下的。
这几处赋值都出现了SDWebImageDelayPlaceholder。看下它的注释,首先,它是一个SDWebImageOptions枚举值,而参数options也是一个枚举类型的变量,注定两者是好基友了。话说回来,SDWebImageDelayPlaceholder表示的是什么呢?看注释:
/**
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
*/
翻译过来就是,默认情况下,当正在加载网络端的image 时,placeholder已经加载到了UIImageView,这个枚举项就是为了避免这种默认情况,他将延迟placeholder的加载直到网络端的image加载完成。可能有些抽象,看代码就行了。
在还没发送请求获取网络端图片之前(即网络端的image还没加载),如果options中有SDWebImageDelayPlaceholder这一选项,就不给image赋值,如果没有这一项,那么就给image赋值placeholder。说白了就是下面这段代码:
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
其中dispatch_main_async_safe就是SDWebImage定义的一个宏,很好理解:如果当前是主进程,就直接执行block,否则把block放到主进程运行。为什么要判断是否是主进程?因为iOS上任何UI的操作都在主线程上执行,所以主进程还有一个名字,叫做“UI进程”。
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
后面我们看到有一处代码,表示即使options中有SDWebImageDelayPlaceholder这一选项,也给image赋值placeholder,为啥了?因为此时image已经从网络端加载过了,但是网络端获取image没成功,此时才会用placeholder来替代,赤裸裸的备胎,有代码为证。
else { // image已经尝试获取过了,但是没有从网络端获取到
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
而else上面的else if那段代码,就是表示image从网络获取成功,直接赋值给image。
哈哈,不知道你们会不会有疑惑,你怎么知道此处表示向网络获取image的,也就是注释中说的the image is loading?~~我猜的,不过我猜的没错的话,这段获取的代码既然整体赋值给了id <SDWebImageOperation> operation,那可能是为了多任务(多个图片加载),为什么呢?我怀疑SDWebImageOperation是一个NSOperation子类(这样才能放到NSOperationQueue中进行多任务嘛)。你们肯定说我是SB,这一看就是一个protocol嘛!确实是我猜错了,但是我隐约觉得既然叫Operation,不跟NSOperation有点关系也说不清啊,或者它可能模仿了NSOperation的多任务运行方式。以上都是猜测,我们还是来看代码(后面会揭秘)。
以上的代码(还有几处没说,但是涉及到什么SDImageCacheType还有其他的,暂时不去想)封装成的operation作为参数放到了sd_setImageLoadOperation中。我们接着跳到sd_setImageLoadOperation函数中。很简单,只有三行,我直接贴代码了:
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
虽然很多变量和函数不认识,但是我们大概也能猜到这三行做了什么。我先看[self operationDictionary],具体定义不要看,我们知道它是一个NSMutableDictionary即可,而且既然叫operationDictionary,那么存放的一定是各种operation的序列了(当然也就包括SDWebImageOperation类型的operation),而且这些operation是根据key来索引的。好的,我们回到函数中。一进函数,先取消索引为key的operation的操作,有些人说,如果我之前正在进行索引为key的操作,那不就取消了嘛?是啊,就是这样,如果该operation存在,就取消掉了,还要删除这个key对应的object(operation)。然后重新设置key对应的operation。我们可以看看函数sd_cancelImageLoadOperationWithKey。(这一段文字我解释得不好,下面评论区有详细解释)。
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
代码也很容易理解,先获取到operation的序列,即[self operationDictionary]。然后根据key来索引到对应的operation,如果operation存在的话。就要取消该operation。这里有一个注意的地方,也是我之前没想到的,就是索引到的operation其实一组operation的集合,那么就需要来个遍历一个个取消掉operation序列中的operation了。最后移除key对应的object。
这里我有个疑惑:为啥operation都是id<SDWebImageOperation>?而且,你们也注意到了SDWebImageOperation只有一个cancel接口。为什么要这样设计,还有待进一步研究。
我们还是回到sd_setImageWithURL这个函数中,现在我们有个大概思路了。我们来看看我们能够理解的部分:

未完待续,请君移步【原】SDWebImage源码阅读(二)。
【原】SDWebImage源码阅读(一)的更多相关文章
- 【原】SDWebImage源码阅读(五)
[原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲 ...
- 【原】SDWebImage源码阅读(四)
[原]SDWebImage源码阅读(四) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 SDWebImage中主要实现了NSURLConnectionDataDelega ...
- 【原】SDWebImage源码阅读(三)
[原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们 ...
- 【原】SDWebImage源码阅读(二)
[原]SDWebImage源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 解决上一篇遗留的坑 上一篇中对sd_setImageWithURL函数简单分析了一下,还 ...
- SDWebImage 源码阅读分享
SDWebImage 源码阅读分享 疑问列表 SDWebImage 整体框架图,主要的类包含哪些 SDWebImage 如何进行缓存管理,过期失效策略,缓存更新 SDWebImage 如何多线程处理的 ...
- SDWebImage源码阅读-第三篇
这一篇讲讲不常用的一些方法. 1 sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: com ...
- SDWebImage源码阅读-第一篇
一 题外话 之前写过一篇最新版SDWebImage的使用,也简单的介绍了一下原理.这两天正梳理自己的知识网络,觉得有必要再阅读一下源码,一是看具体实现,二是学习一下优秀开源代码的代码风格,比如接口设计 ...
- (原)NSQ源码阅读和分析(1)
原文出处:https://www.cnblogs.com/lihaiping/p/12324371.html 本文记录自己在阅读和学习nsq源码的时候的一些学习笔记,主要目的是个人总结和方便后期查阅. ...
- SDWebImage源码阅读-第二篇
一 SDWebImageManager的downloadImageWithURL的方法 上一篇,我们刚开了个头,分析了一下开始加载图片之前如何取消其他正在下载的任务,接着,我们回到 - (void) ...
随机推荐
- [APUE]进程控制(上)
一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstr ...
- CSS 3学习——animation动画
以下内容根据官方文档翻译以及自己的理解整理. 1. 介绍 本方案介绍动画(animations).通过动画,开发者可以将CSS属性值的变化指定为一个随时间变化的关键帧(keyframes)的集合.在 ...
- ArcGIS 10.0紧凑型切片读写方法
首先介绍一下ArcGIS10.0的缓存机制: 切片方案 切片方案包括缓存的比例级别.切片尺寸和切片原点.这些属性定义缓存边界的存在位置,在某些客户端中叠加缓存时匹配这些属性十分重要.图像格式和抗锯齿等 ...
- .NET里简易实现AOP
.NET里简易实现AOP 前言 在MVC的过滤器章节中对于过滤器的使用就是AOP的一个实现了吧,时常在工作学习中遇到AOP对于它的运用可以说是很熟练了,就是没想过如果自己来实现的话是怎么实现的,性子比 ...
- Android业务组件化之现状分析与探讨
前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一 ...
- 【开源】.Net 分布式服务中心
分布式服务中心 开源地址: http://git.oschina.net/chejiangyi/Dyd.BaseService.ServiceCenter 当垂直应用越来越多,应用之间交互不可避免,将 ...
- 云瓣影音网站&&微信端(已开源)
随着该项目的发布到线上(小打小闹),即将又要开启另一段崭新的旅程.强迫自己停下来写写所学所得,个人认为总结和分享是一种很棒的学习方式.那让我们先来瞧瞧项目长的什么样.如果着急要源码的朋友,可以下拉到最 ...
- winform异步加载数据到界面
做一个学习记录. 有两个需求: 1.点击按钮,异步加载数据,不卡顿UI. 2.把获取的数据加载到gridview上面. 对于需求1,2,代码如下: public delegate void ShowD ...
- html中返回上一页的各种写法【转】
超链接返回上一页代码: <a href="#" onClick="javascript :history.back(-1);">返回上一页</ ...
- SQL SERVER导入数据到ORACLE的方法总结
我们偶尔会有将数据从SQL SERVER导入到ORACLE当中的这种需求,那么这种跨数据库导数有那些方法呢?这些方法又有那些利弊呢? 下面比较肤浅的总结了一些可行的方法. 1:生成SQL脚本然后去OR ...