【原】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上也给了一些例子,我们看一下最常用的一个例子:

 1 #import <SDWebImage/UIImageView+WebCache.h>
 2
 3 ...
 4
 5 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 6     static NSString *MyIdentifier = @"MyIdentifier";
 7
 8     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
 9     if (cell == nil) {
10         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
11                                        reuseIdentifier:MyIdentifier] autorelease];
12     }
13
14     // Here we use the new provided sd_setImageWithURL: method to load the web image
15     [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
16                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
17
18     cell.textLabel.text = @"My Text";
19     return cell;
20 }

这确实是一个很常见的需求,就是在一个tableView上,每一个cell都需要显示网络端获取的image。比如我们常用的新浪微博、网易新闻、知乎日报等等,都会用到。

这里最关键的一行代码就是:

1     // Here we use the new provided sd_setImageWithURL: method to load the web image
2     [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
3                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

看到这里,我情不自禁地要赞叹两句,这个接口设计的真的很棒!你想想,我要从网络端获取图片,并显示到UIImageView上,其实我只要给你一个图片的url就ok啦,另外当前图片如果还未获取到,怎么办?弄个placeholderImage呗(当网络端图片还为加载完成,作为一个替代的图片,比如一些app如果网络不好的话,文章对应图片加载不出来,就会显示带有“暂无图片”的图片)。

其中的图片如何获取,如何缓存等等都屏蔽了。甚至没有暴露从网络端获取到的是什么图片。

细想下其中的过程,我大体有一个简单的实现概念(先自己想想怎么实现,然后对照实际源码,这样才能看到自己不足):

  1. 先将UIImageView的image设为placeholderImage
  2. 然后发出网络请求,获取图片image
  3. 如果图片获取成功,赋值给UIImageView

带着我这简陋的想法,我模仿SDWebImage写了如下代码:

首先我创建了一个UIImageView的category —— UIImageView+Extension.h

主要是模仿SDWebImage的 sd_setImageWithURL:placeholderImage:函数写了一个pjx_setImageWithURL:placeholderImage:

- UIImageView+Extension.h

1 #import <UIKit/UIKit.h>
2
3 @interface UIImageView (Extension)
4
5 - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage;
6
7 @end

- UIImageView+Extension.m

 1 #import "UIImageView+Extension.h"
 2
 3 @implementation UIImageView (Extension)
 4
 5 - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage
 6 {
 7     // 1.先将UIImageView的image设为placeholderImage
 8     self.image = placeholderImage;
 9
10     // 2.然后发出网络请求,获取图片image
11     NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
12     UIImage *image = [UIImage imageWithData:imageData];
13
14     // 3.如果图片获取成功,赋值给UIImageView
15     if (image) {
16         self.image = image;
17     }
18 }
19
20 @end

ViewController调用代码:

 1 #import "ViewController.h"
 2 #import "UIImageView+Extension.h"
 3
 4 @interface ViewController ()
 5
 6 @property (weak, nonatomic) IBOutlet UIImageView *imageView;
 7
 8 @end
 9
10 @implementation ViewController
11
12 #pragma mark - life cycle
13 - (void)viewDidLoad {
14     [super viewDidLoad];
15
16     NSString *baiduLogoString = @"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png";
17
18     [self.imageView pjx_setImageWithURL:[NSURL URLWithString:baiduLogoString] placeholderImage:[UIImage imageNamed:@"placeholderImage"]];
19 }
20
21 @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吧。果然找到了,就在上面那个巨多参数的函数中。我只截取了部分代码

 1 ......
 2 if (!(options & SDWebImageDelayPlaceholder)) {
 3     dispatch_main_async_safe(^{
 4         self.image = placeholder;
 5     });
 6 }
 7 .....
 8 else if (image) {
 9     wself.image = image;
10     [wself setNeedsLayout];
11 } else {
12     if ((options & SDWebImageDelayPlaceholder)) {
13         wself.image = placeholder;
14         [wself setNeedsLayout];
15     }
16 }
17 ...

可以看到这里实现了两类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。说白了就是下面这段代码:

1 if (!(options & SDWebImageDelayPlaceholder)) {
2     dispatch_main_async_safe(^{
3         self.image = placeholder;
4     });
5 }

其中dispatch_main_async_safe就是SDWebImage定义的一个宏,很好理解:如果当前是主进程,就直接执行block,否则把block放到主进程运行。为什么要判断是否是主进程?因为iOS上任何UI的操作都在主线程上执行,所以主进程还有一个名字,叫做“UI进程”。

1 #define dispatch_main_async_safe(block)2     if ([NSThread isMainThread]) {3         block();4     } else {5         dispatch_async(dispatch_get_main_queue(), block);6     }

后面我们看到有一处代码,表示即使options中有SDWebImageDelayPlaceholder这一选项,也给image赋值placeholder,为啥了?因为此时image已经从网络端加载过了,但是网络端获取image没成功,此时才会用placeholder来替代,赤裸裸的备胎,有代码为证。

else { // 没有从网络端获取到图片
    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函数中。很简单,只有三行,我直接贴代码了:

1 - (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
2     [self sd_cancelImageLoadOperationWithKey:key];
3     NSMutableDictionary *operationDictionary = [self operationDictionary];
4     [operationDictionary setObject:operation forKey:key];
5 }

虽然很多变量和函数不认识,但是我们大概也能猜到这三行做了什么。我先看[self operationDictionary],具体定义不要看,我们知道它是一个NSMutableDictionary即可,而且既然叫operationDictionary,那么存放的一定是各种operation的序列了(当然也就包括SDWebImageOperation类型的operation),而且这些operation是根据key来索引的。好的,我们回到函数中。一进函数,先取消索引为key的operation的操作,有些人说,如果我之前正在进行索引为key的操作,那不就取消了嘛?是啊,就是这样,如果该operation存在,就取消掉了,还要删除这个key对应的object(operation)。然后重新设置key对应的operation。我们可以看看函数sd_cancelImageLoadOperationWithKey。

 1 - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
 2     // Cancel in progress downloader from queue
 3     NSMutableDictionary *operationDictionary = [self operationDictionary];
 4     id operations = [operationDictionary objectForKey:key];
 5     if (operations) {
 6         if ([operations isKindOfClass:[NSArray class]]) {
 7             for (id <SDWebImageOperation> operation in operations) {
 8                 if (operation) {
 9                     [operation cancel];
10                 }
11             }
12         } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
13             [(id<SDWebImageOperation>) operations cancel];
14         }
15         [operationDictionary removeObjectForKey:key];
16     }
17 }

代码也很容易理解,先获取到operation的序列,即[self operationDictionary]。然后根据key来索引到对应的operation,如果operation存在的话。就要取消该operation。这里有一个注意的地方,也是我之前没想到的,就是索引到的operation其实一组operation的集合,那么就需要来个遍历一个个取消掉operation序列中的operation了。最后移除key对应的object。

这里我有个疑惑:为啥operation都是id<SDWebImageOperation>?而且,你们也注意到了SDWebImageOperation只有一个cancel接口。为什么要这样设计,还有待进一步研究。

我们还是回到sd_setImageWithURL这个函数中,现在我们有个大概思路了。我们来看看我们能够理解的部分:

未完待续,请君移步【原】SDWebImage源码阅读(二)

时间: 2024-10-22 04:10:28

【原】SDWebImage源码阅读(一)的相关文章

【原】SDWebImage源码阅读(三)

[原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们来到SDWebImageDownloader.m文件中,找到downloadImageWithURL函数.发现代码不是很长,那就一行行读.毕竟这个函数大概做什么我们是知道的.这个函数大概就是创建了一个SDWebImageSownloader的异步下载器,根据给定的URL下载image. 先映入眼帘的

【原】SDWebImage源码阅读(五)

[原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲解缓存.之前我们也遇到一些缓存的属性和方法,比如storeImage.queryDiskCacheForKey.memCache等等. SDWebImage的缓存分为两个部分,一个内存缓存,使用NSCache实现,另一个就是硬盘缓存(disk),使用NSFileManager实现. 不过这么多函数,

【原】SDWebImage源码阅读(二)

[原]SDWebImage源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 解决上一篇遗留的坑 上一篇中对sd_setImageWithURL函数简单分析了一下,还留了一些坑.不过因为我们现在对这个函数有一个大概框架了,我们就按顺序一个个来解决. 首先是这一句代码: objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 就是给UIImageVi

SDWebImage源码阅读-第三篇

这一篇讲讲不常用的一些方法. 1 sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: completed: 取得上次缓存的图片,然后作为占位图的参数再次进行一次图片设置. - (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options

SDWebImage源码阅读(四)SDWebImageDecoder

一般我们都是使用: 1 + (nullable UIImage *)imageNamed:(NSString *)name; // load from main bundle 和: 1 + (nullable UIImage *)imageWithContentsOfFile:(NSString *)path; 两种方式加载图片,它们两个的区别在SDWebImage源码阅读前的准备(三)UIImage.h 里面的 "(六):加载和创建UIImage 的类方法和实例方法:"部分有详细的介

SDWebImage源码阅读(三)UIImage+GIF

UIImage+GIF 是UIImage 类的一个GIF 分类,在之前的版本里面这个分类是用了处理GIF 动态图片的但是会有内存暴增的bug.在当前 '4.0.0-beta2' 的版本里GIF 动态图片处理放在了UIImage+MultiFormat  这个分类里面,而当前这个GIF 的分类的功能只是将动态图片作为静态图片来处理,如果是静态图片的NSData 数据则转化为静态UIImage 直接返回,如果是动态图片的NSData 数据,则把图像的第1帧图像转换化为静态UIImage 返回. 首先

SDWebImage源码阅读(二)NSData+ImageContentType

NSData+ImageContentType 是NSData 的分类(Category). 创建分类的步骤: 在Xcode 工程页面,按command + N ,在iOS -> Source 选择Objective-C File ,点击Next ,File Type 选择 Category ,file 框里输入分类名字,Calss 为要添加分类的类,可以是系统的类也可以是自定义的类,当然这里出现的主要是系统的类. 分类的描述: 无论一个类设计的如何完美,都不可避免的会遇到没有预测到的需求,那怎

SDWebImage源码阅读前的准备(一)预处理条件编译

阅读前的扩展: 条件编译:#ifdef #elif #ifndef #if #else #endif 的用法,及 #define #undef 的用法. #ifdef/#elif/#ifndef/#if/#else/#endif 这几个条件编译指令是在进行条件编译的时候使用的. 学习条件编译首先对C语言的预处理进行学习,C语言由源代码生成的各阶段如下: C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件 其中 编译预处理阶段,读取c源程序,

SDWebImage源码阅读(五)SDImageCacheConfig/SDImageCache(下)

在上篇中已经了解分析了 SDImageCache.h 文件中所有的方法和属性.大概对 SDImageCache 能实现的功能已经有了全面的认识.在这篇则着重学习研究这些功能的实现过程和实现原理. SDImageCache  是 SDWebImage 里面用来做缓存的类,虽然只是针对的图片的缓存,但是其实在 iOS 开发甚至在程序开发中,缓存类对缓存文件的类型区别而要单独针对文件类型做处理的需要并不多,不管是图片的缓存还是别的类型文件的缓存,它们在缓存原理上是基本一致的.通过对 SDImageCa