SDWebImage源码分析 1

前言

开发iOS有一段时间了,平时工作中主要还是完成业务功能。类似网络请求,图片加载等等都直接使用现成的开源类库,项目主要还是以稳定为先。
但长期这样感觉难以进步,想要进阶除了看书外就得多看看开源类库的源码了。
于是就从SDWebImage入手,在深入学习后发现它的代码各层职责分工明确,代码量也不是很多,利用业余时间断断续续学习花费了大约三周时间,感觉比较适合作为第一个供学习的开源类库。

大致涉及到的知识点:

  • Block
  • GCD
  • NSOperation
  • Associated Objects
  • NSURLRequest
  • NSCache
  • 图片类型识别与处理

文章中难免出现问题,望各位给予纠正,有问题欢迎一起讨论。

源码分析

SDWebImage使用起来非常简单,只需调用sd_setImageWithURL方法,就可以将图片异步的加载并显示在UIImageView上。

所以接下来我们就从sd_setImageWithURL开始说起:

NSURL * url = [NSURL URLWithString:@"http://hbimg.b0.upaiyun.com/ddd2cee8ff21d4a09a86b68972b78b15ba7bc2a035fa4-sGYzEJ_fw658"];
[imageView sd_setImageWithURL:url];

上面代码所使用的是sd_setImageWithURL最简单的版本,我们跟进去看一下,发现方法里其实帮我们设置好了默认参数,最终调用到的是另一个方法:

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

我们跟进去看看,通过注释可以得知这个方法的用途:

/**
 * �根据url给imageView设置image,占位图和各种自定义设置
 *
 * 使用异步下载和缓存
 *
 * @param url            图片的url
 * @param placeholder    占位图
 * @param options        下载图片时的各种设置. @see SDWebImageOptions.
 * @param progressBlock  当图片正在下载时会被回调到
 * @param completedBlock 当任务完成时会被回调到 。该block没有返回值使用UIImage作为第一个参数
 *                       如果下载中出现错误UIIMage为nil并且第二个参数会包含NSError
 *                       第三个参数是一个枚举(*原注释这块写的是布尔值),表示图片是从本地缓存中还是网络中取回的
 *                       第四个参数是原生的image url
 */
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

//completedBlock,参数与注释对应
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

接着我们看代码,然后一步步分析:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    //取消当前UIImageView正在加载的图片任务
    [self sd_cancelCurrentImageLoad];

    //相当于给当前UIImageView对象上绑定图片url属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    //如果options中没有传入SDWebImageDelayPlaceholder参数,则设置占位图
    //这里出现了dispatch_main_async_safe,其实是SDWebImage定义的宏,其实就是将UI操作放入主线程中用的
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

    if (url) {

        // 检查是否打开了"会转动的菊花"选项
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator]; //< 界面上会出现转动的菊花
        }

        __weak __typeof(self)wself = self;
        //从方法名中可以猜出它是用来下载图片用的,目前只需要这么理解就好,后面章节会具体谈到
        id  operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            [wself removeActivityIndicator]; //<将转动的菊花从界面上移除
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                //设置了SDWebImageAvoidAutoSetImage参数时,默认不会将image添加进UIViewImage对象,而是放置到completedBlock中交由调用方自己处理,比如做个滤镜或者添加淡出淡入效果什么的
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image; //< 设置image
                    [wself setNeedsLayout];
                } else { //< 当image为nil
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;//< 此时再将占位图设置进去
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //保存本次operation,如果发生多次图片请求加载可以用来取消
        //先取消当前UIImageView正在加载的任务,再保存operation
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

这里先提几个点:
1.在代码中我们会发现有dispatch_main_async_safe这么一个神奇的东东,其实它是SDWebImage定义的宏,将UI操作放入主线程中用的:

#define dispatch_main_async_safe(block)
     if ([NSThread isMainThread]) { //< 如果当前在主线程中
         block();
     } else { //< 不在主线程就将它放入主线程
         dispatch_async(dispatch_get_main_queue(), block);
     }

2.代码中偶尔会出现objc_setAssociatedObject,简单的说使用该技巧可以很方便的将变量动态绑定在该实例下,原因在于Category中是不允许添加实例变量。

回到主题来,代码在请求下载图片前执行了[self sd_cancelCurrentImageLoad],从方法名上可以猜出它的大意“取消当前图片的加载”,他是作什么用的呢,为什么在加载图片前会需要用到取消这么一个方法?带着疑问我们继续,发现调用了另一个方法,看来这里只负责传入对应的“key”

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

再跟进来我们可以看到具体的实现了

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    //利用AssociatedObject维护的字典,用于存放当前任务中的operation(图片请求)
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    //key为"UIImageViewImageLoad"
    id operations = [operationDictionary objectForKey:key];

    if (operations) { //< 当前有正在执行的operation,需要取消任务
        //多个operation的是gif(多帧),单个的是普通图片
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id  operation in operations) {
                if (operation) {
                    [operation cancel]; //< 取消
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id) operations cancel]; //< 取消
        }
        //删除对应key的对象
        //每次对应UIView有一个图片请求的任务时,都会设置对应的key,所以可以根据这个key来判断是否有正在执行的任务
        [operationDictionary removeObjectForKey:key];
    }
}

看完上面这段代码后,我们大致有了一个概念,同时也发现这两段代码的“key”是一样的:

//取消当前UIImageView正在加载的图片任务
[self sd_cancelCurrentImageLoad];
...
//保存本次operation,如果发生多次图片请求加载可以用来取消
//先取消当前UIImageView正在加载的任务,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

再回到刚才的疑问,举个例子来说就能明白方法的意图和具体流程了:

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/1.png"] placeholderImage:nil];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/2.png"] placeholderImage:nil];

一个imageView请求了两张图片,1.png 和 2.png,但我们只希望显示 2.png,所以需要取消 1.png的请求。原因有两点:
1.在异步请求中(先后顺序不定),有可能 1.png 会在 2.png 后面获取到,会覆盖掉2.png
2.减少网络请求,网络请求是一个很耗时的操作

你可能感兴趣的:(SDWebImage源码分析 1)