整体架构
按照分组方式,可以分为几组
定义通用宏和方法
-
SDWebImageCompat
: 宏定义和C语言的一些工具方法。 -
SDWebImageOperation
:定义通用的Operation协议,主要就是一个方法,cancel。从而在cancel的时候,可以面向协议编程。
下载
-
SDWebImageDownloader
:实际的下载功能和配置提供者,使用了单例的设计模式。 -
SDWebImageDownloaderOperation
:继承自NSOperation,是一个异步的NSOperation,封装了NSURLSession
进行实际的下载任务。
缓存处理
-
SDImageCache
:继承自NSCache
,实际处理内存cache和磁盘cache。 -
SDImageCacheConfig
:缓存处理的配置。 -
SDWebImageCoder
:定义了编码解码的协议,从而可以实现面向协议编程。
功能类
-
SDWebImageManager
:宏观的从整体上管理整个框架的类。 -
SDWebImagePrefetcher
:图片的预加载管理。
加载GIF动图
- FLAnimatedImage:处理加载GIF动图的逻辑。
图片的编码解码处理
- SDWebImageCodersManager:实际的编码解码功能处理,使用了单例模式。
Category
- 类别用来为UIView和UIImageView等”添加”属性来存储必要的信息,同时暴露出接口,进行实际的操作。
用类别来提供接口往往是最方便的,因为用户只需要import这个文件,就可以像使用原生SDK那样去开发,不需要修改原有的什么代码。
面向对象开发有一个原则是-单一功能原则,所以不管是在开发一个Lib或者开发App的时候,尽量保证各个模块之前功能单一,这样会降低耦合。
sd_setImageWithURL的加载逻辑
1.取消正在加载的图片
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
方法源代码如下,这里的key是FLAnimatedImageView。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
//使用NSMapTable存储当前的operation
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
//使用@synchronized保证线程安全,后面会讲到
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
//这里属于面向协议编程,不关心具体的类,只关心遵守某个协议
[operation cancel];
}
@synchronized (self) {
//删除对应的key
[operationDictionary removeObjectForKey:key];
}
}
}
}
我们看一下SDOperationsDictionary
的数据结构
- (SDOperationsDictionary *)sd_operationDictionary {
@synchronized(self) {
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
SDOperationsDictionary
通过runtime
关联对象机制来为UIView添加的属性。它的数据结构是NSMapTable
。后面会讲到。
2. 如果有PlaceHolder,设置placeHolder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
dispatch_main_async_safe
是一个宏定义,会判断是否是并行队列,不是的话异步切换到主队列执行。
#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {\
block();\
} else {\
dispatch_async(queue, block);\
}
#endif
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif
3. 根据SDImageCache来查缓存,看看是否有图片
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {//异步返回查询的结果}
queryCacheOperationForKey
返回一个NSOperation,之所以这样,是因为从磁盘或者内存查询的过程是异步的,后面可能需要cancel,所以这样做。
我们再看看queryCacheOperationForKey这个方法是怎么实现的?
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// First check the in-memory cache...
//先从检查内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
//如果不需要检查磁盘直接返回
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//创建一个NSOperation,因为从磁盘查询的过程是异步的,后面可能需要cancel
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
//从磁盘中查询
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
diskImage = [self diskImageForKey:key data:diskData options:options];
if (doneBlock) {
//回归到主线程行,进行doneBlock操作
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {//切换到ioQueue,进行异步磁盘查询操作
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
这里使用到了@autoreleasepool
,后面讲解。
4. 创建下载任务
//downloadToken用于取消下载的操作
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
//下载完成
if (downloadedImage && finished) {
//是否需要序列化成data
if (self.cacheSerializer) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
//保存至内存或磁盘
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
});
} else {
//如果不需要保存序列化,直接保存至内存或磁盘
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//回归到主线程行,进行completedBlock操作
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
if (finished) {//完成之后要记得移除这个operation
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}
safelyRemoveOperationFromRunning:
为了保证多线程安全,移除的时候加上锁,这个锁是通过信号量实现的。这个方法的源代码如下:
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
if (!operation) {
return;
}
LOCK(self.runningOperationsLock);
[self.runningOperations removeObject:operation];
UNLOCK(self.runningOperationsLock);
}
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
self.runningOperations
的数据结构是NSMutableSet
。里面都是SDWebImageCombinedOperation对象
@property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;
接下来,我们来看看实际的下载operation是什么样子的
也就是这个方法:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 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 nil;
}
LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
if (!operation || operation.isFinished) {
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
这个方法之所以返回SDWebImageDownloadToken
,应该主要是为了返回后面取消下载操作用的。
URLOperations
的数据结构是一个NSMutableDictionary,key是图片url,value是一个operation
4.1由于有各种各样的block回调,例如下载进度的回调,完成的回调,所以需要一个数据结构来存储这些回调
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
其中,用来存储回调的数据结构是一个NSMutableDictionary,其中key是图片的url,value是回调的数组
举个例子,存储后应该是这样的,
@{
@"http://imageurl":[
@{
@"progress":progressBlock1,
@"completed":completedBlock1,
},
@{
@"progress":progressBlock2,
@"completed":completedBlock2,
},
],
//其他
}
如何做到url防护的
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果不是有效的url就直接返回
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
failedURLs是个NSMutableSet
类型的,里面存放着请求失败的url。所以每次在请求之前先去failedURLs检查是否包含这个url
4.3如何保证同一个url不被下载两次:
在创建操作之前,先去URLOperations,如果取不到或者已经完成,再去创建。因为同一个url对应的operation就只有一个
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
// 去URLOperations去取operation,如果取不到或者已经完成才去创建operation
if (!operation || operation.isFinished) {
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
//完成之后移除operation
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
[self.downloadQueue addOperation:operation];
}
这样的话可以保证一个URL在多次下载的时候,只进行多次回调,而不会进行多次网络请求
4.4 对于同一个url,在第一次调用sd_setImage的时候进行,创建网络请求SDWebImageDownloaderOperation
。
[[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
在看看Progress回调:
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];
//渐进式下载
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// Get the image data
__block NSData *imageData = [self.imageData copy];
// Get the total bytes downloaded
const NSInteger totalSize = imageData.length;
// Get the finish status
BOOL finished = (totalSize >= self.expectedSize);
if (!self.progressiveCoder) {
// 创建渐进解压实例
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
self.progressiveCoder = [[[coder class] alloc] init];
break;
}
}
}
//在coderQueue队列解压图片
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
//异步切换到主线程上进行回调
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
}
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
completion回调:
@synchronized(self) {
self.dataTask = nil;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
//发送停止下载的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
if (!error) {
//发送停止下载完成的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
}
});
}
//保证可以取到下载完成的block
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
//用__block来修饰imageData,保证在能在block中修改这个变量
__block NSData *imageData = [self.imageData copy];
if (imageData) {
// 在coderQueue队列解压图片
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
//图片解码
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
BOOL shouldDecode = YES;
// 不强制解压GIF和webp
if (image.images) {
shouldDecode = NO;
}
//解压图片
if (shouldDecode) {
if (self.shouldDecompressImages) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
}
}
4.5 下载图片完成后,根据需要图片解码和处理图片格式,回调给Imageview
//图片解码
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
BOOL shouldDecode = YES;
// 不强制解压GIF和webp
if (image.images) {
shouldDecode = NO;
}
if (shouldDecode) {
if (self.shouldDecompressImages) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
//解压图片
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
更新
基于最新的版本:
当下载完成后,会将图片存入内存:
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
再看一下这个方法callStoreCacheProcessForOperation:
// Store cache process
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// the target image store cache type
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}
这里注意一下缓存策略,默认是内存和磁盘都存的
总结下整个调用过程
- 取消上一次调用
- 设置placeHolder
- 保存此次operation
- cache查询是否已经下载过了,先检查内存,后检查磁盘
- 利用NSURLSession来下载图片,根据需要解码,回调给imageview,存储到缓存
线程管理
整个SDWebImage一共有四个队列
- Main queue,主队列,在这个队列上进行UIKit对象的更新,发送notification
- ioQueue,用在图片的磁盘操作
- downloadQueue(NSOperationQueue),用来全局的管理下载的任务
- coderQueue专门复杂解压图片的队列。
注意:barrierQueue已经被废掉了,统一使用信号量来确保线程的安全。
图片解码
传统的UIImage进行解码都是在主线程上进行的,比如
UIImage * image = [UIImage imageNamed:@"123.jpg"]
self.imageView.image = image;
在这个时候,图片其实并没有解码。而是,当图片实际需要显示到屏幕上的时候,CPU才会进行解码,绘制成纹理什么的,交给GPU渲染。这其实是很占用主线程CPU时间的,而众所周知,主线程的时间真的很宝贵
现在,我们看看SDWebImage是如何在后台进行解码的 :
在coderQueue进行异步解压图片,解压成功后切换到主线程上回调给调用方。
注意解码操作是在一个单独的队列coderQueue
里面处理的
//在coderQueue队列解压图片
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
//异步切换到主线程上进行回调
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
incrementallyDecodedImageWithData
方法:
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
if (!_imageSource) {
_imageSource = CGImageSourceCreateIncremental(NULL);
}
UIImage *image;
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
if (_width + _height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = 1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
_orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
}
}
if (_width + _height > 0) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (partialImageRef) {
image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
CGImageRelease(partialImageRef);
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
}
}
if (finished) {
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
return image;
}
解压图片
//解压图片
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
//是否有alpha通道
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
//解压图片
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
缓存处理(SDImageCache)
缓存处理 包含两块:
- 内存缓存(SDMemoryCache):
- 磁盘缓存
内存缓存集成自NSCache。添加了在收到内存警告通知UIApplicationDidReceiveMemoryWarningNotification的时候自动removeAllObjects。
再看看磁盘缓存是如何做的?
磁盘缓存是基于文件系统NSFileManager对象的,也就是说图片是以普通文件的方式存储到沙盒里的。
下面看一下这几个问题:
1.磁盘缓存的默认路径是啥:
/Library/Caches/default/com.hackemist.SDWebImageCache.default/
2.SDWebImage 缓存图片的名称如何 避免重名?
缓存图片的名称是对key做了一次md5加密处理
- (nullable NSString *)cachedFileNameForKey:(nullable 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);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
return filename;
}
3.SDWebImage Disk默认缓存时长? Disk清理操作时间点? Disk清理原则?
默认缓存时长一周:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
在App关闭的时候或者app退到后台的时候(后台清理):
//在App关闭的时候清除过期图片
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
//在App进入后台的时候,后台处理过期图片
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
Disk清理原则:
1.获取文件的modify时间,然后比较下过期时间,如果过期了就删除。
2.当磁盘缓存超过阈值后,根据最后访问的时间排序,删除最老的访问图片。
SDWebImage 如何 区分图片格式?
将数据data转为十六进制数据,取第一个字节数据进行判断。
//根据data获取图片格式:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
//将数据data转为十六进制数据,取第一个字节数据进行判断。
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
}
return SDImageFormatUndefined;
}
SDWebImageDownloader的最大并发数和超时时长
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
/**
* The timeout value (in seconds) for the download operation. Default: 15.0.
*/
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
最大并发下载量6个。下载超时时长15s。
NSMapTable
NSMapTable类似于NSDictionary,但是NSDictionary只提供了key->value的映射。NSMapTable还提供了对象->对象的映射。
NSDictionary的局限性:
NSDictionary 中存储的 object 位置是由 key 来索引的。由于对象存储在特定位置,NSDictionary 中要求 key 的值不能改变(否则 object 的位置会错误)。为了保证这一点,NSDictionary 会始终复制 key 到自己私有空间。但这也有一个限制:你只能使用 OC 对象作为 NSDictionary 的 key,并且必须支持 NSCopying 协议。这意味着,NSDictionary 中真的只适合将值类型的对象作为 key(如简短字符串和数字)。并不适合自己的模型类来做对象到对象的映射。
NSMapTable对象到对象的映射:
比如一个 NSMapTable 的构造如下:
NSMapTable *keyToObjectMapping =
[NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn
valueOptions:NSMapTableStrongMemory];
这将会和 NSMutableDictionary 用起来一样一样的,复制 key,并对它的 object 引用计数 +1。
NSPointerFunctionsOptions
:
NSMapTableCopyIn
NSMapTableStrongMemory
NSPointerFunctionsWeakMemory
可以通过设置NSPointerFunctionsOptions来指定的对象的内存管理方式。
我们看看SDWebImage怎么使用的NSMapTable
:
// Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
// This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
// At this case, we can sync weak cache back and do not need to load from disk cache
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
key的内存管理方式是NSPointerFunctionsStrongMemory,当一个对象添加到NSMapTable中后,key的引用技术+1。
value内存管理方式是NSPointerFunctionsWeakMemory,当一个对象添加到NSMapTable中后,key的引用技术不会+1。
这样使用的意义在哪呢:
1.遵循NSCache不复制key的文档。
2.当收到内存警告,缓存被清理的时候,可以保存image实例。这个时候我们可以同步若缓存表,不需要从磁盘加载。
@autoreleasepool
@autoreleasepool {
//从磁盘中查询
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
diskImage = [self diskImageForKey:key data:diskData options:options];
if (doneBlock) {
//回归到主线程行,进行doneBlock操作
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
如果不使用autoreleasepool,已经创建的临时变量就无法释放,要等到下次runloop结束时,才会清空系统的自动释放池中的临时变量,但是这个时间是不确定的,这就会导致内存爆发式的增长。如果autoreleasepool,等待autoreleasepool结束时,里面的临时变量就会释放。因为autoreleasepool的作用就是加速局部变量的释放。
具体可以看一下我这篇文章autoreleasepool
读取Memory和Disk的时候如何保证线程安全
Memory是通过信号量。
LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
UNLOCK(self.weakCacheLock);
Disk操作是在一个单独的IO 队列去处理的。
存图片至磁盘
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
dispatch_sync(self.ioQueue, ^{
[self _storeImageDataToDisk:imageData forKey:key];
});
}
取图片:
dispatch_sync(self.ioQueue, ^{
imageData = [self diskImageDataBySearchingAllPathsForKey:key];
});
dispatch_semaphore_t信号量
通过宏定义使用信号量(创建,提高,降低)
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
使用
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
更多信号量可以看这篇文章
FOUNDATION_EXPORT
.h文件
中声明
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;
.m文件
中是这样实现的
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
如以上代码所示,FOUNDATION_EXPORT
是用来定义常量的,众所周知,#define
也可以定义常量。
他们的主要区别在哪呢?
- 使用
FOUNDATION_EXPORT
定义常量在检测字符串的值是否相等的时候效率更快,可以使用可以直接使用(myString == SDWebImageDownloadStartNotification)来比较。而define定义的常量如果要比较的话,就得使用[myString isEqualToString:SDWebImageDownloadStartNotification],效率更低一点,因为前者是比较指针地址,后者是比较每一个字符。 -
FOUNDATION_EXPORT
是可以兼容c++编程的。 - 过多的使用宏定义会产生过多的二进制文件。
@synchronized
1.为啥要引入@synchronized
Objective-C支持程序中的多线程。这就意味着两个线程有可能同时修改同一个对象,这将在程序中导致严重的问题。为了避免这种多个线程同时执行同一段代码的情况,Objective-C提供了@synchronized()指令。
2.参数
指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不同的关键代码段,我们应该分别使用不同的信号量。只有在应用程序编程执行多线程之前就创建好所有需要的互斥信号量对象来避免线程间的竞争才是最安全的。
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
- @synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
- @synchronized 主要用于多线程的程序,这个指令可以将{ } 内的代码限制在一个线程执行,如果某个线程没有执行完,其他的线程如果需要执行就得等着。
@synthesize:
ios6之后 LLVM 编译器会新增加一个技术,叫自动合成技术
,会给每个属性添加@synthesize,即
@synthesize propertyName = _propertyName;
也就是说会自动生成一个带下划线的实例变量,同时为属性生成getter
和setter
方法。当然这些都是默认实现的。
如果我们不想使用编译器为我们生成的实例变量,我们就可以在代码中显示的起一个别名:
@synthesize propertyName = _anotherPropertyName;
如果我们想要阻止编译器自动合成,可以使用@dynamic
,使用场景就是你想自己实现getter
和setter
方法。
5.#pragma clang diagnostic push与#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
表示在这个区间里忽略一些特定的clang的编译警告,因为SDWebImage作为一个库被其他项目引用,所以不能全局忽略clang的一些警告,只能在有需要的时候局部这样做。
NS_ENUM && NS_OPTIONS
NS_ENUM
多用于一般枚举,NS_OPTIONS
多用于同一个枚举变量可以同时赋值多个枚举成员的情况。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0,
SDWebImageLowPriority = 1 << 1,
SDWebImageCacheMemoryOnly = 1 << 2,
SDWebImageProgressiveDownload = 1 << 3,
}
这里的NS_OPTIONS是用位运算的方式定义的。
//用“或”运算同时赋值多个选项
SDWebImageOptions option = SDWebImageRetryFailed | SDWebImageLowPriority | SDWebImageCacheMemoryOnly | SDWebImageProgressiveDownload;
//用“与”运算取出对应位
if (option & SDWebImageRetryFailed) {
NSLog(@"SDWebImageRetryFailed");
}
if (option & SDWebImageLowPriority) {
NSLog(@"SDWebImageLowPriority");
}
if (option & SDWebImageCacheMemoryOnly) {
NSLog(@"SDWebImageCacheMemoryOnly");
}
if (option & SDWebImageProgressiveDownload) {
NSLog(@"SDWebImageProgressiveDownload");
}
这样,用位运算,就可以同时支持多个值。
相关面试题
假如一个界面里面有很多图片,如何优先下载最大的图片。
方法1:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
可以将options设置成SDWebImageDownloaderHighPriority
。就是说将这个操作设成高优先级的。
方法2:给大的图片做标记,让它先sd_setImage
网友评论