深入解读SDWebImage(一)

解读SDWebImage

SDWebImage具有缓存支持的异步映像下载程序。并添加了像UI元素分类类UIImageView、UIButton、MKAnnotationView,可以直接为这些UI元素添加图片。

首先我们看一下SDWebImage中的主要功能:

功能

1、对UIImageView、UIButton、MKAnnotationView添加Web图像和告诉缓存管理
2、异步图像下载器
3、具有自动缓存到期处理的异步内存+磁盘映像缓存
4、背景图像解压缩
5、对动画图像的支持
6、可以自定义和组合的转换,可在下载后立即应用于图像
7、可以自定义加载器(如照片库)来扩展图像加载功能
8、加载中的indicator显示
9、保证不会下载相同的URL
10、下载过程或者资源保存过程用到了GCD和ARC
11、提前将获取到的图片放到主线程,保证不会阻塞主线程

支持的图片格式

1、UIImage(JPEG,PNG,...)支持的图像格式,包括GIF
2、WebP格式,包括动画WebP
3、支持其它图片格式,包括APNG,BPG...

模块化

装载机

与第三方库集成

SDWebImage源码分析

SDWebImage架构图

时序图

从SDWebImage中提供的架构图中我们可以大概的看出,整个库分为两层,Top Level、Base Module。
Top Level:当UIImageView调用加载image方法,会进入SDWebImage中的UIImageView分类,在分类中调用负责加载UIImage的核心代码块ImageManager,其主要负责调度Image Cache/Image Loader,这两者分别从缓存或者网络端加载图片,并且又进行了细分。Cache中获取图片业务,拆分到了memory/disk两个分类中;Image Loader中又分为从网络端获取或者从系统的Photos中获取。
Base Module:获取到图片的二进制数据处理,二进制解压缩,二进制中格式字节判断出具体的图片类型。

  • 核心类
    • UIImageView+WebCache:SDWebImage加载图片的入口,随后都会进入sd_setImageWithURL: placeholderImage:中。
    • UIView+WebCache:由于分类中不能添加成员变量,所以runtime关联了sd_imageURL图片的url、sd_imageProgress下载进度;sd_cancelCurrentImageLoad方法取消当前下载,sd_imageIndicator设置当前加载动画,sd_internalSetImageWithURL:内部通过SDWebImageManager调用加载图片过程并返回调用进度
    • SDWebImageManager:内部分别由SDImageCache、SDWebImageDownloader来处理下载任务,

UIImageView+WebCache层

面向UIImageView的是UIImageView+WebCache这个分类,将图片的URL,占位图片直接给这个类,下面是这个类的公共接口:

- (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context;

- (void)sd_setImageWithURL:(nullable NSURL *)url
                 completed:(nullable SDExternalCompletionBlock)completedBlock;


- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

其中这几个方法最终都会进入

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

SDWebImageOptions,设置的图片加载以及缓存策略,有几种类型

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     默认情况下,当一个URL下载失败的时候,这个URL会被加入黑名单列表,
     下次再有这个url的请求则停止请求。
     如果为true,这个值表示需要再尝试请求。
     */
    SDWebImageRetryFailed = 1 << 0,
    
    /**
     默认情况下,当UI可以交互的时候就开始加载图片。这个标记可以阻止这个时候加载。
     而是当UIScrollView开始减速滑动的时候开始加载。
     */
    SDWebImageLowPriority = 1 << 1,
    
    /**
     这个flag启动渐进式下载图像,类似浏览器加载图像那样逐步显示(从上倒下加载)
     */
    SDWebImageProgressiveLoad = 1 << 2,
    
    /**
     一个图片缓存了,还是会重新请求.并且缓存侧略依据NSURLCache而不是SDImageCache。即在URL没变但是服务器图片发生更新时使用,这时我们需要在NSMutableRequest中设置上(If-Modified-Since-其实是缓存的最后修改时间,有后台返回),这个参数是在上一次网络请求之后,NSResponse中的Last-Modified获取,并保存下载,在下次发送网络请求的时候添加到请求头中;这样当我们下载同一张照片的时候,其实是拿取的上次GET请求的NSURLCache缓存中的图片,并且如果后台发生变化会重新请求图片
     */
    SDWebImageRefreshCached = 1 << 3,
    
    /**
     启动后台下载,实现原理是通过向系统询问后台的额外时间来完成请求的。 如果后台任务到期,则操作将被取消
     */
    SDWebImageContinueInBackground = 1 << 4,
    
    /**
     当设置了NSMutableURLRequest.HTTPShouldHandleCookies = YES时,可以控制存储NSHTTPCookieStorage中的cookie
     */
    SDWebImageHandleCookies = 1 << 5,
    
    /**
     允许不安全的SSL证书,用于测试环境,在正式环境中谨慎使用
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 6,
    
    /**
     默认情况下,image在加载的时候是按照他们在队列中的顺序装载的(就是先进先出)。这个flag会把他们移动到队列的前端,并且立刻装载,而不是等到当前队列装载的时候再装载。
     */
    SDWebImageHighPriority = 1 << 7,
    
    /**
    默认情况下,占位图会在图片下载的时候显示.这个flag开启会延迟占位图显示的时间,等到图片下载完成之后才会显示占位图.
     */
    SDWebImageDelayPlaceholder = 1 << 8,
    
    /**
     一般不会在动画图片上调用 transformDownloadedImage 代理方法,这样不能够管理动画图片
     这个flag为尝试转换动画图片
     */
    SDWebImageTransformAnimatedImage = 1 << 9,
    
    /**
     图片在下载后被加载到imageView。这个flag避免自动设置图片,来手动设置一下图片(引用一个滤镜或者加入透入动画)
     */
    SDWebImageAvoidAutoSetImage = 1 << 10,
    
    /**
     默认情况下,图像将根据其原始大小进行解码。 在iOS上,此flat会将图片缩小到与设备的受限内存兼容的大小。    但如果设置了SDWebImageAvoidDecodeImage则此flat不起作用。 如果设置了SDWebImageProgressiveLoad它将被忽略
     */
    SDWebImageScaleDownLargeImages = 1 << 11,
    
    /**
     结合SDWebImageQueryMemoryData设置同步查询图像数据(一般不建议这么使用,除非是在同一个runloop里避免单元格复用时发生闪现)
     */
    SDWebImageQueryMemoryData = 1 << 12,
    
    /**
     结合SDWebImageQueryMemoryData设置同步查询图像数据(一般不建议这么使用,除非是在同一个runloop里避免单元格复用时发生闪现)
     */
    SDWebImageQueryMemoryDataSync = 1 << 13,
    
    /**
     如果内存查询没有的时候,强制同步磁盘查询(这三个查询可以组合使用,一般不建议这么使用,除非是在同一个runloop里避免单元格复用时发生闪现)
     */
    SDWebImageQueryDiskDataSync = 1 << 14,
    
    /**
     * 默认情况下,当缓存丢失时,SD将从网络下载图像。 此flat可以防止这样,使其仅从缓存加载。
     */
    SDWebImageFromCacheOnly = 1 << 15,
    
    /**
     * 默认情况下,SD在下载之前先从缓存中查找,此flat可以防止这样,使其仅从网络下载
     */
    SDWebImageFromLoaderOnly = 1 << 16,
    
    /**
     * 默认情况下,SD在图像加载完成后使用SDWebImageTransition进行某些视图转换,此转换仅适用于从网络下载图像。 此flat可以强制为内存和磁盘缓存应用视图转换。
     */
    SDWebImageForceTransition = 1 << 17,
    
    /**
    默认情况下,SD在查询缓存和从网络下载时会在后台解码图像,这有助于提高性能,因为在屏幕上渲染图像时,需要首先对其进行解码。这发生在Core Animation的主队列中。然而此过程也可能会增加内存使用量。
     如果由于过多的内存消耗而遇到问题,可以用此flat禁止解码图像。
     */
    SDWebImageAvoidDecodeImage = 1 << 18,
    
    /**
     * 默认情况下,SD会解码动画图像,该flat强制只解码第一帧并生成静态图。
     */
    SDWebImageDecodeFirstFrameOnly = 1 << 19,
    
    /**
     默认情况下,对于SDAnimatedImage,SD会在渲染过程中解码动画图像帧以减少内存使用量。 但是用户可以指定将所有帧预加载到内存中,以便在大量imageView共享动画图像时降低CPU使用率。这实际上会在后台队列中触发preloadAllAnimatedImageFrames(仅限磁盘缓存和下载)。
     */
    SDWebImagePreloadAllFrames = 1 << 20
};

还有一个变量是SDWebImageContext *context,可以看到SDWebImageContext 其实就是以 SDWebImageContextOption为key、id(指定类型或者协议)为value 的NSDictionary,SDWebImageContextOption有下面的几种类型:


在UIImageView+WebCache内部调用的最终方法sd_setImageWithURL:进入到了UIView+WebCache,下面我们看一下UIView+WebCache

UIView+WebCache

首先还是看看API:

@interface UIView (WebCache)

/**
 当前图片的URL
 * Get the current image URL.
 */
@property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL;

/**
 * 当前view中图片loading的下载进度
 可以用KVO监听当前图片的下载进度,注意更新UI要回到主线程
**/
@property (nonatomic, strong, null_resettable) NSProgress *sd_imageProgress;


- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;

/**
 * 取消当前图片的获取请求
 */
- (void)sd_cancelCurrentImageLoad;


#pragma mark - Image Transition

/**
 图片的过渡属性,内部包含了很多的属性duration、过渡的动画样式
 默认是nil
 */
@property (nonatomic, strong, nullable) SDWebImageTransition *sd_imageTransition;

/**
 图片loading的indicator图标,默认是nil
 */
@property (nonatomic, strong, nullable) id<SDWebImageIndicator> sd_imageIndicator;

@end

看一下具体的方法实现:
sd_imageURL属性,为属性重写setter、getter方法,并用关联对象保存成员变量的值

- (nullable NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, @selector(sd_imageURL));
}

- (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL {
    objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

sd_imageProgress 进度的方法和上边的类似。

- (NSProgress *)sd_imageProgress {
    NSProgress *progress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
    if (!progress) {
        progress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
        self.sd_imageProgress = progress;
    }
    return progress;
}

- (void)setSd_imageProgress:(NSProgress *)sd_imageProgress {
    objc_setAssociatedObject(self, @selector(sd_imageProgress), sd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

接下来是最重要的sd_internalSetImageWithURL: 获取图片,并展示到UIButton、UIImageView上

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
    context = [context copy]; // copy to avoid mutable object
    
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
    }
    self.sd_latestOperationKey = validOperationKey;
    //1.判断是否有正在执行的任务
    //有正在进行的任务,那么取消下载任务
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    
    //2.设置Placeholder
    //SDWebImageDelayPlaceholder
    //!(options & SDWebImageDelayPlaceholder)不需要延时加载
    if (!(options & SDWebImageDelayPlaceholder)) {
        
        //必须保证是在主线程中执行
        dispatch_main_async_safe(^{
            
            //更新placeholder,设置当前的uiimageview
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    //3.进入SDWebImageManager来管理到是下载/缓存获取图片
    if (url) {
        //获取当前关联的NSProgress,并设置值为0
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        
        //开始图片indicator展示
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        }
        
        //开始封装下载progerees的block,用于会掉
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            //不断更新当前imageProgress进度
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            //假如imageIndicator存在更新updateIndicatorProgress:方法
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                //获取进度
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                //回到主线程更新imageIndicator
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        //开始下载图片
        @weakify(self);
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            //图片加载进度字段
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // 加载完成,停止indicator展示
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            
            //图片请求完成之后
            //可以设置SDWebImageAvoidAutoSetImage,是否可以设置图片
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            
            //是否需要自动设置图片/图片不存在并且立刻展示plachholder
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!self) { return; }
                
                //如果外部不需要设置图片,那么自动调整
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                
                //否则加载完成之后会掉给外界
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            //1.设置了SDWebImageAvoidAutoSetImage
            //2.没有获得图片但是有SDWebImageDelayPlaceholder
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            //设置图片
            //如果图片没有,并且placeholder延迟设置,那么将placeholder放到targetImage处
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            //检查是否需要图片的过渡动画Transition
            //SDWebImageForceTransition 强制进行过渡动画
            SDWebImageTransition *transition = nil;
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                //将获取到的图片image显示到对应的UIButton或者UIImageView中
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClojure();
            });
        }];
        //设置当前的下载operation到MapTable中
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
        //假如URL不存在,停止indicator的展示
        [self sd_stopImageIndicator];
#endif
        //返回completedBlock
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}

当前方法中有几个点需要注意:
第一:取消当前的获取图片任务 - sd_cancelCurrentImageLoad
当前view中有一个MapTable列表,用于记录当前所有的下载任务,可内部调用方法sd_cancelImageLoadOperationWithKey:在方法内部,实现[NSURLSessionTask cancel]结束当前的任务。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        //NSMapTable 其实就是字典,key是强引用,value是弱引用
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;

        //递归锁防止数据冲突
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                //取消任务  
                //内部调用[NSURLSessionTask cancel]结束当前的任务
                [operation cancel];
            }
            @synchronized (self) {
                
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

第二:在该方法内部调用[SDWebImageManager sharedManager],用来处理接下来缓存数据和下载数据之间的逻辑,在后边我们在讲解

sd_imageTransition属性

这个属性是设置的图片的过渡属性,内部包含了很多的属性duration、过渡的动画样式,下面是获取到image之后,通过Transition动画,将图片加载到当前的视图上,这个属性是需要开发者外部传递进来的。

//当拿到图片之后展示到NSButton、UIButton、UIImageView上的(包括提前设置placeholder也会调用这个方法)
//设置了过度动画
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
    UIView *view = self;
    SDSetImageBlock finalSetImageBlock;
    //进行判断是否是需要回掉block,如果是那么将block回掉
    if (setImageBlock) {
        finalSetImageBlock = setImageBlock;
    //否则直接判断是UIImageView或者是UIButton
    } else if ([view isKindOfClass:[UIImageView class]]) {
        UIImageView *imageView = (UIImageView *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            imageView.image = setImage;
        };
    }
#if SD_UIKIT
    //在手机端是UIButton
    else if ([view isKindOfClass:[UIButton class]]) {
        UIButton *button = (UIButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            [button setImage:setImage forState:UIControlStateNormal];
        };
    }
#endif
#if SD_MAC
    //在MAC中是NSButton
    else if ([view isKindOfClass:[NSButton class]]) {
        NSButton *button = (NSButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            button.image = setImage;
        };
    }
#endif
    
    if (transition) {
#if SD_UIKIT
        //过渡动画
        //UIViewAnimationOptions 中0代表  UIViewAnimationOptionLayoutSubviews作为子视图添加
        //
        [UIView transitionWithView:view duration:0 options:0 animations:^{
            //prepares是过渡动画开始之前的准备工作。所以在0 duration去渲染完之前的placeholder
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completion:^(BOOL finished) {
            //当当前的过渡动画执行完之后,执行下面的过渡动画,才真正开始进行过渡
            [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                //finalSetImageBlock 最终设置图片的block,在这里执行图片的过渡效果
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                //animations 过渡动画中,想要去改改变的某些对象
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completion:transition.completion];
        }];
#elif SD_MAC
//下边是Mac端
        [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
            // 0 duration to let AppKit render placeholder and prepares block
            prepareContext.duration = 0;
            if (transition.prepares) {
                transition.prepares(view, image, imageData, cacheType, imageURL);
            }
        } completionHandler:^{
            [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
                context.duration = transition.duration;
                context.timingFunction = transition.timingFunction;
                context.allowsImplicitAnimation = (transition.animationOptions & SDWebImageAnimationOptionAllowsImplicitAnimation);
                if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                    finalSetImageBlock(image, imageData, cacheType, imageURL);
                }
                if (transition.animations) {
                    transition.animations(view, image);
                }
            } completionHandler:^{
                if (transition.completion) {
                    transition.completion(YES);
                }
            }];
        }];
#endif
    } else {
        if (finalSetImageBlock) {
            finalSetImageBlock(image, imageData, cacheType, imageURL);
        }
    }
}

其中有这些过渡Transition的类型,可以作为参考

typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    UIViewAnimationOptionRepeat                    = 1 <<  3, // repeat animation indefinitely
    UIViewAnimationOptionAutoreverse               = 1 <<  4, // if repeat, run animation back and forth
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, // ignore nested duration
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, // ignore nested curve
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, // animate contents (applies to transitions only)
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, // flip to/from hidden state instead of adding/removing
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, // do not inherit any options or animation type
    
    UIViewAnimationOptionCurveEaseInOut            = 0 << 16, // default
    UIViewAnimationOptionCurveEaseIn               = 1 << 16,
    UIViewAnimationOptionCurveEaseOut              = 2 << 16,
    UIViewAnimationOptionCurveLinear               = 3 << 16,
    
    UIViewAnimationOptionTransitionNone            = 0 << 20, // default
    UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20,
    UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20,
    UIViewAnimationOptionTransitionCurlUp          = 3 << 20,
    UIViewAnimationOptionTransitionCurlDown        = 4 << 20,
    UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20,
    UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20,
    UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20,

    UIViewAnimationOptionPreferredFramesPerSecondDefault     = 0 << 24,
    UIViewAnimationOptionPreferredFramesPerSecond60          = 3 << 24,
    UIViewAnimationOptionPreferredFramesPerSecond30          = 7 << 24,
    
} NS_ENUM_AVAILABLE_IOS(4_0);

具体的用法,比较简单,

//图片设置fadeTransition类型的过渡效果
self.image.sd_imageTransition = SDWebImageTransition.fadeTransition;

SDWebImageTransition类是专门来处理过渡动画的,并在分类中设置了对应的过渡效果属性,可以自己看看哪中比较合适就用哪一种

/// Fade transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *fadeTransition;
/// Flip from left transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromLeftTransition;
/// Flip from right transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromRightTransition;
/// Flip from top transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromTopTransition;
/// Flip from bottom transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromBottomTransition;
/// Curl up transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *curlUpTransition;
/// Curl down transition.
@property (nonatomic, class, nonnull, readonly) SDWebImageTransition *curlDownTransition;

sd_imageIndicator

图片下载loading的indicator,主要是这两个方法,这个属性也是需要开发者自己去设置的

//对属性sd_imageIndicator 加载indicator添加关联对象
- (id<SDWebImageIndicator>)sd_imageIndicator {
    return objc_getAssociatedObject(self, @selector(sd_imageIndicator));
}

- (void)setSd_imageIndicator:(id<SDWebImageIndicator>)sd_imageIndicator {
    // 由于是之前添加了一个indicator,所以每次进来的时候
    //都需要移除之前的indicator
    id<SDWebImageIndicator> previousIndicator = self.sd_imageIndicator;
    [previousIndicator.indicatorView removeFromSuperview];
    
    //重新设置关联对象sd_imageIndicator
    objc_setAssociatedObject(self, @selector(sd_imageIndicator), sd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //在当前UIImageView/UIButton中添加indicatorView
    //这样就展示出indicatorView来了
    UIView *view = sd_imageIndicator.indicatorView;
    if (CGRectEqualToRect(view.frame, CGRectZero)) {
        view.frame = self.bounds;
    }
    // Center the indicator view
#if SD_MAC
    CGPoint center = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
    NSRect frame = view.frame;
    view.frame = NSMakeRect(center.x - NSMidX(frame), center.y - NSMidY(frame), NSWidth(frame), NSHeight(frame));
#else
    //设置展示在当前的中心center位置
    view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
#endif
    view.hidden = NO;
    [self addSubview:view];
}

SDWebImage中已经为我们封装好了对应的类SDWebImageActivityIndicator,以及相应的方法,来实现对应的loading中的indicator效果只需要按照下面代码进行调用,

//设置indicator方式为grayLargeIndicator,会显示一个大的灰色indicator
self.image.sd_imageIndicator = SDWebImageActivityIndicator.grayLargeIndicator

在SDWebImageActivityIndicator对应的分类中还有很多类型,下面是所有的类型

/// These indicator use the fixed color without dark mode support
/// gray-style activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *grayIndicator;
/// large gray-style activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *grayLargeIndicator;
/// white-style activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *whiteIndicator;
/// large white-style activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *whiteLargeIndicator;
/// These indicator use the system style, supports dark mode if available (iOS 13+/macOS 10.14+)
/// large activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *largeIndicator;
/// medium activity indicator
@property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *mediumIndicator;

总结一下在UIView+WebCache中用到的其它类
(1)、UIView+WebCacheOperation - 内部的MapTable中key保存了NSStringFromClass([self class]) 视图类对象字符,value是SDWebImageCombinedOperation - 区分加载任务而生成的中间类
(2)、SDWebImageManager - 主要处理下一步具体图片的获取流程
(3)、SDWebImageTransition - 专门处理过渡动画
(4)、SDWebImageActivityIndicator - 处理loading过程中的indicator动画

下一篇:[深入解读SDWebImage(一)](https://www.jianshu.com/p/c49f9642efb4

欢迎关注我的公众号,专注iOS开发、大前端开发、跨平台技术分享。


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

推荐阅读更多精彩内容

  • 图片下载的这些回调信息存储在SDWebImageDownloader类的URLOperations属性中,该属性是...
    怎样m阅读 2,359评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 项目中一直都有使用SDWebImage,对这个框架有一定的了解,但是体系却未能贯通,因此特地整理下,主要参考: i...
    林大鹏阅读 1,457评论 2 13
  • 1 概述 SDWebImage基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清...
    NS西北风阅读 703评论 0 4
  • 技术无极限,从菜鸟开始,从源码开始。 由于公司目前项目还是用OC写的项目,没有升级swift 所以暂时SDWebI...
    充满活力的早晨阅读 12,622评论 0 2