SDWebImage 源码解析 (一)

1,995 阅读12分钟

1 概述

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

img

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

  • 各种分类:
    • UIButton(WebCache)UIButton类添加载图片的方法。比如正常情况下、点击情况下、的image属性和背景图片等。
    • MKAnnotationView(WebCache)MKAnnotationView类添加各种加载图片的方法。
    • UIImageView(WebCache)UIImageView类添加加载图片的方法。
    • UIImageView(HighlightedWebCache)UIImageView类添加高亮状态下加载图片的方法。
    • FLAnimatedImageView(WebCache)FLAnimatedImageView类添加加载动态的方法,这个分类需要引入FLAnimatedImage框架。SDWebImage推荐使用这个框架来处理动态图片(GIF)的加载。
    • UIImageView、UIButton、FLAnimatedImageView通过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: 拥有一个SDWebImageCacheSDWebImageDownloader属性分别用于图片的缓存和加载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合。这个类是 一个单列。还有就是各种加载选项的处理。
    • SDWebImageDownloader: 实现了图片加载的具体处理,如果图片在缓存存在则从缓存区。如果缓存不存在,则直接创建一个。SDWebImageDownloaderOperation对象来下载图片。管理NSURLRequest对象请求头的封装、缓存、cookie的设置。加载选项的处理等功能。管理Operation之间的依赖关系。这个类是 一个单列.
    • SDWebImageDownloaderOperation: 一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。
    • UIView+WebCache: 所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有UIView及其子类的分类都是用这个类的sd_intemalSetImageWithURL:来实现图片的加载。
    • FLAnimatedImageView: 动态图片的数据通过ALAnimatedImage对象来封装。FLAnimatedImageViewUIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比 UIImageView做了流程优化。

2 实现流程

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

img

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

3 UIView+WebCache分析

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

/**
 所有UIView及其子类都是通过这个方法来加载图片

 @param url 加载的url
 @param placeholder 占位图
 @param options 加载选项
 @param operationKey key
 @param setImageBlock Block
 @param progressBlock 进度Block
 @param completedBlock 回调Block
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    
    //取消当前类所对应的所有下载Operation对象
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    /*
     把UIImageView的加载图片操作和他自身用关联对象关联起来,方便后面取消等操作。关联的key就是UIImageView对应的类名
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //如果有设置站位图,则先显示站位图
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        //如果UIImageView对象有设置添加转动菊花数据,加载的时候添加转动的菊花
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        /*
         *operation是一个`SDWebImageCombinedOperation`对象。通过这个对象来获取图片
         */
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            //停止菊花
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                //如果设置了不自动显示图片,则直接调用completedBlock,让调用者处理图片的显示
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    //自动显示图片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    //如果设置了延迟显示占位图,则图片加载失败的情况下显示占位图
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //完成回调
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //关联Operationkey与Operation对象。方便后面根据key取消operation操作等。
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        //加载失败的情况
        dispatch_main_async_safe(^{
            //移除菊花
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

给UIView及其子类添加旋转菊花是通过关联对象来实现的。通过如下几个方法来实现:

#pragma mark 通过关联对象来实现菊花的添加
- (UIActivityIndicatorView *)activityIndicator {
    return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
}

- (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark 是否显示旋转菊花
- (void)sd_setShowActivityIndicatorView:(BOOL)show {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)sd_showActivityIndicatorView {
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
#pragma mark 旋转菊花的样式
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}

- (int)sd_getIndicatorStyle{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}

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

/**
 关联Operation对象与key对象

 @param operation Operation对象
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}
/**
 取消当前key对应的所有实现了SDWebImageOperation协议的Operation对象

 @param key Operation对应的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //获取当前View对应的所有key
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    //获取对应的图片加载Operation
    id operations = operationDictionary[key];
    //取消所有当前View对应的所有Operation
    if (operations) {
        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];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

4 FLAnimatedImageView分析

SDWebImage使用FLAnimatedImage框架来处理动态图片,它包含FLAnimatedImageFLAnimatedImageView两个雷。动态图片的数据通过 ALAnimatedImage对象来封装。FLAnimatedImageViewUIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView做了流程优化。我们来看一下 FLAnimatedImageView.h里面定义的接口:

/**
 `FLAnimatedImageView`是一个`UIImageView`的子类。实现了`UIImageView`的`start/stop/isAnimating`方法。所以我们可以直接使用`FLAnimatedImageView`替代`UIImageView`。
 通过`CADisplayLink`对象来处理当前图片帧和下一帧图片的显示。
 */
@interface FLAnimatedImageView : UIImageView
/**
 动态图片的封装对象。首先通过设置`[UIImageView.image]`为nil来清除已经存在的动态图片。设置`animatedImage`属性会自动设置新的动态图片并且开始显示。而且会把当前显示的UIImage存入`currentFrame`中。
 */
@property (nonatomic, strong) FLAnimatedImage *animatedImage;

@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
/**
 当前动画帧对应的UIImage对象
 */
@property (nonatomic, strong, readonly) UIImage *currentFrame;
/**
 当前图片镇对应的索引
 */
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
/**
 指定动态图片执行所在的runloop的mode。NSRunLoopCommonMode
 */
@property (nonatomic, copy) NSString *runLoopMode;
@end

我们通过FLAnimatedImageView+WebCache这个分类的sd_setImageWithURL来加载动态图片:

/**
 FLAnimatedImage+WebCache分类通过这个方法来加载动态图片

 @param url 图片的url
 @param placeholder 占位图
 @param options 加载选项
 @param progressBlock 进度Block
 @param completedBlock 完成Block
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           //根据NSData的类型获取图片的类型
                           SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                           //如果是GIF,则处理
                           if (imageFormat == SDImageFormatGIF) {
                               //给FLAnimatedImageView的animatedImage属性设置动态图片。这个setter方法被重写了
                               weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];
                               weakSelf.image = nil;
                           } else {
                               //不是动态图片,则正常显示
                               weakSelf.image = image;
                               weakSelf.animatedImage = nil;
                           }
                       }
                            progress:progressBlock
                           completed:completedBlock];
}

从上面可以看出,获取图片数据以后。首先通过SDImageFormat得到图片的类型。如果是GIF类型,则先把图片数据封装成一个FLAnimatedImage对象。然后设置给animatedImage属性。这个属性的setter方法如下:

/**
 animatedImage的setter方法。通过这个属性setter方法来设置FLAnimatedImageView的数据。并且开始动态显示

 @param animatedImage animatedImage属性
 */
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
    if (![_animatedImage isEqual:animatedImage]) {
        if (animatedImage) {
            //清除UIImageView以前的图片数据
            super.image = nil;
            super.highlighted = NO;
            //先说intrinsicContentSize,也就是控件的内置大小。比如UILabel,UIButton等控件,他们都有自己的内置大小。控件的内置大小往往是由控件本身的内容所决定的,比如一个UILabel的文字很长,那么该UILabel的内置大小自然会很长。控件的内置大小可以通过UIView的intrinsicContentSize属性来获取内置大小,也可以通过invalidateIntrinsicContentSize方法来在下次UI规划事件中重新计算intrinsicContentSize。如果直接创建一个原始的UIView对象,显然它的内置大小为0。
            [self invalidateIntrinsicContentSize];
        } else {
            //停止动态图片的动态显示
            [self stopAnimating];
        }
        //赋值
        _animatedImage = animatedImage;
        //当前动态图片数据帧
        self.currentFrame = animatedImage.posterImage;
        //当前数据帧索引
        self.currentFrameIndex = 0;
        if (animatedImage.loopCount > 0) {
            self.loopCountdown = animatedImage.loopCount;
        } else {
            self.loopCountdown = NSUIntegerMax;
        }
        self.accumulator = 0.0;
        
        //更新对象的状态。从而更新shouldAnimated这个属性的值。
        [self updateShouldAnimate];
        if (self.shouldAnimate) {
            //开始动态显示
            [self startAnimating];
        }
        
        [self.layer setNeedsDisplay];
    }
}
/**
 判断当前FLAnimatedImageView是否需要显示动画
 */
- (void)updateShouldAnimate
{
    BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
    self.shouldAnimate = self.animatedImage && isVisible;
}

5 CADisplayLink

有趣的地方是FLAnimatedImageView通过过CADisplayLink来刷新动态图片帧的显示。CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的CADisplayLink对象,把它添加到一个runloop中,并给它提供一个target和selector在屏幕刷新的时候调用。

一但CADisplayLink以特定的模式注册到runloop之后,每当屏幕需要刷新的时候runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到CADisplayLink的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。duration属性提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。frameInterval属性是可读可写的NSInteger型值,标识间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。我们通过pause属性开控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate从runloop中删除并删除之前绑定的 target跟selector。另外CADisplayLink 不能被继承。

//每1/60秒都回调用一次displayDidRefresh方法来做UI处理
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
//把displayLink加入主线程的commomMode里面        
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];

最后原文地址,demo地址