SDWebImage分析

2017-01-11  本文已影响97人  心里的另一个你

一个异步下载图片并且支持缓存的UIImageView分类.
参考文章
(https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/SDWebImage/iOS%20源代码分析%20---%20SDWebImage.md)
(http://www.cnblogs.com/dreamDeveloper/p/6052764.html)
本人是开发小白,这里只是作为日常笔记整理.
经常使用的方法:

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                 placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

异步加载图片的时候,先加载占位图片.
UIImageView+WebCache 为UIKit框架提供接口,SDWebImageManager负责处理和协调SDWebImageDownLoader(下载)和SDWebImageCahce(缓存).并与UIKit层进行交互,而底层的一些类为更高层级的抽象提供支持.
UIImageView+WebCache

- (void)sd_setImageWithURL:(NSURL *)url 
          placeholderImage:(UIImage *)placeholder;

内部实现

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

方法的作用是用于调用webCache的核心代码

[self sd_setImageWithURL:placeholderImage:options:progress:completed:]

很多的 sd_setImageWithURL...... 方法, 它们最终都会调用上面这个方法, 只是根据需要传入不同的参数.

关于操作的管理
这个方法的第一个代码

[self sd_cancelCurrentImageLoad];

框架中的所有操作实际上是通过一个 operationDictionary来管理,而这个字典实际上也是动态的添加到UIView上的一个属性,原因是operationDictionary需要在UIButtonUIImageView上重用,所以需要添加到他们的根类,也就是view上.

[self sd_cancelCurrentImageLoad];

这行代码是为了保证没有当前没有正在执行的异步下载操作,不会与即将进行的操作发生冲突,它会调用下列代码:

[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]

从而使当前UIImageView中的所有操作都被cancel,不会影响之后进行的下载操作.

占位图的实现

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

如果传入的options中没有SDWebImageDelayPlaceholder(默认情况下 options == 0),那么就会为UIImageView添加一个临时的image,也就是占位图.

获取图片

if (url)

接下来会检测传入的url是否为空,如果非空那么一个全局的** SDWebImageManager**就会调用以下的方法获取图片:

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

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

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

dispatch_main_sync_safe宏定义

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

宏的作用:图像的绘制只能在主线程完成,所以,dispatch_main_sync_safe就是保证block在主线程中执行.
最后,在[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]返回operation的同时,也会向operationDictionary中添加一个键值对,来表示操作的正在进行:

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

它将opertion存储到operationDictionary中,方便以后的cancel.

SDWebImageManager

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

这个类就是隐藏在UIImageView+WebCache背后,用于处理异步下载和图片缓存的类,当然也可以直接使用SDWebImageManager的上述方法downloadImageWithURL:options:progress:completed:来直接下载图片.
这个类的主要作用就是为UIImageView+WebCacheSDWebImageDownloader,SDImageCache之间构建一个桥梁,使它们能够更好的协同工作,我们在这里分析这个核心方法的源代码,它是如何协调异步下载和图片缓存的.

//判断url是否被正确传入,如果是正确传入NSString,就会转化成NSURL
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}
//如果转换失败,url赋值为空,下载操作出错
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

SDWebImageCombinedOperation

url被正确传入之后,会实例一个非常奇怪的"operation",它其实是一个遵循SDWebImageOperation协议的NSObject的子类,而这个协议也非常的简单:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

这里仅仅是将这个SDWebImageOperation类包装成一个看着像NSOperation其实并不是NSOperation的类,而这个类唯一与NSOperation的相同之处就是它们都可以响应cancel方法.

而调用这个类的存在实际是为了使代码更加的简洁,因为盗用这个类的cancel方法,会使得它持有的两个operation都被cancel.

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

而这个类,应该是为了实现更简洁的cancel操作而设计出来的.

既然我们获取了url,再通过url获取对应的key.

NSString *key = [self cacheKeyForURL:url];

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

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

如果我们在缓存中查找到了对应的图片,那么我们直接调用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];

并调用completedBlockUIImageView添加图片,或者其它操作.
最后,我们将这个subOperationcancel操作添加到operation.cancelBlock中,方便操作的取消.

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

SDWebImageCache

它维护了一个内存缓存和一个可选的磁盘缓存,在上一阶段没有解读的两个方法,首先是:

//根据key值查询磁盘
- (NSOperation *)queryDiskCacheForKey:(NSString *)key 
                                 done:(SDWebImageQueryCompletedBlock)doneBlock;

这个方法的主要功能是异步的查询图片缓存,因为图片的缓存可能在两个地方,而该方法首先会在内存中查找是否有图片的缓存.

UIImage *image = [self imageFromMemoryCacheForKey:key];

这个imageFromMemoryCacheForKey方法会在SDWebImageCache维护的缓存memCache中查找是否有对应的数据,而memCache就是一个NSCache.
如果在内存中并没有找到图片的缓存的话,就需要在磁盘中寻找了,这个就比较麻烦了...

首先说一下SDWebImage内部实现过程:

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

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

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

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

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

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

7.如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存操作).SDImageCacheDelegate回调imageCache: didFindImage: 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.SDWebImage也提供了UIButton+WebCacheMKAnnotationView+WebCache,方便调用.

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

以上是SDWebImage加载图片的过程.

对于图片文件的存储使用MD5处理:

CC_MD5(str, (CC_LONG)strlen(str), r);

如果在磁盘中查找到对应的图片,我们会将它复制都内存中,以便下次的使用.

UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
    CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
    [self.memCache setObject:diskImage forKey:key cost:cost];
}

以上是SDImageCache的核心内容.

如果缓存没有被命中,图片是如何下载的.

SDWebImageDownloader

专用的并且优化的图片异步下载器.
这个类的核心功能就是下载图片,而核心方法就是上面提到的:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url 
        options:(SDWebImageDownloaderOptions)options 
       progress:(SDWebImageDownloaderProgressBlock)progressBlock 
      completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
回调

这个方法直接调用了另一个关键的方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock 
                     forURL:(NSURL *)url 
             createCallback:(SDWebImageNoParamsBlock)createCallback

它为这个下载的操作添加回调的块,在下载进行时,或者在下载结束时执行一些操作.方法源代码:

BOOL first = NO;
if (!self.URLCallbacks[url]) {
   self.URLCallbacks[url] = [NSMutableArray new];
   first = YES;
}

// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

if (first) {
   createCallback();
}

方法会先查看这个url是否有对应的callback,使用的是downloader持有的一个字典URLCallBacks.
如果是第一次添加回调的话,就会执行first=YES,这个赋值很关键,因为first不为YES,那么HTTP请求就不会被初始化,图片也无法被获取.
然后,在这个方法中会重新修正在URLCallbacks中存储的回调块.

NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

如果是第一次添加回调块,那么就会直接运行这个createCallback这个block,而这个block,就是我们在前一个方法downloadImageWithURL: options: progress: completed:传入的回调块.

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];

下面是传入的无参数的代码,首先这段代码初始化了一个NSMutableURLRequest:

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] 
        initWithURL:url 
        cachePolicy:...
    timeoutInterval:timeoutInterval];

这个request就用于在之后发送HTTP请求.
在初始化了这个request之后,又初始化了一个SDWebImageDownloaderOperation的实例,就是用于请求网络资源的操作,它是一个NSOperation的子类.

operation = [[SDWebImageDownloaderOperation alloc] 
        initWithRequest:request
                options:options
               progress:...
              completed:...
              cancelled:...}];

但是在初始化之后,这个操作并不会开始(NSOperation实例只有在调用start方法或者加入NSOperation才会执行),我们需要将这个操作加入到一个NSOperationQueue中.

[wself.downloadQueue addOperation:operation];

只有将它加入到这个下载队列中,这个操作才会执行.

SDWebImageDownLoaderOperation

这个类就是处理HTTP请求,URL连接的类,当这个类的实例被加入到队列之后,start方法就会被调用,而start方法首先会产生一个NSURLConnection.

@synchronized (self) {
    if (self.isCancelled) {
        self.finished = YES;
        [self reset];
        return;
    }
    self.executing = YES;
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    self.thread = [NSThread currentThread];
}

而接下来这个connection就会开始运行:

[self.connection start];

它会发出一个SDWebImageDownloadStartNotification通知.

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];

代理

start方法调用之后,就是NSURLConnectionDataDelegate中代理方法的调用.

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

在这三个代理方法中的前两个会不停回调progressBlock来提示下载的进度.

而最后一个代理方法会在图片下载完成之后调用completionBlock来完成最后UIImageView.image的更新.

而这里调用的progressBlock completionBlock cancelBlock都在之前存储在URLCallbacks字典中的.

到这里,基本解析了SDWebImage

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                 placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

这个方法的全部过程.

图解

20150726190747372.png

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: 来存储下载过程中和下载完成的回调, 当回调块是第一次添加的时候, 方法会实例化一个 NSMutableURLRequestSDWebImageDownloaderOperation, 并将后者加入 downloader 持有的下载队列开始图片的异步下载.

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

SDWebImage 使用

1.使用UIImageView+WebCache categoryU来加载UITableViewcell的图片.

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

2.使用Blocks,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"] completed:
^(UIImage image, NSError error, SDImageCacheType cacheType, NSURL *imageURL)
 { ... completion code here ... 
}
];

3.使用SDWebImageManager,SDWebImageManagerUIImageView+WebCache category的实现提供接口。

SDWebImageManager manager = [SDWebImageManager sharedManager] ;

[manager downloadImageWithURL:imageURL options:0 progress:^(NSInteger   receivedSize, NSInteger expectedSize) 
{ 
// progression tracking code 
}   completed:^(UIImage image, NSError error, SDImageCacheType cacheType,   BOOL finished, NSURL imageURL) 
{ 
         if (image)
         { // do something with image 
         } 
}
];

至此,SDWebImage的全部分析到此为止.重申,只是作为个人笔记的整理,尊重原创.

上一篇下一篇

猜你喜欢

热点阅读