SDWebImage源码解析(一)

1、概述

SDWebImage基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清理等一些列功能。让我们专心于业务的处理。但是并不意味着会用就可以了,通过源码分析和学习,让我们知道如何用好它。学习分析优秀源码也可以从潜移默化中给我们提供很多解决日常需求的思路。下面就是一张图来概述SDWebImage的所有类:

类关系图.png

通过对这个图片的分析,我们可以把SDWebImage的源码分为三种:

  • 各种分类:
    • UIButton(WebCache)UIButton类添加载图片的方法。比如正常情况下、点击情况下、的image属性和背景图片等。
    • MKAnnotationView(WebCache)MKAnnotationView类添加各种加载图片的方法。
    • UIImageView(WebCache)UIImageView类添加加载图片的方法。
    • UIImageView(HighlightedWebCache)为UIImageView`类添加高亮状态下加载图片的方法。
    • FLAnimatedImageView(WebCache)FLAnimatedImageView类添加加载动态的方法,这个分类需要引入FLAnimatedImage框架。SDWebImage推荐使用这个框架来处理动态图片(GIF)的加载。
    • UIImageViewUIButtonFLAnimatedImageView通过sd_setImageWithURL等api来做图片加载请求。这也是我们唯一需要做的。
    • 上面的几个UIView子类都会调用UIView(WebCache)分类的sd_internalSetImageWithURL方法来做图片加载请求。具体是通过SDWebImageManager调用来实现的。同时实现了 Operation取消、ActivityIndicator的添加与取消。
  • 各种工具类:
    • NSData+ImageContentType: 根据图片数据获取图片的类型,比如GIF、PNG等。
    • SDWebImageCompat: 根据屏幕的分辨倍数成倍放大或者缩小图片大小。
    • SDImageCacheConfig: 图片缓存策略记录。比如是否解压缩、是否允许iCloud、是否允许内存缓存、缓存时间等。默认的缓存时间是一周。
    • UIImage+MultiFormat: 获取UIImage对象对应的data、或者根据data生成指定格式的UIImage,其实就是UIImage和NSData之间的转换处理。
    • UIImage+GIF: 对于一张图片是否GIF做判断。可以根据NSData返回一张GIF的UIImage对象,并且只返回GIF的第一张图片生成的GIF。如果要显示多张GIF,使用FLAnimatedImageView。
    • SDWebImageDecoder: 根据图片的情况,做图片的解压缩处理。并且根据图片的情况决定如何处理解压缩。
  • 核心类:
    • SDImageCache 负责SDWebImage的整个缓存工作,是一个单列对象。缓存路径处理、缓存名字处理、管理内存缓存和磁盘缓存的创建和删除、根据指定key获取图片、存入图片的类型处理、根据缓存的创建和修改日期删除缓存。
    • SDWebImageManager: 拥有一个SDWebImageCache和SDWebImageDownloader属性分别用于图片的缓存和加载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合。这个类是一个单列。还有就是各种加载选项的处理。
    • SDWebImageDownloader: 实现了图片加载的具体处理,如果图片在缓存存在则从缓存区。如果缓存不存在,则直接创建一个。 SDWebImageDownloaderOperation对象来下载图片。管理NSURLRequest对象请求头的封装、缓存、cookie的设置。加载选项的处理等功能。管理Operation之间的依赖关系。这个类是一个单列.
    • SDWebImageDownloaderOperation: 一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。
    • UIView+WebCache: 所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有 UIView及其子类的分类都是用这个类的sd_intemalSetImageWithURL:来实现图片的加载。
    • FLAnimatedImageView: 动态图片的数据通过ALAnimatedImage对象来封装。-FLAnimatedImageView是UIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView做了流程优化。

2、实现流程

SDWebImage为我们实现了图片加载、数据处理、图片缓存等一些列工作。通过下图我们可以分析一下他的流程:

流程图.png

通过这个图,我们发现SDWebImage加载的过程是首先从缓存中加载数据。而且缓存加载又是优先从内存缓存中加载,然后才是磁盘加载。最后如果缓存没有,才从网络上加载。同时网络成功加载图片以后,存入本地缓存。

3、UIView+WebCache 、UIView+WebCacheOperation分析

UIImageViewUIButtonFLAnimatedImageView都会调用UIView(WebCache)分类的sd_internalSetImageWithURL方法来做图片加载请求。具体是通过SDWebImageManager调用来实现的。同时实现了Operation取消、ActivityIndicator的添加与取消。我们首先来看sd_internalSetImageWithURL方法的实现:

/**
 《核心方法》
 使用image的url和占位符设置imageView的image

 @param url 图像的URL
 @param placeholder 初始化的占位图,直到url请求完成
 @param options 下载图片时的选项,详见 SDWebImageOptions
 @param operationKey 操作的关键字,如果为空,则为类的名字
 @param setImageBlock 设置图片时的回调代码
 @param progressBlock 图片下载时的回调,注意执行是在后台的队列
 @param completedBlock 所有操作完成时回调,不返回值
                         *并将请求的uiimage作为第一个参数。如果出现错误,图像参数
                         *为零,第二个参数可能包含nserrror。第三个参数是布尔值
                         *指示是从本地缓存还是从网络检索图像。
                         *第四个参数是原始图像URL。
 @param context 具有执行指定更改或进程的额外信息的上下文。
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
             internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary<NSString *, id> *)context {
    
    /**
     1、停止所有和这个View相关的Operation。
     这里还有一个operation 字典:UIView+WebCacheOperation  有一个关联对象,用来保存这个View相关的Operation
     @param operationKey 操作的关键字,如果为空,则为类的名字
     UIView+WebCacheOperation 分类用来保存view相关的操作
     */
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    
    /** 存url
     *  把UIImageView的加载图片操作和他自身用关联对象关联起来,方便后面取消等操作。关联的key就是UIImageView对应的类名
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    /** 如果有设置SDWebImageDelayPlaceholder,则先显示站位图 */
    dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];
    if (!(options & SDWebImageDelayPlaceholder)) {
        if (group) {
            dispatch_group_enter(group);
        }
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    if (url) {
#if SD_UIKIT
        // check if activityView is enabled or not
        /** 如果UIImageView对象有设置添加转动菊花数据,加载的时候添加转动的菊花 */
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
#endif
        // reset the progress
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        }
        
        // 定义完成进度的block回调
        __weak __typeof(self)wself = self;
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
#if SD_UIKIT
            //停止菊花
            [sself sd_removeActivityIndicator];
#endif
            // if the progress not been updated, mark it to complete state
            /** 如果progress没有更新,设置总量和完成量为未知状态 */
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
             /** 如果设置了不自动显示图片,则直接调用completedBlock,让调用者处理图片的显示
              *
              * finished or SDWebImageAvoidAutoSetImage --->应该 调用CompletedBlock
              */
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            /**
             *  得到image and  (SDWebImageAvoidAutoSetImage在completeblock 中设置图片)---> 不自动显示
             *  没得到image and  (默认显示了SDWebImageDelayPlaceholder占位图) ----> 不自动显示。
             *  不自动显示 ---> 代用completedBlock
             */
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            /** ⏬  设置imageview的image */
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                /**
                 *  得到image and 没有设置SDWebImageAvoidAutoSetImage
                 */
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                /**
                 *  没得到image and 没有设置SDWebImageDelayPlaceholder
                 */
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            SDWebImageTransition *transition = nil;
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = sself.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
                if (group) {
                    dispatch_group_enter(group);
                }
#if SD_UIKIT || SD_MAC
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                if (group) {
                    // compatible code for FLAnimatedImage, because we assume completedBlock called after image was set. This will be removed in 5.x
                    BOOL shouldUseGroup = [objc_getAssociatedObject(group, &SDWebImageInternalSetImageGroupKey) boolValue];
                    if (shouldUseGroup) {
                        dispatch_group_notify(group, dispatch_get_main_queue(), callCompletedBlockClojure);
                    } else {
                        callCompletedBlockClojure();
                    }
                } else {
                    callCompletedBlockClojure();
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
#if SD_UIKIT
            [self sd_removeActivityIndicator];
#endif
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

还有就是通过UIView+WebCacheOperation类来实现UIView的图片下载Operation的关联和取消。具体key的值可以从sd_internalSetImageWithURL中找到具体获取方式,通过在这个方法中实现Operation的关联与取消。

/**
 关联Operation对象与key对象
 
 @param operation Operation对象
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}
/**
 取消当前key对应的所有实现了SDWebImageOperation协议的Operation对象
 
 @param key Operation对应的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容