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 <SDWebImageOperation> 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 <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel]; //< 取消
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) 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.减少网络请求,网络请求是一个很耗时的操作

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 技术无极限,从菜鸟开始,从源码开始。 由于公司目前项目还是用OC写的项目,没有升级swift 所以暂时SDWebI...
    充满活力的早晨阅读 12,551评论 0 2
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,616评论 4 59
  • 今天我们来聊聊“眼光”吧…… 话说,在中国,家喻户晓,被认可最有眼光,最有影响力的商人,非马云是也。 大家认同吗?...
    曹阳CY阅读 244评论 0 0
  • 上一章:大汉王朝(下) [前言] 近来,项目的事情基本成型,业已走向正规...
    独孤一鸣阅读 633评论 56 41