项目中一直都有使用SDWebImage
,对这个框架有一定的了解,但是体系却未能贯通,因此特地整理下,主要参考:
iOS 源代码分析 --- SDWebImage
SDWebImage源码剖析(-)
SDWebImage源码剖析(二)
一. 简介
SDWebImage
提供了一个异步下载图片并且支持缓存的UIImageView
分类。
主要逻辑为:
- 查看缓存,如果缓存中存在图片就返回图片并且更新
UIImageView
. - 缓存中不存在图片就异步下载图片,加入缓存,更新
UIImageView
.
主要用到的对象:
1、UIImageView (WebCache)
类别,入口封装,实现读取图片完成后的回调
2、SDWebImageManager
,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache
(调用SDImageCache
),或者向网络请求下载对象(调用SDWebImageDownloader
) 。
实现SDImageCache
和SDWebImageDownloader
的回调。
3、SDImageCache
,根据URL的MD5生成key对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4、SDWebImageDownloader
,根据URL
向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)
其他类:
SDWebImageDecoder,异步对图像进行了一次解压。
具体流程图:
SDWebImage
加载图片的流程 :
入口
setImageWithURL:placeholderImage:options:
会先把placeholderImage
显示,然后 SDWebImageManager 根据 URL 开始处理图片。进入
SDWebImageManager-downloadWithURL:delegate:options:userInfo:
,交给SDImageCache
从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:
.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,
SDImageCacheDelegate
回调imageCache:didFindImage:forKey:userInfo:
到SDWebImageManager
。
4.SDWebImageManagerDelegate
回调 webImageManager:didFinishWithImage:
到 UIImageView+WebCache
等前端展示图片。
如果内存缓存中没有,生成
NSInvocationOperation
添加到队列开始从硬盘异步查找图片是否已经缓存。根据
URLKey
在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调notifyDelegate:
。如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。
SDImageCacheDelegate
回调imageCache:didFindImage:forKey:userInfo:
。进而回调展示图片。如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调
imageCache:didNotFindImageForKey:userInfo:
。共享或重新生成一个下载器
SDWebImageDownloader
开始下载图片。图片下载由
NSURLConnection
来做,实现相关delegate
来判断图片下载中、下载完成和下载失败。connection:didReceiveData:
中利用ImageIO
做了按图片下载进度加载效果。connectionDidFinishLoading:
数据下载完成后交给SDWebImageDecoder
做图片解码处理。图片解码处理在一个
NSOperationQueue
完成,不会拖慢主线程UI
。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。在主线程
notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:
回调给SDWebImageDownloader
。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
将图片保存到
SDImageCache
中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation
完成,避免拖慢主线程。SDImageCache
在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。SDWebImage
也提供了UIButton+WebCach
e 和MKAnnotationView+WebCache
,方便使用。SDWebImagePrefetcher
可以预先下载图片,方便后续使用。
二. 架构简介
A.架构图:
UIImageView+WebCaceh
和UIButton+WebCache
直接为UIkit框架提供接口,而SDWebImageManger
负责处理和协调SDWebImageDownloader
和SDWebImageCache
并与UIkit
层进行交互。
三. 具体分析
1.UIImageView+WebCache
A.框架常用入口
// 所有设置图片最终都会调用这个方法
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url
placeholderImage:placeholder
options:0
progress:nil
completed:nil];
}
该接口调用下面这个方法:
[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
该方法作为sd_setImageWithURL接口的最终入口,提供了多种参数。
url
:远程图片的地址placeholder
: 预显示图片-
options
:SDWebImageOptions
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { //下载失败了会再次尝试下载 SDWebImageRetryFailed = 1 << 0, //当UIScrollView等正在滚动时,延迟下载图片(放置scrollView滚动卡) WebImageLowPriority = 1 << 1, //只缓存到内存中 SDWebImageCacheMemoryOnly = 1 << 2, // 图片会边下边显示 SDWebImageProgressiveDownload = 1 << 3, // 将硬盘缓存交给系统自带的NSURLCache去处理 SDWebImageRefreshCached = 1 << 4, //后台下载 SDWebImageContinueInBackground = 1 << 5, // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie SDWebImageHandleCookies = 1 << 6, // 允许不受信任的SSL证书。主要用于测试目的。 SDWebImageAllowInvalidSSLCertificates = 1 << 7, // 默认情况下,image在装载的时候是按照他们在队列中的顺序装载的(就是先进先出).这个flag会把他们移动到队列的前端,并且立刻装载,而不是等到当前队列装载的时候再装载 SDWebImageHighPriority = 1 << 8, // 默认情况下,占位图会在图片下载的时候显示.这个flag开启会延迟占位图显示的时间,等到图片下载完成之后才会显示占位图 SDWebImageDelayPlaceholder = 1 << 9, // 是否transform图片 SDWebImageTransformAnimatedImage = 1 << 10, };
progress
:下载进度
B.代码分析:
操作的管理:
- (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);
// 如果选项非SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 设置占位图
self.image = placeholder;
});
}
if (url.absoluteString.length > 0) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
// 显示 下载转圈
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
id 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);
}
});
}
}
[self sd_cancelCurrentImageLoad]
;取消当前的下载操作,它表明 SDWebImage
管理操作的方法:
SDWebImage
所有的操作实际都是通过一个 operationDictionary
的字典管理,这个字典是动态添加到 UIView
上的一个属性,因为这个operationDictionary
需要在UIButton
和 UIImageView
上重用,所以需要添加到它们的根类上。
这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:
// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
这行代码会取消当前这个UIImageView
的所有操作,不会影响之后进行的下载操作。
占位图的实现:
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;
}
当options
中没有SDWebImageDelayPlaceholder
,UIImageView添加一个占位图image.
获取图片:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)
检测传入的URL
是否为空,如果非空就调用全局的SDWebImageManager
来获取图片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
下载完成后调用(SDWebImageCompletionWithFinishedBlock)completedBlock 为 UIImageView.image 赋值, 添加上最终所需要的图片.
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #10
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);
}
});
最后在返回 operation
的同时, 也会向 operationDictionary
中添加一个键值对, 来表示操作的正在进行:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
它将operation 存储到operationDictionary 中方便以后的cancel操作。
2. SDWebImageManager
这个类主要用于处理异步下载和图片缓存的类,也可以直接用SDWebImageManager
的downloadImageWithURL:options:progress:completed:
来直接下载图片。
可以看出这个类主要作用就是为了UIImageView+WebCache
和 SDWebImageDownloader
, SDImageCache
之间构建一个桥梁,使它们能够更好的协同工作。
A.核心代码分析:
a.SDWebImageManager
// SDWebImageManager
//- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
这块代码的功能是确定 url是否被正确传入, 如果传入参数的是 NSString
类型就会被转换为NSURL
, 如果转换失败, 那么url
会被赋值为空, 这个下载的操作就会出错.
b. SDWebImageCombinedOperation
当 url
被正确传入之后, 会实例一个非常奇怪的 operation
, 它其实是一个遵循 SDWebImageOperation
协议的 NSObject
的子类. 而这个协议也非常的简单:
@protocol SDWebImageOperation
- (void)cancel;
@end
SDWebImageOperation
只是看着像NSOperation
但是它唯一跟NSOperation
相同就是都可以响应cancel
方法。调用这个类的cancel
方法,会使得它持有的两个operation
都被cancel
。
// SDWebImageCombinedOperation
// cancel #1
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
既然获取了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
回调块结束这一次图片的下载操作
// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});
如果没有找到就调用SDWebImageDownLoader的实例方法去下载该图片:
id 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]; }
3. SDWebImageCache
维护了一个内存缓存和一个可选的磁盘缓存,首先看下查询图片缓存的方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
该方法主要功能是异步查询图片缓存,先在内存中查找
// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 内存中查找图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
imageFromMemoryCacheForKey:key
方法会在SDWebImageCache 维护的缓存memCache
中查找是否有对应的数据,而 memCache
就是一个 NSCache
.
NSCache
是一个类似于 NSMutableDictionary
存储 key-value
的容器,主要有以下几个特点:
自动删除机制:当系统内存紧张时,NSCache
会自动删除一些缓存对象
线程安全:从不同线程中对同一个 NSCache
对象进行增删改查时,不需要加锁
不同于 NSMutableDictionary
,NSCache
存储对象时不会对key
进行 copy
操作
如果在内存中没有找到图片的缓存的话,就需要在磁盘中查找。
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
得到图片对应的NSData
后还有经过:
- 根据图片的不同种类,生成对应的UIImage,
- 根据
key
值,调整image
的Scale
值 - 如果设置图片需要解压缩,需要对图片进行解码
对图片进行存储需要对url
进行MD5
加密计算生成相应的key
值:
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}
然后用该key
作为图片文件名存储在默认路径下:
// 获取缓存路径方法(自己写的)
- (NSString*)getCachePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
NSString *path = [paths[0] stringByAppendingFormat:@"/com.hackemist.SDWebImageCache.default"];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}else{
return nil;
}
}
之前做朋友圈后台发送图片就是先将小图命名,然后根据获取到的七牛的domain和token,拼出url,接着将该url,进行md5
加密,加密后存储到SDWebImage
的默认存储路径下,然后在主界面显示存储的小图,后台去进行图片压缩上传任务。
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
如果在磁盘中找到图片,就将他复制到内存中,以便下次使用。
4.SDWebImageDownloader
专用的并且优化的图片异步下载器,主要用来下载图片,下载放在NSOperationQueue
中进行,默认maxConcurrentOperationCount
为6,timeout时间为15s.
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
该方法直接调用了下载进度回调函数:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
dispatch_barrier_sync(self.barrierQueue, ^{
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
中存储的回调块.
通过dispatch_barrier_async
函数提交的任务会等它前面的任务执行完才开始,然后它后面的任务必须等它执行完毕才能开始. 必须使用dispatch_queue_create
创建的队列才会达到上面的效果.通过该函数来保证每张图片进度顺序。
如果是第一次添加回调块,那么就会直接运行这个createCallBack
这个block
,而这个block
,就是我们在downloadImageWithURL:options:progress:completed:
中传入的回调块.
接着分析下NSMutableURLRequest
请求:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
该request
发送了一个http
请求,接着又初始化一个SDWebImageDownloaderOperation
实例,这个实例用于请求网络资源的操作,是NSOperation
的子类:
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
初始化之后,将该operation
添加到NSOperationQueue
中。(备注:NSOperation
实例只有在调用start
方法或者加入NSOperationQueue
才会执行)
[wself.downloadQueue addOperation:operation];
5.SDWebImageDownloaderOperation
这个类主要处理HTTP
请求,URL
连接的类,当这个类的实例被加入到队列之后,start
方法被调用,start
方法首先产生一个NSURLConnection
,通过NSURLConnection
进行图片的下载,为了确保能够处理下载的数据,需要在后台运行runloop
,保证程序不被挂起.
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//在主线程发通知,这样也保证在主线程收到通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
接下来这个 connection
就会开始运行:
[self.connection start];
它发出一个SDWebImageDownloadStartNotification
通知,开启状态栏的请求加载转圈。同时调用NSURLConnectionDataDelegate
代理
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
前两个代理会不停的回调 pregressBlock
来提示下载进度。
而最后一个代理方法会在图片下载完成之后调用completionBlock
来完成最后 UIImageView.image
的更新,而这里调用的 progressBlock
,completionBlock
, cancelBlock
都是在之前存储在 URLCallbacks
字典中的.
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
// 停止 该线程 运行时
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
// 通知停止状态栏转圈请求
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
if (completionBlock) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {
// 进行缓存
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
// 进行解码
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
completionBlock(image, self.imageData, nil, YES);
}
} else {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
self.completionBlock = nil;
[self done];
}
转换处理图片和进行缓存后,将下载image
赋值给控件。
四. 面试点
1、SDImageCache
是怎么做数据管理的?
SDImageCache
分成两部分,一个是内存层面的,一个是磁盘层面的。内存缓存的处理是使用
NSCache
对象来实现的。NSCache
是一个类似于集合的容器。它存储key-value
对,这一点类似于NSDictionary
类,用搜索文件系统的方式做管理,文件替换方式是以时间为单位。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。磁盘缓存的处理则是使用
NSFileManager
对象来实现的。图片存储的位置是位于Cache
文件夹,文件替换方式是以时间为单位,剔除时间大一一周的图片文件。当
SDWebImageManager
向SDImageCache
要资源时, 先搜索内存层面的数据,如果有直接返回,没有再访问磁盘,如果有将图片从磁盘读取出来,然后做解压,将图片对象放到内存层面做备份,再返回调用层。
- 为什么图片要进行解压?
- 因为
UIImage
的imageWithData
函数是每次画图的时候才将Data
解压成ARGB
图像,所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求,为了提高效率,通过SDWebImageDecoder
将包装在Data
下的资源解压,然后画在另外一张图片上面,这样这张图片就不需要重复解压了,这种做法就是典型的空间换取时间的做法。
3.SDWebImage
在多线程下载图片时防止错乱的策略
SDWebImage
会将ImageView
对象关联一个下载列表(列表是给AnimationImages
用的,这个时候会下载多张图片),当tableView滚动时,imageView会重设数据源url
,这时会cancel掉下载列表中当前对应的下载任务,然后开启一个新的下载任务,这样就保证只有当前可见的cell
对象的ImageView
对象关联的下载任务能够回调,不会发生Image
错乱。同时,
SDWebImage
管理了一个全局下载队列SDWebDownloadManager
,并发量设置为6,也就表示如果cell
的数目大于6,就会有部分下载队列处于等待状态,而且,在添加下载任务到全局的下载队列中去的时候,SDWebImage
默认采取的是LIFO(后进先出)策略,具体是添加新的下载任务的时候,将之前的下载任务添加依赖为新的下载任务。
另外解决方案:
将
imageView
对象和图片的url
相关联,在滑动时,不取消旧的下载任务,而是在下载任务完成回调时,进行url
匹配,只有匹配成功的image
会刷新imageView
对象,而其他的image
则只做缓存操作,而不刷新UI
。同时,仍然管理一个执行队列,为了避免占用太多的资源,通常会对执行队列设置一个最大的并发量。此外,为了保证
LIFO
的下载策略,可以自己维持一个等待队列,每次下载任务开始的时候,将后进入的下载任务插入到等待队列的前面。
-
SDWebImage
的主要任务就是图片的下载和缓存。为了支持这些操作,它主要使用了以下知识点:
dispatch_barrier_sync
函数:该方法用于对操作设置屏障,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。NSMutableURLRequest
:用于创建一个网络请求对象,我们可以根据需要来配置请求报头等信息。
NSOperation
及NSOperationQueue
:操作队列是Objective-C
中一种高级的并发处理方法,现在它是基于GCD
来实现的。相对于GCD
来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对SDWebImage
中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。
-
NSURLConnection
:用于网络请求及响应处理。在iOS7.0
后,苹果推出了一套新的网络请求接口,即NSURLSession
类。
开启一个后台任务。
NSCache
类:一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
对图片的解压缩操作:这一操作可以查看
SDWebImageDecoder.m
中+decodedImageWithImage
方法的实现。对
GIF
图片的处理对
WebP
图片的处理
- 系统级内存警告如何处理
- 取消当前正在进行的所有下载操作
[[SDWebImageManager sharedManager] cancelAll];
- 清除缓存数据:
内存缓存:直接删除文件,重新创建新的文件
磁盘缓存:删除过期的文件数据,计算当前未过期的已经下载的文件数据的大小,如果发现该数据大小大于我们设置的最大缓存数据大小,那么程序内部会按照按文件数据缓存的时间从远到近删除,知道小于最大缓存数据为止。
- 如何播放
gif
图片
- 把用户传入的gif图片->
NSData
- 根据该
Data
创建一个图片数据源(NSData
->CFImageSourceRef
) - 计算该数据源中一共有多少帧,把每一帧数据取出来放到图片数组中
- 根据得到的数组+计算的动画时间-》可动画的image
- [UIImage animatedImageWithImages:images duration:duration];
- 如何判断当前图片类型
+ (NSString *)sd_contentTypeForImageData:(NSData *)data;
图片的十六进制数据, 的前8个字节都是一样的, 所以可以通过判断十六进制来判断图片的类型
五. 最后
送上一张自己喜欢的图片:
个人小结,有兴趣的朋友可以看一下,如果觉得不错,麻烦给个喜欢或star,若发现问题请及时反馈,谢谢!