SDWebImage源码阅读1——整体脉络结构

前言

SDWebImage作为一个加载图片开源库,在大多数项目里它和一些刷新、弹窗控件一样普及。而且这个东西写得很好,网上已经有很多篇对此库剖析的文章了。正好这段时间有时间,我也花了好多天时间认真阅读了下它的代码。确实感觉受益颇多,因此就专门写篇笔记,一来再次梳理下思路,加深理解。二来也是为方便日后温习,温故而知新。

好了,开始。


分析

我自己画了一张图,展示了SDWebImage在加载图片时的整个脉络结构。先将此图亮出,下面边分析边回过头来对照该图加深理解。

屏幕快照 2016-08-01 下午2.31.38.png

我们一般在在项目中是这样使用它的:

    UIImage *image = [UIImage imageNamed:@"1.jpg"];
    NSString *urlStr = @"http://dn-edu-test.qbox.me/5436557847300726784";
    NSURL *url = [NSURL URLWithString:urlStr];
    
    [_imgV setImageWithURL:url placeholderImage:image];

SDWebImage提供的接口方法非常简单,只需要一个图片的url和一个占位图作为参数而已。而且它的接口方法是以UIImageView的分类的方式提供的,而非通过继承的方式,这样降低了耦合和侵入性,同时也降低了使用者的使用成本,不用在创建UIImageView时继承某类,或使用它封装好的某类。只要在类中导入了该分类的头文件,就可以通过UIImageView的实例对象调用库里接口方法。


UIImageView+WebCache.m

我们从使用者的视角开始,从外向内一层层的分析它的代码。首先我们进入摁住command键的同时点击[_imgV setImageWithURL:url placeholderImage:image];,便进入了UIImageView+WebCache.m分类文件。

#import "UIImageView+WebCache.h"
#import "objc/runtime.h"

// 3个动态添加的属性
static char imageURLKey; // 图片url
static char operationKey;   // 操作
static char operationArrayKey; // 一组操作

@implementation UIImageView (WebCache)

- (void)setImageWithURL:(NSURL *)url {
    [self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options {
    [self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock {
    [self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock {
    [self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock {
    [self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}

这段代码值得说的是,所有暴露在外的接口方法内部其实都是在调用一个参数最多的一个方法,我们暂且将其称为全能方法。这样的话将核心代码封装在一处,不必写冗余饿的代码,且看起来一目了然。
我们自己在写一个自定义控件或者说自己写东西时,提供的初始化方法和功能方法最好都以此种形式写。
另外,该分类声明了三个静态字符,均用于通过运行时动态地给分类添加属性,也叫属性关联。分类中是不可以直接定义属性的,需要通过运行时添加。

接下来我们跳入全能方法观察它做了什么。首先,我们看到该方法有五个参数,分别是图片url,占位图placeholder,配置选项options,下载进行中的进度block回调progressBlock,下载完成后的completedBlock回调。

- (void)setImageWithURL:(NSURL *)url
       placeholderImage:(UIImage *)placeholder
                options:(SDWebImageOptions)options
               progress:(SDWebImageDownloaderProgressBlock)progressBlock
              completed:(SDWebImageCompletedBlock)completedBlock;

然后,我们开始一步一步的观察此方法内部做了什么:

- (void)setImageWithURL:(NSURL *)url
       placeholderImage:(UIImage *)placeholder
                options:(SDWebImageOptions)options
               progress:(SDWebImageDownloaderProgressBlock)progressBlock
              completed:(SDWebImageCompletedBlock)completedBlock {
    [self cancelCurrentImageLoad]; // 取消当前的operation(取消操作并设置operationKey属性为nil)
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 设置imageURLKey属性
    self.image = placeholder; // 先设置占位图
    
    if (!(options & SDWebImageDelayPlaceholder)) { // SDWebImageDelayPlaceholder枚举值的含义是取消网络图片加载好前展示占位图片。所以在这里并不能直接把placeholder直接赋值给self.image,而要用if条件排除这种情况。
        self.image = placeholder;
    }
    
    if (url) {
        // 获取图片(先从内存找,若无再从磁盘找,若仍无则从网络下载。)
        // 先从内存找,若找到,则在主线程设置图片,并调用block回调;若在磁盘找到,则还要将其添加到内存缓存再做后续的;若是从网络下载而得,则内存和磁盘均要添加缓存,而后再进行后续的事。
        __weak UIImageView *wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
            if (!wself) return;
            dispatch_main_sync_safe(^{ // dispatch_main_sync_safe宏的目的是确保一定在主线程
                if (!wself) return;
                if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) { // 这个条件在这里又出现了,SDWebImageDelayPlaceholder的含义是网络图片加载前不显示占位图片,但此时网络图片没有成功请求到,所以此时需要加载占位图。
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType);
                }
            });
        }];
        objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 获取图片的同时,将该操作赋为UIImageView+WebCache的属性。
    }
}

该方法内部一上来首先就调用了方法[self cancelCurrentImageLoad];取消了当前UIImageView对象的获取图片操作;然后将参数url设为该分类的一个属性;接着设置了UIImageView对象的占位图;然后便调用了SDWebImageManager的实例对象方法获取图片,在其回调block里,若获取图片成功,则将其设为UIImageView的图片,若获取失败则将placeholder赋为占位图,并且最后将完成的block回调。(尽管我们通常使用的是没有回调的接口,若改为有回调的接口,下载完成后是可以收到回调的)
SDWebImageManager的获取图片的对象方法是异步下载图片的,该方法return的是id <SDWebImageOperation>类型的对象,它代表一个实现了SDWebImageOperation协议的任何对象,在这里表示获取图片这一操作。在调用了该方法获取图片的后,旋即我们便将该方法的返回值,即该operation赋为该分类的属性。至于为什么要将其赋为属性?因为它代表获取图片的“操作”,我们要允许后续将获取图片的操作取消。(下面马上会讲解“取消当前获取图片的操作”的方法)
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

上面的代码有几处仍需要细说,或者说留意一下。
First:一开始调用的取消当前获取图片的操作那个方法内部是这样实现的:

// 取消当前UIImageView对象的获取图片操作,并将分类的该属性赋为nil
- (void)cancelCurrentImageLoad {
    // Cancel in progress downloader from queue
    id <SDWebImageOperation> operation = objc_getAssociatedObject(self, &operationKey);
    if (operation) {
        [operation cancel];
        objc_setAssociatedObject(self, &operationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}

即通过运行时get到当前操作的属性对象operation,然后它调用了[operation cancel];方法,其实这个cancel方法是这么回事:获取图片的方法返回的是“获取图片的操作”——operation对象,这个operation是实现了SDWebImageOperation协议的一个对象。其实在获取图片方法内部我们就可以看到(后面将会说到)该对象是类SDWebImageCombinedOperation创建的,它正是实现了SDWebImageOperation协议,并实现了cancel方法,在其中进行了更多的取消处理(不仅是调用了NSOperationcancel方法)。

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        self.cancelBlock = nil;
    }
}

** Second:**SDWebImageDelayPlaceholder这个枚举值的意思是取消网络图片加载好之前展示占位图片,即网络图片加载好之前不显示占位图。

if (!(options & SDWebImageDelayPlaceholder)) { 
        self.image = placeholder;
    }

在这里并不能直接把placeholder直接赋值给self.image,而要用if条件排除这种情况。然而在网络下载图片的回调里,若没有得到图片的情况下又有段这样的代码:

if ((options & SDWebImageDelayPlaceholder)) { 
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }

此时网络图片没有成功请求到,而占位图此前又没有设置,所以此时单就这种情形需要加载占位图。
** Third:**在获取图片的block回调里我们可以看到,代码中使用的是dispatch_main_sync_safe,它是被定义的一个宏,目的是为了保证是在主线程的。因为设置图片这些UI操作,必须得在主线程进行。

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    }\
    else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

SDWebImageManager

然后我们点击downloadWithURL:options: progress: completed:方法便跳入了SDWebImageManager类中。该类是封装了获取图片的整个动作。我们先来看看SDWebImageManager.h头文件的代码吧。

@interface SDWebImageManager : NSObject

@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;

@property (strong, nonatomic, readonly) SDImageCache *imageCache; // 关于缓存
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 关于下载
@property (strong) NSString *(^cacheKeyFilter)(NSURL *url);


+ (SDWebImageManager *)sharedManager;

- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
                                    options:(SDWebImageOptions)options
                                   progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                  completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;

/**
 * Saves image to cache for given URL
 *
 * @param image The image to cache
 * @param url The URL to the image
 *
 */

- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;

/**
 * Cancel all current opreations
 */
- (void)cancelAll;

/**
 * Check one or more operations running
 */
- (BOOL)isRunning;

/**
 * Check if image has already been cached
 */
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (BOOL)diskImageExistsForURL:(NSURL *)url;

/**
 *Return the cache key for a given URL
 */
- (NSString *)cacheKeyForURL:(NSURL *)url;

@end

可以看到属性中有两个是非常重要的:imageCacheimageDownloader分别代表对缓存的处理和对下载的处理。同样下面也定义好几个方法,都自带了注释。
接下来我们重点看downloadWithURL:options: progress: completed:方法的实现。代码太长就不全部列出了,只列出主要代码:

- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
                                    options:(SDWebImageOptions)options
                                   progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                  completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 该方法return的就是该operation实例对象
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url]; // 判断该url是否已在“黑名单”内
    }

    // 对于url为空或在黑名单内且不允许再次请求配置的情况,则直接在此时用block回调,并抛出错误,而不用费力做后续步骤。
    if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES);
        });
        return operation;
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation]; // 将当前操作,添加进runningOperations数组,表示当前正在进行的操作们
    }
    NSString *key = [self cacheKeyForURL:url]; // 转换为url的字符串,图片在缓存中是以url字符串为key存储的。
    // 先从缓存中查找。返回NSOperation类型的operation对象
    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); // 有图片就通过block回调。
                });
            }
  ...
  ...

首先一开始对参数的校验及处理,我们自己写代码也一定要养成这样严谨的代码习惯。在方法内部首先对参数进行非空或者其他一些校验处理,一来可以防止因为参数值为nil引起的崩溃,二来若参数为nil,则大多数情况下可以直接return了,不必再费力往下执行了。同样的道理,下面判断了该url是否在黑名单内,若在黑名单内,并且是“不允许再次请求”配置的情况下,就直接在此用block回调,并抛出错误,而不用费力执行后续动作。

然后我们看到通过SDWebImageCombinedOperation类创建了operation对象,该方法return的就是该对象,SDWebImageCombinedOperation类就是一个实现了SDWebImageOperation协议的类。

旋即将该operation对象加入了self.runningOperations数组,表示当前正在进行的操作们。

然后我们以url的字符串为key,调用SDImageCache的实例方法queryDiskCacheForKey:key done:从缓存获取获取图片,若获取成功则将其回调。

若从缓存中没有找到图片,则继续往下执行至此,则开始要从网络请求图片了。

// 执行到此,说明缓存中并未找到图片,所以得从网络下载。
// 该方法进行异步下载图片,并返回了一个subOperation对象,它是一个实现了SDWebImageOperation协议的对象
    id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url
                                                                               options:downloaderOptions
                                                                              progress:progressBlock
                                                                             completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
    ...
    ...
    ...
    }];

然后在completed回调里,若下载成功,则首先将该图片存入缓存,然后回调至主线程。

// 下载成功后,将图片添加到缓存中,并通过block回调。  
    if (downloadedImage && finished) {
        [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
    }

    dispatch_main_sync_safe(^{
        completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished);
    });

并且,若该下载操作已完成,则runningOperations数组中移除该operation:

if (finished) {
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
    }

并且实现了operationcancelBlockblock回调:

operation.cancelBlock = ^{
        [subOperation cancel]; 
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:weakOperation];
        }
    };

结尾

SDWebImage的整体脉络结构,及它加载图片的思路就是以上了,看那张图更简洁清楚。但是这些也只是大体的脉络而已,还有最重要的缓存部分,以及网络请求部分都还没写,留在后续,分别另开两篇研究其缓存和网络部分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容