SDWebImage
基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清理等一些列功能。让咱们专心于业务的处理。可是并不意味着会用就能够了,经过源码分析和学习,让咱们知道如何用好它。学习分析优秀源码也能够从潜移默化中给咱们提供不少解决平常需求的思路。下面就是一张图来概述SDWebImage
的全部类:html
经过对这个图片的分析,咱们能够把SDWebImage
的源码分为三种:git
各类分类:github
UIButton(WebCache)
为UIButton
类添加载图片的方法。好比正常状况下、点击状况下、的image属性和背景图片等。api
MKAnnotationView(WebCache)
为MKAnnotationView
类添加各类加载图片的方法。缓存
UIImageView(WebCache)
为UIImageView
类添加加载图片的方法。cookie
UIImageView(HighlightedWebCache)
为UIImageView
类添加高亮状态下加载图片的方法。网络
FLAnimatedImageView(WebCache)
为FLAnimatedImageView
类添加加载动态的方法,这个分类须要引入FLAnimatedImage
框架。SDWebImage
推荐使用这个框架来处理动态图片(GIF)的加载。框架
UIImageView、UIButton、FLAnimatedImageView经过sd_setImageWithURL
等api来作图片加载请求。这也是咱们惟一须要作的。async
上面的几个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
作了流程优化。
SDWebImage
为咱们实现了图片加载、数据处理、图片缓存等一些列工做。经过下图咱们能够分析一下他的流程:
经过这个图,咱们发现SDWebImage
加载的过程是首先从缓存中加载数据。并且缓存加载又是优先从内存缓存中加载,而后才是磁盘加载。最后若是缓存没有,才从网络上加载。同时网络成功加载图片之后,存入本地缓存。
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]; } }
SDWebImage
使用FLAnimatedImage
框架来处理动态图片,它包含FLAnimatedImage
和FLAnimatedImageView
两个雷。动态图片的数据经过ALAnimatedImage
对象来封装。FLAnimatedImageView
是UIImageView
的子类。经过他彻底能够实现动态图片的加载显示和管理。而且比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; }
有趣的地方是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];