1 概述
SDWebImage
基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清理等一些列功能。让我们专心于业务的处理。但是并不意味着会用就可以了,通过源码分析和学习,让我们知道如何用好它。学习分析优秀源码也可以从潜移默化中给我们提供很多解决日常需求的思路。下面就是一张图来概述SDWebImage
的所有类:
通过对这个图片的分析,我们可以把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
: 拥有一个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
为我们实现了图片加载、数据处理、图片缓存等一些列工作。通过下图我们可以分析一下他的流程:
通过这个图,我们发现SDWebImage
加载的过程是首先从缓存中加载数据。而且缓存加载又是优先从内存缓存中加载,然后才是磁盘加载。最后如果缓存没有,才从网络上加载。同时网络成功加载图片以后,存入本地缓存。
3 UIView+WebCache
分析
UIImageView、UIButton、FLAnimatedImageView都会调用UIView(WebCache)
分类的sd_internalSetImageWithURL
方法来做图片加载请求。具体是通过SDWebImageManager
调用来实现的。同时实现了Operation取消、ActivityIndicator的添加与取消。我们首先来看sd_internalSetImageWithURL
方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
- (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 { NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); [self sd_cancelImageLoadOperationWithKey:validOperationKey];
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) { if ([self sd_showActivityIndicatorView]) { [self sd_addActivityIndicator]; } __weak __typeof(self)wself = self;
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; } 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); } }); }]; [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及其子类添加旋转菊花是通过关联对象来实现的。通过如下几个方法来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #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
的关联与取消。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
- (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; } } }
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key { SDOperationsDictionary *operationDictionary = [self operationDictionary]; id operations = operationDictionary[key]; 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
框架来处理动态图片,它包含FLAnimatedImage
和FLAnimatedImageView
两个雷。动态图片的数据通过ALAnimatedImage
对象来封装。FLAnimatedImageView
是UIImageView
的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView
做了流程优化。我们来看一下FLAnimatedImageView.h
里面定义的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
@interface FLAnimatedImageView : UIImageView
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
@property (nonatomic, strong, readonly) UIImage *currentFrame;
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
@property (nonatomic, copy) NSString *runLoopMode; @end
|
我们通过FLAnimatedImageView+WebCache
这个分类的sd_setImageWithURL
来加载动态图片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
- (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) { SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData]; if (imageFormat == SDImageFormatGIF) { weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData]; weakSelf.image = nil; } else { weakSelf.image = image; weakSelf.animatedImage = nil; } } progress:progressBlock completed:completedBlock]; }
|
从上面可以看出,获取图片数据以后。首先通过SDImageFormat
得到图片的类型。如果是GIF类型,则先把图片数据封装成一个FLAnimatedImage
对象。然后设置给animatedImage属性。这个属性的setter方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage { if (![_animatedImage isEqual:animatedImage]) { if (animatedImage) { super.image = nil; super.highlighted = NO; [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; [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } [self.layer setNeedsDisplay]; } }
- (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 2 3 4
| self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
|
最后原文地址.html),demo地址。