zoukankan      html  css  js  c++  java
  • 【原】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如果网络不好的话,文章对应图片加载不出来,就会显示带有“暂无图片”的图片)。

    其中的图片如何获取,如何缓存等等都屏蔽了。甚至没有暴露从网络端获取到的是什么图片,当然后面我们会提到SDWebImage中有其他的借口会暴露返回的图片image,允许在image上操作后再赋值给imageView。

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

    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 { // 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函数中。很简单,只有三行,我直接贴代码了:

    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源码阅读(二)

  • 相关阅读:
    [算法] 堆栈
    [刷题] PTA 02-线性结构3 Reversing Linked List
    java IO流 (八) RandomAccessFile的使用
    java IO流 (七) 对象流的使用
    java IO流 (六) 其它的流的使用
    java IO流 (五) 转换流的使用 以及编码集
    java IO流 (四) 缓冲流的使用
    java IO流 (三) 节点流(或文件流)
    java IO流 (二) IO流概述
    java IO流 (一) File类的使用
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5010303.html
Copyright © 2011-2022 走看看