1.综述
类目 | 选项 |
---|---|
时间 | 耗时6天 |
版本 | v3.7.0 |
SDWebImage是一个处理服务端加载图片的库,主要解决了以下几个问题:
- 图片从网路加载,预加载、同时下载多张、失败重试等
- 图片缓存,支持内存和磁盘
- 图片显示优化,解压缩、渐进显示、gif处理等
- 提供常见UI控件的扩展,方便使用
大致分为4个大模块,各模块的类名如下
模块 | 类名 |
---|---|
下载 | SDWeImageManager、SDWebImageDownloader、SDWebImageDownloaderOperation、SDWebImagePrefetcher |
缓存 | SDImageCache |
图片处理 | SDWebImageCompat、SDWebImageDecoder |
控件扩展 | UIButton(webCache)等 |
核心类及功能如下
类名 | 功能 |
---|---|
SDWeImageManager | 头文件有注释,It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). ,绑定downloader和imageCache |
SDWebImageDownloader | 异步下载器,维护下载配置 |
SDWebImageDownloaderOperation | 默认operation,供内部operationQueue使用 |
SDWebImagePrefetcher | 预加载urls到cache中,低优先级下载 |
SDImageCache | 支持缓存图片到内存和磁盘,异步缓存到磁盘,iCloud备份配置,清理内存和磁盘 |
SDWebImageCompat | 处理图片scale,根据url后的@2x和@3x字符串处理 |
SDWebImageDecoder | 生成解压缩图片,可以提升imageView图片渲染速度 |
带着问题阅读可能会更好。
2.提出问题:
- 下载部分:
- 1.1 如何从网络下载图片,超时、失败重试如何处理?如何取消?
- 1.2 支持同时下载几张?默认6张
- 1.3 同一个url同时下载如何处理?
- 1.4 同时有多张下载完成,如何处理回调?
- 缓存处理
- 2.1 缓存到内存、磁盘怎么实现?操作是异步的吗?两种缓存是并发的吗?
- 2.2 缓存大小如何设置?如何清理?
- 2.3 图片的格式怎么保证,gif/png/jpeg等
- 图片处理
- 3.1 为何要解压缩?如何解压缩?可以异步吗?
- 3.2 为何要处理scale?怎么处理?可以异步吗?
- 3.3 gif图如何处理?webp呢?
- 控件扩展
- 4.1 UIButton、UIImageView的扩展都有哪些方法?
- 4.2 NSData+ImageContentType是如何获取图片类型的?
- 4.3 UIView (WebCacheOperation)干啥的?
- 其他问题
- 5.1 SDWebImageOperation如何使用自己的?
- 5.2 还有哪些好的设计?思路?
3.解决问题
- 下载部分:
1.1 如何从网络下载图片,超时、失败重试如何处理?
- 使用NSOperation+NSUrlConnection处理网络请求,在connection:didReceiveData:方法中,拼接data;在connectionDidFinishLoading方法中将data转为UIImage;具体操作,在图片处理中统一说明
- 超时利用NSMutableURLRequest设置,默认超时时间15秒
- 失败重试,当配置SDWebImageOptions参数为SDWebImageRetryFailed时支持,SDWebImageManager中维护一个failedURLs属性,类型为NSMutableSet,存储下已经下载失败的url,若再次下载该url,同时配置了该参数,则 继续下载,否则不再下载。【此处失败重试,并没有在失败后再重复请求,只是一个能否下次下载的标记,相当于一个url黑名单】
- 取消下载,在自定义operation的cancel方法中,同时调用NSUrlConnection对象的cancel方法即可。实现为利用SDWebImageManager downloadImageWithURL:方法返回的id
对象,该对象实现了cancel协议,调用其cancel方法即可【此处为何要用协议这种设计,而不是直接给方法?已经搞清楚了,因为downloaderOperation是可以自定义的,自定义类要自己实现cancel方法,所以采用协议形式设计】
补充:
网络下载核心逻辑,文字描述不够清晰,上代码
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// 1.生成SDWebImageCombinedOperation对象,弱引用避免block中引用循环
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 2.全局字典保存正在运行的operation
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 3.用url构建缓存标记key
NSString *key = [self cacheKeyForURL:url];
// 4.由imageCache queryDiskCacheForKey 方法生成NSOperation对象,保存在combinedOperation中,此处相当于包装了一层
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// 5.如果缓存不存在,先由代理判断是否要下载url(此处是多虑了吧)
if (image && options & SDWebImageRefreshCached) {
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
dispatch_main_sync_safe(^{
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES, url);
});
}
// 6.配置downloaderOptions,将SDWebImageOptions配置传给SDWebImageDownloaderOptions
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
// ...
// 7.使用上方配置参数,调用imageDownloader downloadImageWithURL方法,生成subOperation对象
id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 8.处理下载结果
// 8.1 操作取消,不操作任何事情
if (!strongOperation || strongOperation.isCancelled) {
}
// 8.2 下载失败,将url加入到failedUrls集合中
else if (error) {
}
// 8.3 下载成功,缓存到内存和磁盘(可选)中,并在主线程回调
else {
// 8.3.1 判断是否缓存到磁盘
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// 8.3.2 image如果是NSUrlCache中图片,并且下载的图片为空,不回调
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
// 8.3.3 如果是动图,有delegate imageManager:transformDownloadedImage:withURL:方法判断是否要转换成指定的image,在子线程中处理,处理完缓存你transformImage,并回调
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
// 8.3.4 正常图片,缓存完回调
else {
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
// 9. 更新runningOperations值
if (finished) {
@synchronized (self.runningOperations) {
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
}
}];
}
1.2 支持同时下载几张?
默认6张,利用NSOperationQueue的maxConcurrentOperationCount属性实现
1.3 同一个url多次下载如何处理?
同一个url同时只会下载一次,主要是保证回调问题
- 在创建任务时将每个url的回调都被保存起来
// 全局的URLCallbacks字典,每个元素保存url的callback数组
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
//存放新任务的回调
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
//更新全局的URLCallbacks
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
- 任务结束时将回调有序执行,此处巧妙结合dispatch_barrier_sync实现(创建任务时也利用了此函数,主要作用是保证提交到全局队列中的同步和异步任务(主要是上一次的创建任务、进度回调、结束回调)都执行完,再创建新的任务,保证创建的任务有序)此处的代码段如下:
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//获取callback,此处使用sync提交同步任务到barrierQueue中,可以保证获取callbacksForURL肯定是最新的,如果这会儿self.URLCallbacks有更新,即addProgressCallback函数中,dispatch_barrier_sync会保证它前边的任务执行完再更新self.URLCallbacks,简单的说读和写都是同步任务,肯定就有顺序;此处用dispatch_barrier_sync应该也可以,只是sync就够用了
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
//回调在主线程
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//此处使用栅栏函数,保证移除操作有序
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
// 该url的所有回调都会执行,此处回调执行在当前线程
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
1.4 同时有多张下载完成,如何处理回调?
此处不是问题,各处理各的回调即可;每个url都有自己的operation,所以不会干扰,之前多虑了
1.5 SDWebImageDownloaderOperation解析
- 上边所述都是对于任务创建及回调等的处理,真正的网络请求是在该类实现的,主要使用NSUrlConnection结合runloop实现,核心逻辑在NSUrlConnection的几个回调方法中
1> 由于此处NSURLConnection对象是在子线程中创建,所以其回调也是在子线程中返回,需要手动处理子线程的runloop,这里使用了CoreFoundation框架中的几个方法,非常正确。
2> 同时处理了应用进入后台情况,取消当前的connection任务
代码块如下:
- (void)start {
@synchronized (self) {
// 1.判断是否是取消状态
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
// 2.处理后台过期事件
#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
// 3.创建网络连接
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
// 保留当前的线程,供cancel方法使用,主要是为了取消当前的runloop
self.thread = [NSThread currentThread];
}
// 4.启动网络连接
[self.connection start];
if (self.connection) {
// 5.回调,并发通知
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
// 6.启动runloop,开始接收delegate
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();
}
//...
}
- 该类主要实现了自定义NSOperation必须得几个方法:
start、cancel以及executing、finished的setter自定义
2.缓存处理
2.1 缓存到内存、磁盘怎么实现?操作是异步的吗?两种缓存是并发的吗?
- 利用NSCache缓存到内存,需要在收到内存警告时,清空内存中的缓存;
- 磁盘缓存则是保存data到文件中,
1>.默认存放在 cache/com.hackemist.SDWebImageCache.default目录下
2>.文件名取url得md5值,
3>.SDWebImage并没有使用defaultFileManager,而是new了一个,此处考虑很周到 - 都是异步操作,使用gcd的gloableQueue处理任务,下载完成时开始缓存,当option & SDWebImageCacheMemoryOnly为真时,则不缓存到磁盘
2.2 缓存大小如何设置?如何清理?
- 内存中缓存的大小利用NSCache的totalCostLimit属性设置,系统会在内存超出最大值时,自动清理最早缓存的对象
- 磁盘中缓存无默认最大值,有最长生存周期默认为7天(可重新设置),磁盘中清理逻辑为:每次当应用进入后台或者应用即将被杀死时,开始在后台检查文件的生命时长,如果文件已经过期,则从删除之
#if TARGET_OS_IOS
// 内存警告时清内存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
// 应用即将挂起,清理过期文件
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
// 应用进入后台,在后台清理过期文件,加了后台过期的处理,跟cleanDisk方法的逻辑基本一致
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
2.3 图片的格式怎么保证,gif/png/jpeg等
if (image && (recalculate || !data)) {
//如果有alpha,则视其为png图
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 如果还有原始数据,直接用原始数据头部8位来检查,这个最准
// png的固定头部为0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 转pngdata
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
//转jpegdata,最低压缩比
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
- 图片处理
- 3.1 为何要解压缩?如何解压缩?可以异步吗?
1>. 无论是png、jpeg等图片都是压缩图片,无法直接显示,需要先转化为bitmap,才能展示在imageView的layer上;
2>. 利用CGContext提供的方法,将UIImage利用context重新绘制一遍,生成一个新的image,该版本SDWebImage不支持含有alpha通道图片解压缩
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
return nil;
}
//此处加入autoreleasepool可能并非必须,因为在方法return之后,这堆临时对象就立马释放了,当然外部如果有循环调用的话,还是加上比较好
@autoreleasepool{
// do not decode animated images
if (image.images != nil) {
return image;
}
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// 如果图片有alpha通道,直接return原始图片,不解压缩
if (anyAlpha) {
return image;
}
// 处理颜色空间
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
// 未知空间,使用RGB颜色空间
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
}
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
NSUInteger bytesPerPixel = 4;
// 据说此处用0,还可以支持行对齐,此处设置并不是最优
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitsPerComponent = 8;
//创建bitmapContext,参数有:宽、高、每组的位数(就是每个像素8位)、每行的字节数、颜色空间和字节顺序[SDWebImage的设置也不是最优解,应该使用kCGBitmapByteOrder32Host|kCGImageAlphaNoneSkipLast(无alpha通道时),kCGBitmapByteOrder32Host|kCGImageAlphaPremultipliedFirst(有alpha通道时)]
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
bitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
//在指定区域绘制图片
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
// 创建新的image
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
// 释放CoreFoundation对象
if (unsupportedColorSpace) {
CGColorSpaceRelease(colorspaceRef);
}
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
- 3.2 为何要处理scale?怎么处理?可以异步吗?
iPhone是视网膜屏幕,若屏幕scale为2,一个点填充4个像素,scale为3,一个点填充9个像素,如果图片的scale为1,填充到屏幕scale为2的机器上,则因像素不够,会显示模糊,所以要给当前机型合适scale的图片,这也是为啥我们的素材要准备为@2x、@3x这两种(6和6plus使用)
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
if (!image) {
return nil;
}
// gif图,每个处理一遍,用了递归
if ([image.images count] > 0) {
NSMutableArray *scaledImages = [NSMutableArray array];
for (UIImage *tempImage in image.images) {
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
}
else {
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
CGFloat scale = 1;
if (key.length >= 8) {
//根据图片名称是否包含@2x、@3x字符串来处理,图片的命名一定要规范
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
}
- 3.3 gif图如何处理?webp呢?
能够生成正确的anmatedImage就可以,在UIImage (GIF)扩展中
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
// 确定data中有几帧图片
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
// 单图片
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
}
else {
// 多图片
NSMutableArray *images = [NSMutableArray array];
NSTimeInterval duration = 0.0f;
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!image) {
continue;
}
//计算每帧图片的时长
duration += [self sd_frameDurationAtIndex:i source:source];
// 生成图片数组,注意scale
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
CGImageRelease(image);
}
if (!duration) {
duration = (1.0f / 10.0f) * count;
}
// 用图片数组和时长,创建anmatedImage
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}
CFRelease(source);
return animatedImage;
}
- 控件扩展
4.1 UIButton、UIImageView的扩展都有哪些方法?
有两大类
1>. 根据状态设置中心图、背景图url,可选取placeHolder、competitionHandler参数等
2>.取消之前根据状态设置的url4.2 NSData+ImageContentType是如何获取图片类型的?
根据data的前两位数值来确定类型
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return @"image/jpeg";
case 0x89:
return @"image/png";
case 0x47:
return @"image/gif";
case 0x49:
case 0x4D:
return @"image/tiff";
case 0x52:
// R as RIFF for WEBP
if ([data length] < 12) {
return nil;
}
// 此处做特殊处理
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"image/webp";
}
return nil;
}
return nil;
}
- 4.3 UIView (WebCacheOperation)干啥的?
持有一个字典来存储,当前对象所有的operation,当取消其下载时用得着
- (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]]) {
//此处id为SDWebImageDownloaderOperation对象,它实现了SDWebImageOperation协议,为何要这么设计?
for (id operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
- 其他问题
- 5.1 SDWebImageOperation如何使用自己的?
利用SDWebImageDownloader setOperationClass:方法设置 - 5.2 还有哪些好的设计?思路?
1>.在子线程中获取磁盘缓存大小
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}
2>. 子类化NSCache,内存警告时自动释放内存
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
5.3 图片渐进显示处理,不停得根据当前拿到的data创建一个新的UIImage
if (width + height > 0 && totalSize < self.expectedSize) {
// 从指定位置创建imageRef
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 处理失真图片?此处不知是为何
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
// 用新创建的imageRef,做scale、decode处理,生成正确的UIImage对象
if (partialImageRef) {
//初始UIImage对象
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
//scale处理
UIImage *scaledImage = [self scaledImageForKey:key image:image];
//decode处理
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
//主线程回调
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}