[iOS开发]SDWebImage的实现机制及源码分析(未完

SDWebImage的主要功能以及一些知识点

提供UIImageView的一个分类,以支持网络图片的加载和缓存管理

  • 其是一个异步的图片加载器
  • 一个异步的内存+磁盘图片缓存
  • 支持GIF图片
  • 支持WebP图片(WebP图像格式是新的图像压缩格式)
  • 后台图片解压缩处理
  • 确保同一个URL图片不被下载多次以及确保虚假的URL不会被反复加载
  • 确保下载及缓存时,主线程不被阻塞

主要功能

下载

SDWebImage 下载的核心其实就是利用 NSURLConnection 对象来加载数据。每个图片的下载都由一个 Operation 操作来完成,并将这些操作放到一个操作队列中。 这样可以实现图片的并发下载。

@interface NSURLConnection : NSObject
{
    @private
    NSURLConnectionInternal *_internal;
}//我们可以看到NSURLConnection是继承自NSObject的类,其只有一个访问控制符为private的(只允许同一个类中进行访问)属性。

缓存

为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。
SDWebImage 提供了对图片缓存的支持,而该功能是由SDImageCache 类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对 UI 操作造成影响。

内存缓存

内存缓存的处理是使用 NSCache 对象来实现的。NSCache 是一个类似于集合的容器。它存储 key-value 对,这一点类似于 NSDictionary 类。 我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。 重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。
在这里插入图片描述

磁盘缓存

使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache 还定义了一个串行队列,来异步存储图片。
SDImageCache 提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key 值来索引它。 在内存中,我们将其作为 NSCache 的 key 值,而在磁盘中,我们用这个 key 作为图片的文件名。 对于一个远程服务器下载的图片,其 url 是作为这个 key 的最佳选择了。

相关知识点

各种工具类

  • 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做了流程优化。

dispatch_barrier_sync函数

该方法用于对操作设置屏幕,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。

NSMutableURLRequest

用于创建一个网络请求对象,我们可以根据需要来配置请求中的等等信息。

NSOperation 及 NSOperationQueue

关于NSOperation我们可以看到官方文档中的解释:
表示与单个任务相关联的代码和数据的抽象类。
因为NSOperation类是一个抽象类,你不能直接使用它,而是使用它的子类或使用系统定义的子类(NSInvocationOperation或NSBlockOperation)来执行实际的任务。尽管是抽象的,NSOperation的基本实现确实包含了重要的逻辑来协调任务的安全执行。这种内置逻辑的存在允许您将注意力集中在任务的实际实现上,而不是确保它与其他系统对象正确工作所需的粘合代码上。通常通过将操作添加到操作队列(NSOperationQueue类的实例)来执行操作。如果不想使用操作队列,可以直接从代码中调用操作的start方法来执行操作。手动执行操作会给代码带来更大的负担,因为启动未处于就绪状态的操作会触发异常。就绪属性报告操作准备就绪。

操作队列是 Objective-C 中一种高级的并发处理方法,现在它是基于 GCD 来实现的。相对于 GCD 来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对 SDWebImage 中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。

NSURLConnection

用于网络请求及响应处理。在 iOS7.0 后,苹果推出了一套新的网络
请求接口,即 NSURLSession 类,开启一个后台任务。
在这里插入图片描述
在这里插入图片描述

NSCache 类

NSCache 是一个类似于集合的容器。它存储 key-value 对,这一点类似于 NSDictionary 类。 我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。 重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

清理缓存图片的策略:

如果所有缓存文件的总大小超过最大缓存空间的大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。

对图片的解压缩操作:

SDWebImageDecoder.m 中+decodedImageWithImage 方法的实现。

异步下载

NSOperation + 操作队列

解决图片错位问题,需要判定 cell 对应的图片地址已经改变

给每一个 imageView 都绑定一个下载地址. 如果外界传入的下载地址改变, 让
iamgeView 绑定的地址变成新的地址,原来的下载操作取消.开始新的下载操作.

如何给imageView 绑定下载地址

利用运行时,在分类中动态的为 imageView 添加一个属性(urlString).

SDWebImage下载的流程

使用流程

SDWebImage 的主要任务就是图片的下载和缓存。
在这里插入图片描述

实现步骤

1. 入口setImageWithUrl:placeHolderImage:options:会把placeHolderImage显示,然后SDWebImageManager根据URL开始处理图片.

2. 进入SDWebImageManager-downloadWithURL:delegate:options:userInfo:交给SDImageCache从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:

3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调imageCache:didFineImage:forKey:userInfo:到SDWebImageManager.

4. SDWebImageManagerDelegate回调webImageManager:didFinishWithImage:到UIImageView + WebCache等前端展示图片.

5. 如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存

6. 根据URLKey在硬盘缓存目录下尝试读取图片文件.这一步是在NSOperation进行的操作,所以回主线程进行结果回调notifyDelegate.

7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小 会先清空内存缓存).SDImageCacheDelegate 回调imageCache:didFinishImage:forKey:userInfo:进而回调展示图片.

8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo.

9. 共享或重新生成一个下载器SDWebImageDownLoader开始下载图片

10. 图片下载由NSURLConnection来做,实现相关delegate来判断图片下载中,下载完成和下载失败

11. connection:didReceiveData:中利用ImageIO做了按图片下载进度加载效果

12. connectionDidFinishLoading:数据下载完成后交给SDWebImageDecoder做图片解码处理

13. 图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI.如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多.

14. 在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成imageDecoder:didFinishDecodingImage:userInfo:回调给SDWebImageDownloader

15. imageDownLoader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成

16. 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片

17. 将图片保存到SDImageCache中内存缓存和硬盘缓存同时保存,写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程

18. SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片

19. SDWI也提供UIButton + WebCache和MKAnnptation + WebCache方便使用

20. SDWebImagePrefetcher 可以预先下载图片,方便后续使用

在这里插入图片描述

源码分析

一点一点来看其对应源码

UIImageView+WebCache

取消当前的下载流

首先是入口处的setImageWithUrl:这一部分

- (void)sd_setImageWithURL:(NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}

这些方法都对应着其函数

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    
    if (url) {

        // check if activityView is enabled or not
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }

        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            [wself removeActivityIndicator];
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

其总共有五个参数,URL就是我们需要下载的在线图片链接,placeholder(占位符)Image其是UIImage类型,而SDWebImageOptions我们查看其源码并进行相关信息的查询
其是一种暴露在外的可供使用者使用的选择方法

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    // 默认情况下,当URL下载失败时,URL会被列入黑名单,导致库不会再去重试,该标记用于禁用黑名单
    SDWebImageRetryFailed = 1 << 0,
    // 默认情况下,图片下载开始于UI交互,该标记禁用这一特性,这样下载延迟到UIScrollView减速时
    SDWebImageLowPriority = 1 << 1,
    // 该标记禁用磁盘缓存
    SDWebImageCacheMemoryOnly = 1 << 2,
    // 该标记启用渐进式下载,图片在下载过程中是渐渐显示的,如同浏览器一下。
    // 默认情况下,图像在下载完成后一次性显示
    SDWebImageProgressiveDownload = 1 << 3,
    // 即使图片缓存了,也期望HTTP响应cache control,并在需要的情况下从远程刷新图片。
    // 磁盘缓存将被NSURLCache处理而不是SDWebImage,因为SDWebImage会导致轻微的性能下载。
    // 该标记帮助处理在相同请求URL后面改变的图片。如果缓存图片被刷新,则完成block会使用缓存图片调用一次
    // 然后再用最终图片调用一次
    SDWebImageRefreshCached = 1 << 4,
    // 在iOS 4+系统中,当程序进入后台后继续下载图片。这将要求系统给予额外的时间让请求完成
    // 如果后台任务超时,则操作被取消
    SDWebImageContinueInBackground = 1 << 5,
    // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES;来处理存储在NSHTTPCookieStore中的cookie
    SDWebImageHandleCookies = 1 << 6,
    // 允许不受信任的SSL认证
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    // 默认情况下,图片下载按入队的顺序来执行。该标记将其移到队列的前面,
    // 以便图片能立即下载而不是等到当前队列被加载
    SDWebImageHighPriority = 1 << 8,
    // 默认情况下,占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成
    SDWebImageDelayPlaceholder = 1 << 9,
    // 通常我们不调用动画图片的transformDownloadedImage代理方法,因为大多数转换代码可以管理它。
    // 使用这个票房则不任何情况下都进行转换。
    SDWebImageTransformAnimatedImage = 1 << 10,
};

一行一行的看
先是[self sd_cancelCurrentImageLoad];这个函数 取消当前图像加载?点开它的下一层

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

通过键值取消当前图像的加载,键值是UIImageViewImageLoad,我们继续点开下一层看到sd_cancelImageLoadOperationWithKey这个方法的实现,其有注释:Cancel in progress downloader from queue,从队列中取消正在进行的下载。

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey: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];
    }
}

来了解一下其是怎样实现取消队列中所有操作的下载。
也是一行一行的看NSMutableDictionary *operationDictionary = [self operationDictionary];
我们点开 operationDictionary的源码

- (NSMutableDictionary *)operationDictionary {
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

通过键值来得到绑定的的关联对象(分类,可以通过它来扩展方法;关联对象,可以通过它来扩展属性)的值赋值给可变字典类型的operations,如果operations不为空,那么返回该关联对象的值。暂时就这样理解吧(runtime还没学555)如果为空,就通过键值与关联策略(传递nil以清除现有关联)为给定对象设定关联值。这就是operationDictionary的实现。

这个方法在取消后面的下一行也有实现,其一开始默认的loadOperationKey为静态变量,没有进行赋值,所以如果我们第一次进入点击下载按钮,其绑定就为空。
再说回来我们来看一下取消队列中下载的实现,其先为每个每一个UIView+WebCache 创建operationDictionary。假设我们第一次进行这个操作,所以其不会进入第一个if中进行判断,因为上面说的一开始默认的loadOperationKey为静态变量,没有进行赋值操作。自然也没有key值,所以后面的if也不会走。
同样我们也看一下如果前面已经有相同的key值其是怎么进行取消与判断的,后面的代码。

如果有两个键值相同的内容进入队列,那么通过前面字典的创建得到的关联对象的值也是一样的,传入key在返回的字典中查找是否已经存在,如果存在则取消所有操作,conformsToProtocol方法如果符合这个协议(协议中声明了取消方法),也调用协议中的取消方法。

好处

我们已经知道其目的了,第一步就是取消队列中所有操作的下载。
当我们调用SDWebImage这个库时,setImageWithUrl:placeHolderImage:options:这个方法会先取消正在进行的下载这样有好处

  • 因为其是针对的一个UIImageView,取消前一个操作,省时、省流量
  • 避免SDWebImage的复用。 也就是避免对一张图片进行重复下载。加载图片完成后, 回调时会先检查任务的Operation还在不在, 不在,则不回调显示, 反之回调显示并移除Operation.
  • 当程序中断导致链接失效时,当前的下载还在操作队列中,但是按道理应该是失效状态,我们可以通过先取消当前正在进行的下载来保证操作队列中的链接不存在这种情况。
    这仅仅是第一行代码

占位图的实现

继续向下看第二行,以每个需要下载的图像的url为加载的键值进行绑定。
这样每个都有自己所对应的键值。

再下一步

if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

先说什么是占位图,占位图就是页面还没有加载完或者加载失败时候显示的图片。我个人的理解就是图片需要加载的时候,其会先申请这个图片的空间,后面再慢慢加载,先占住图片的位置然后进行加载或者什么策略。官方文档对于占位符是这样解释:占位符元素通常用于数据绑定,并在运行时被有效元素替换。

上面解释过SDWebImageDelayPlaceholder是SDWebImageOptions的一个方法,默认情况下,占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成。

回到源码

看一下其是怎么完成这个的判断的

因为定义的SDWebImageDelayPlaceholder是枚举类型,其通过按位与来进行判断,默认为位移9,就是枚举值为2的9次方512,按位于,假设我们在sd_setImageWithURL中给options添加了枚举成员SDWebImageDelayPlaceholder来延迟占位图片的加载,那么 if (!(options & SDWebImageDelayPlaceholder))中的判断的值则为512(1 0000 0000 & 1 0000 0000 = 0),如果options没有设置,那么判断的值为0(0 0000 0000 & 1 0000 0000 = 0)。 这也是用枚举表示状态、选项、状态码的最常见应用。

如果传入的 options 中没有添加枚举成员 SDWebImageDelayPlaceholder,那么就会为 UIImageView 添加一个临时的 image, 也就是占位图。考虑到下载可能是异步的操作,而图像的绘制只能在主线程完成,SDWebImage也提供了判断是否在主线程的方法

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
	NSLog(@"123");\  //我自己进行测试加的
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

宏定义通过判断是否在当前线程是否是主线程从而执行block中的方法。

我们在设置占位图之后进行对self.image和placeholder进行信息的打印,结果也是一样的。
所以虽然测试时候不会专门走到这一步,但是其是通过宏定义已经执行了的。

获取图片

接下来会检测传入的URL是否为空,如果不是空那么会先进入判断

  if (url) {

        // check if activityView is enabled or not
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }

首先检查加载的那个小菊花是否有,如果没有就添加一个小菊花
之前一直没有发现过SDWebImage会自带加载的小菊花,换了一个7MB的图像加黑底的占位图才发现是之前加载的太快了,导致小菊花还没有加载出来就结束了,并且白底不设置确实看不清。

然后进行下一步
一个全局的 SDWebImageManager 就会调用以下的方法获取图片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

在下载完成后起会调用(SDWebImageCompletionWithFinishedBlock)completedBlockUIImageView.image赋值,添加上最终所需要的图片。

dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });

在最后,[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]返回operation的同时,也会向operationDictionary中添加一个键值对,来表示操作的正在进行

[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

看一下这个方法的实现

- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}

即先取消当前UIImageView正在下载的任务,然后在保存operations

首先还是先去取消当前队列的下载,然后以其key值新建一个operationDictionary。
到此为止已经对 SDWebImage 框架中的这一方法分析完了,下篇继续学习SDWebImageManager中的[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]的具体实现。

SDWebImageManager

上面说过其概念

拥有一个SDWebImageCache和SDWebImageDownloader属性分别用于图片的缓存和下载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合。这个类是一个单例。还有就是各种加载选项的处理。

这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader, SDImageCache 之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.

先是一开始的代码

    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

这两行代码是用来保证传入的URL是正确的

当被传入后,会创意一个operation,其是一个遵循SDWebImageOperation协议的NSObject的子类。SDWebImageCombinedOperation与NSOperation的相同之处就是其实现了Cancel方法,会使得其持有两个operation都被Cancel。

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        _cancelBlock = nil;
    }
}

我们继续往下看,下面还是一些URL的判错

 BOOL isFailedUrl = NO;
 @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

failedURLs是静态局部变量,@synchronized(还没学)不过之前有了解,开发中,在多个线程访问同一块资源的时候,我们会加锁来避免引发数据错乱和数据安全的问题。containsObject:判定中是否已经存在URL,得到isFailedUrl的布尔值。

如果url的长度为0或者URL被列入了黑名单那么就报错

如果前面URL的错误判断已经结束,那么我们这个时候就获取到了正确的URL,再通过URL获取到其对应的key

NSString *key = [self cacheKeyForURL:url];

下一步是通过key在缓存中查找以前是否下载过相同的图片

operation.cacheOperation = [self.imageCache 
		queryDiskCacheForKey:key 
        			    done:^(UIImage *image, SDImageCacheType cacheType) { ... }];

这里调用 SDImageCache 的实例方法 queryDiskCacheForKey:done: 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 NSOperation.

如果我们在缓存中查找到了对应的图片, 那么我们直接调用 completedBlock 回调块结束这一次的图片下载操作.

dispatch_main_sync_safe(^{
    completedBlock(image, nil, cacheType, YES, url);
});

如果我们没有找到图片, 那么就会调用 SDWebImageDownloader 的实例方法:

id <SDWebImageOperation> subOperation =
  [self.imageDownloader downloadImageWithURL:url 
                                     options:downloaderOptions 
                                    progress:progressBlock 
                                   completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];

如果这个方法返回了正确的 downloadedImage, 那么我们就会在全局的缓存中存储这个图片的数据:

[self.imageCache storeImage:downloadedImage 
	   recalculateFromImage:NO 
                  imageData:data 
                     forKey:key 
                     toDisk:cacheOnDisk];

并调用 completedBlock 对 UIImageView 或者 UIButton 添加图片, 或者进行其它的操作.

最后, 我们将这个 subOperation 的 cancel 操作添加到 operation.cancelBlock 中. 方便操作的取消.

operation.cancelBlock = ^{
    [subOperation cancel];
    }

SDWebImageCache

在其.h文件中我们可以看到这样一句话
它维护了一个内存缓存和一个可选的磁盘缓存

SDWebImageDownloader

SDWebImageDownloaderOperation

相关问题与理解

避免以后忘记记录一下相关问题与我暂时的理解

一、options 是什么?

options是我们这里选择的操作方式,其是SDWebImageOptions类的,SDWebImageOptions是NSUInteger类型的一个枚举变量,按位与来进行判断状态、选项、状态码等等。

二、UIImageViewImageLoad对应的是什么操作?下载还是加载?

UIImageViewImageLoad对应的是图片正在下载中的操作

三、下面这个方法给固定的key有什么用?

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

当我们加载完了一个图片,我们可以通过先给他一个固定的key“UIImageViewImageLoad”记录其开始下载了,当第二次下载开始时,我们先调用取消,通过“UIImageViewImageLoad”的key来进行取消。

四、关联对象是怎样实现URL的绑定呢?

不会,后面学了来补充

不想写了 后续想写再补上了吧

先写一些值得记录的总结吧

总结

SDWebImage如何为UIImageView添加图片?

SDWebImage 中为 UIView 提供了一个分类叫做 WebCache, 这个分类中有一个最常用的接口, sd_setImageWithURL:placeholderImage:, 这个分类同时提供了很多类似的方法, 这些方法最终会调用一个同时具有 option progressBlock completionBlock 的方法, 而在这个类最终被调用的方法首先会检查是否传入了 placeholderImage 以及对应的参数, 并设置 placeholderImage.

然后会获取 SDWebImageManager 中的单例调用一个 downloadImageWithURL:… 的方法来获取图片, 而这个 manager 获取图片的过程有大体上分为两部分, 它首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以 url 作为数据的索引先在内存中寻找是否有对应的缓存, 如果缓存未命中就会在磁盘中利用 MD5 处理过的 key 来继续查询对应的数据, 如果找到了, 就会把磁盘中的缓存备份到内存中.

然而, 假设我们在内存和磁盘缓存中都没有命中, 那么 manager 就会调用它持有的一个 SDWebImageDownloader 对象的方法 downloadImageWithURL:… 来下载图片, 这个方法会在执行的过程中调用另一个方法 addProgressCallback:andCompletedBlock:forURL:createCallback: 来存储下载过程中和下载完成的回调, 当回调块是第一次添加的时候, 方法会实例化一个 NSMutableURLRequest 和 SDWebImageDownloaderOperation, 并将后者加入 downloader 持有的下载队列开始图片的异步下载.

而在图片下载完成之后, 就会在主线程设置 image 属性, 完成整个图像的异步下载和配置.

  • 查看缓存
    • 缓存命中
      • 返回图片
      • 更新 UIImageView
    • 缓存未命中
      • 异步下载图片
      • 加入缓存
      • 更新 UIImageView
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值