1.导入 1 2 3 4 5 platform :ios, '10.0' target ‘ASDF’ do pod 'SDWebImage' , '~> 3.7.3' end
2.实现 以常用的UIImageView
设置图片为例:
1 2 3 4 5 - (void) sd_setImageWithURL:(NSURL *) url placeholderImage:(UIImage *) placeholder options:(SDWebImageOptions) options progress:(SDWebImageDownloaderProgressBlock) progressBlock completed:(SDWebImageCompletionBlock) completedBlock;
参数
url为图片地址。
placeholder为占位图。
progressBlock为进度。
completedBlock下载完成或有缓存时的回调。
options一般使用 SDWebImageRetryFailed | SDWebImageLowPriority,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { SDWebImageRetryFailed = 1 << 0 , // 下载失败了会再次尝试下载(默认情况下失败的图片链接不会再重新下载) WebImageLowPriority = 1 << 1 , // UIScrollView等正在滚动时延迟下载图片(默认情况下不延迟,立刻开始) SDWebImageCacheMemoryOnly = 1 << 2 ,// 只缓存到内存中 SDWebImageProgressiveDownload = 1 << 3 ,// 图片会边下边显示 SDWebImageRefreshCached = 1 << 4 , // 强制重新请求图片并刷新缓存,将硬盘缓存交给系统自带的NSURLCache去处理 SDWebImageContinueInBackground = 1 << 5 ,// 后台下载 SDWebImageHandleCookies = 1 << 6 , // 处理cookie,请求的request.HTTPShouldHandleCookies 会被置为 YES; SDWebImageAllowInvalidSSLCertificates = 1 << 7 ,// 允许不受信任的SSL证书 SDWebImageHighPriority = 1 << 8 , // 优先下载,下载任务的优先级为高 SDWebImageDelayPlaceholder = 1 << 9 ,// 延迟占位符 SDWebImageTransformAnimatedImage = 1 << 10 ,// 改变动画形象 };
1.+WebCache分类 作用:对外暴露设置图片的接口。
在新的图片加载前,取消本视图之前正在进行的下载任务;
交由 SDWebImageManager 接着处理图片下载逻辑;
保存当前下载任务;
图片下载成功或失败,回到主线程回调数据并给视图设置图片;
源码摘要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - (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 (url) { __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) { }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad" ]; } }
实现细节:
图片的下载任务是由 SDWebImageManager 执行的,它的实现细节后面会讲,这里只需要知道下载的操作经过两次封装,最终被封装到 SDWebImageCombinedOperation 对象中:
1 2 3 4 5 6 7 8 9 10 11 @protocol SDWebImageOperation <NSObject> - (void)cancel; @end @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock; @property (strong, nonatomic) NSOperation *cacheOperation; @end
调用SDWebImageManager
执行下载时,会返回一个SDWebImageCombinedOperation
任务对象:
1 2 3 4 5 6 7 8 9 10 // SDWebImageManager.m - (id <SDWebImageOperation>) downloadImageWithURL:(NSURL *) url options:(SDWebImageOptions) options progress:(SDWebImageDownloaderProgressBlock) progressBlock completed:(SDWebImageCompletionWithFinishedBlock) completedBlock{ __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 略... return operation; }
上面返回的 SDWebImageCombinedOperation 任务对象会被保存到当前视图的对象中:
1 2 // UIImageView+WebCache.m [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad" ];
保存接口的具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (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; } - (void )sd_setImageLoadOperation:(id )operation forKey:(NSString *)key { [self sd_cancelImageLoadOperationWithKey:key]; NSMutableDictionary *operationDictionary = [self operationDictionary]; [operationDictionary setObject:operation forKey:key]; }
使用对象关联技术将下载任务保存到当前分类中的operationDictionary
字典属性中。
场景:cell 被重用时上一张图片尚未加载完,需要取消上一次的加载任务并加载新的图片。
当前视图中所有的下载任务都保存在operationDictionary
中,取消任务时要先从此字典中取出之前的任务,再执行取消方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 - (void )sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad" ]; } - (void )sd_cancelImageLoadOperationWithKey:(NSString *)key { 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]; } }
取消操作调用的是 SDWebImageCombinedOperation 对象 的 cancel 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @implementation SDWebImageCombinedOperation - (void )cancel { self .cancelled = YES ; if (self .cacheOperation) { [self .cacheOperation cancel]; self .cacheOperation = nil ; } if (self .cancelBlock) { self .cancelBlock(); _cancelBlock = nil ; } }
所以,最终取消的是真正的图片下载任务cacheOperation
对象,这是一个 NSOperation 实例。
2.Manager 作用:做下载前的各项检查,缓存下载完成后的图片并返回。
检测URL是否下载失败过,失败过的URL不再重复下载;
根据URL生成的key查询图片缓存,有缓存且不要求强制刷新缓存时使用缓存图片;
没有缓存时,交给 SDWebImageDownloader,封装请求并创建任务和队列,发起网络请求;
缓存下载完成后的图片,返回主线程;
源码摘要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; 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 = ...; completedBlock(nil , error, SDImageCacheTypeNone, YES , url); }); return operation; } @synchronized (self .runningOperations) { [self .runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; operation.cacheOperation = [self .imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType){ if ((!image || options & SDWebImageRefreshCached) && (![self .delegate respondsToSelector: @selector (imageManager:shouldDownloadImageForURL:)] || [self .delegate imageManager:self shouldDownloadImageForURL:url])) { if (image && options & SDWebImageRefreshCached) { dispatch_main_sync_safe(^{ completedBlock(image, nil , cacheType, YES , url); }); } id <SDWebImageOperation> subOperation = [self .imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { /.../ if (downloadedImage && finished) { [self .imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(downloadedImage, nil , SDImageCacheTypeNone, finished, url); } }); } } } else if (image) { dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(image, nil , cacheType, YES , url); } }); } }]; return operation; }
3.Downloader 作用:配置Request,设置下载任务优先级与依赖关系。
设置 URLRequest;
创建 SDWebImageDownloaderOperation 任务,加入到并发数=6的 Queue 中,开始下载;
根据配置信息设置下载任务Operation的优先级和依赖关系;
下载成功,调用 CompletedBlock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 - (instancetype )initWithSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration { if ((self = [super init])) { _operationClass = [SDWebImageDownloaderOperation class ]; _shouldDecompressImages = YES ; _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; _downloadQueue = [NSOperationQueue new]; _downloadQueue.maxConcurrentOperationCount = 6 ; _downloadQueue.name = @"com.hackemist.SDWebImageDownloader" ; _HTTPHeaders = headerDictionary; _operationsLock = dispatch_semaphore_create(1 ); _headersLock = dispatch_semaphore_create(1 ); _downloadTimeout = 15.0 ; [self createNewSessionWithConfiguration:sessionConfiguration]; } return self ; } - (void )createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration { [self cancelAllDownloads]; if (self .session) { [self .session invalidateAndCancel]; } self .session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil ]; } - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy :NSURLRequestReloadIgnoringLocalCacheData ) timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES ; request.allHTTPHeaderFields = wself.HTTPHeaders; operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger 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]; } }); for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } cancelled:^{...}]; operation.shouldDecompressImages = wself.shouldDecompressImages; if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh ; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow ; } [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; }
4.Operation 作用:自定义SDWebImageDownloaderOperation,执行下载任务并处理图片scale与解码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 - (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 ) { 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: didReceiveData:
代理接收数据并保存到 NSMutableData 对象中。如果设置了边下载边显示,则这里会渐进式地绘制、缩放图片,默认还会对图片进行解码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 - (void )connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self .imageData appendData:data]; ...CGImageSourceRef imageSource = CGImageSourceCreateWithData ((__bridge CFDataRef )self .imageData, NULL ); ...CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex (imageSource, 0 , NULL ); ...UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self .request.URL];UIImage *scaledImage = [self scaledImageForKey:key image:image];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 ); } }); }
下载完成后在-connectionDidFinishLoading
代理中完成图片的缩放、解码并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 - (void )connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self .completedBlock; @synchronized (self ) { CFRunLoopStop (CFRunLoopGetCurrent ()); ... }); } ...UIImage *image = [UIImage sd_imageWithData:self .imageData];NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self .request.URL]; image = [self scaledImageForKey:key image:image];if (!image.images) { if (self .shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } }if (CGSizeEqualToSize (image.size, CGSizeZero )) { completionBlock(nil , nil , [NSError ...], YES ); }else { completionBlock(image, self .imageData, nil , YES ); } ...self .completionBlock = nil ; [self done]; }
5.图片scale 上面两个下载的回调中调用了一个共同的方法:
1 2 3 - (UIImage *)scaledImageForKey:(NSString *)key image :(UIImage *)image { return SDScaledImageForKey(key , image ); }
作用 :根据图片URL字符串推断图片的scale,按此scale绘制一张新图。
为啥要做这个操作呢?这里要提到三个关于图片尺寸信息的概念:
1、pixel dimensions
这是指图片的实际像素尺寸
,就是在文件夹中显示的真实图片尺寸。比如@1x图的尺寸是40*40,那么@2x的实际尺寸就是80*80,@3x的实际尺寸就是120*120。图片的清晰度只和图片本身的像素尺寸有关,实际尺寸越大则图片显示越清晰。
2、size
官方文档 中是这么描述的:
The logical dimensions of the image, measured in points.
This value reflects the logical size of the image and takes the image’s current orientation into account. Multiply the size values by the value in the scale property to get the pixel dimensions of the image.
size 是图片的逻辑尺寸
,实际尺寸
要在size对应字段的基础上再乘以scale:
1 2 实际宽度 = size.width * scale size.width = 实际宽度 / scale
根据上面的公式:
scale
pixel
size
@1x
40*40
40*40
@2x
80*80
40*40
@3x
120*120
40*40
如果设置了 imageview 的frame自适应其素材的大小,则提供三种倍数下对应pixel尺寸的图片,就能保证在不同分辨率的设备上 imageview.frame.size 的一致性。
3、scale
官方文档 中是这么描述的:
The scale factor of the image.
If you load an image from a file whose name includes the @2x modifier, the scale is set to 2.0. You can also specify an explicit scale factor when initializing an image from a Core Graphics image. All other images are assumed to have a scale factor of 1.0. If you multiply the logical size of the image (stored in the size property) by the value in this property, you get the dimensions of the image in pixels.
这是图片的缩放比例,是一个CGFloat数值,依图片本身的命名而定。如果图片名为 park@2x.png 则图片的scale就是2;如果图片名为 park@3x.png 则图片的scale就是3。
1 2 3 4 UIImage * p1 = [UIImage imageNamed:@"park @2x"]; UIImage * p2 = [UIImage imageNamed:@"park @3x"]; NSLog(@"+++ @2x.scale:%.0f, @3x.scale:%.0f",p1.scale,p2.scale); // 日志:+++@2x.scale:2, @3x.scale:3
除以@2x和@3x命名的图片外,其他所有图片的scale都是1.0。这意味着我们以 park.png 命名,或者从网络上下载的图片,它们的scale默认都是1.0。这也正是SD库中下载完图片或者从缓存中取出图片后,执行上面的方法重置图片scale属性的原因~
为了适配不同尺寸的设备,我们的素材需要提供三个版本,park.png,park@2x.png,part@3x.png。使用素材时:
1 UIImage *img = [UIImage imageNamed:@"park" ];
系统会自动根据当前屏幕的scale选择对应后缀的图片素材。在4/4s之后的手机上系统会自动加载以@2x为后缀的图片;在6p之后的plus版本手机上系统会自动加载以@3x为后缀的图片。
问题来了,从网络上下载的图片其scale默认是1.0,而其实际尺寸是确定的。根据公式:size
= 实际尺寸
/ scale
,同一张图,scale越小其size就越大。如果设置了imageview自适应素材的大小,则本该使用@2x或@3x素材的视图,其frame.size会比预想的要大,所以为了效果的准确性,还是需要正确设置图片的scale参数。
SD中重置图片scale的具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) { if (!image) { return nil ; } 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 = [UIScreen mainScreen].scale; if (key.length >= 8 ) { 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; } }
6.图片解码 默认情况下_shouldDecompressImages=YES
,SD会对下载的图片执行解码(动图除外)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 + (UIImage *)decodedImageWithImage:(UIImage *)image { @autoreleasepool { if (image.images) { return image; } CGImageRef imageRef = image.CGImage; CGImageAlphaInfo alpha = CGImageGetAlphaInfo (imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); if (anyAlpha) { return image; } size_t width = CGImageGetWidth (imageRef); size_t height = CGImageGetHeight (imageRef); CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel (CGImageGetColorSpace (imageRef)); CGColorSpaceRef colorspaceRef = CGImageGetColorSpace (imageRef); bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed); if (unsupportedColorSpace) colorspaceRef = CGColorSpaceCreateDeviceRGB (); CGContextRef context = CGBitmapContextCreate (NULL , width, height, CGImageGetBitsPerComponent (imageRef), 0 , colorspaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGContextDrawImage (context, CGRectMake (0 , 0 , width, height), imageRef); CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage (context); UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation]; if (unsupportedColorSpace) CGColorSpaceRelease (colorspaceRef); CGContextRelease (context); CGImageRelease (imageRefWithAlpha); return imageWithAlpha; } }
SD为啥要解码图片呢?
一般下载的图片或者我们手动拖进主 bundle 的图片都是 PNG 或者 JPG 格式的图片,它们都是经过编码压缩后的图片数据,并不是控件可以直接显示的位图。如果我们直接使用 “[UIImage imageNamed:]” 来加载图片,系统默认会在主线程立即进行图片的解码工作,这个过程就是把图片数据解码成可供控件直接显示的位图数据。由于这个解码操作比较耗时,并且默认是在主线程进行,所以当在主线程大量调用时就会产生卡顿。反过来,由于位图体积较大,在磁盘缓存中不会直接缓存位图数据,而是编码压缩过的PNG 或者 JPG 数据。
这里更准确的说,[UIImage imageNamed:] 加载图片时,不是立即解码的,而是在图片设置到UIImageView
或者CALayer.contents
中,并且 CALayer 被提交到GPU进行渲染前才会解码。另外,使用这种方式加载图片时,系统会自动缓存一份该图片,后面再使用此图时会直接从缓存中取,不再次解码。即使如此,使用这种方式加载图片时,最后的解码部分仍然时在主线程中进行的,所以当大量加载图片时,仍然有可能会出现卡顿现象。
SD 这里所做的正是对图片进行解码,而且是在异步线程里。SD缓存图片到内存时保存的是解码后的图片 ,缓存是通过 NSCache 实现的。这一步的目的是提高加载图片的速度和效率,需要注意的是,它在解码时会占用相当一部分内存,是典型的空间换时间。如果你遇到了内存问题,可以尝试禁止自动解码或者清空缓存:
7.图片缓存 写这篇文章时最新SD版本为3.7.3,缓存相关功能主要由 SDImageCache 类实现。这里的缓存包括了内存和磁盘两种方式。其中,内存缓存使用的是继承自 NSCache 的 AutoPurgeCache 类;磁盘缓存的操作被单独放在一个串行队列 ioQueue 中。这个类中主要的方法及实现如下:
1.查询缓存
从内存中检测;
从磁盘中检测(异步+串行队列);
磁盘中检测到缓存后,根据需要缓存到内存中;
返回结果;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { ... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil ; } NSOperation *operation = [NSOperation new]; dispatch_async (self .ioQueue, ^{ if (operation.isCancelled) { return ; } @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self .shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self .memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async (dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation; } - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key { return [self .memCache objectForKey:key]; } - (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 ; } }
2.保存图片 图片下载成功后,默认会解码并保存到内存中。
保存到磁盘时,会先检测图片的格式,文件名为URL对应的MD5值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 - (void )storeImage:(UIImage *)image recalculateFromImage:(BOOL )recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL )toDisk { ... if (self .shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self .memCache setObject:image forKey:key cost:cost]; } if (toDisk) { dispatch_async (self .ioQueue, ^{ NSData *data = imageData; if (image && (recalculate || !data)) { #if TARGET_OS_IPHONE int alphaInfo = CGImageGetAlphaInfo (image.CGImage); BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); BOOL imageIsPng = hasAlpha; if ([imageData length] >= [kPNGSignatureData length]) { imageIsPng = ImageDataHasPNGPreffix(imageData); } if (imageIsPng) { data = UIImagePNGRepresentation (image); } else { data = UIImageJPEGRepresentation (image, (CGFloat )1.0 ); } #else data = [NSBitmapImageRep representationOfImageRepsInArray: image.representations usingType: NSJPEGFileType properties:nil ]; #endif } NSString *cachePathForKey = [self defaultCachePathForKey:key]; NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil ]; }); } }
保存到磁盘时,图片的命名规则如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (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; }
3.清除缓存 程序出现内存警告时会清除内存中的缓存;退出应用、进入后台时则根据策略清除相关磁盘缓存:
清除过期的文件,默认一星期;
超过最大缓存限制时,遍历删除最旧的文件,直到当前缓存文件的大小为最大缓存的一半;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 - (void )cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock { dispatch_async (self .ioQueue, ^{ NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow: -self .maxCacheAge]; NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0 ; NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL ]; NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey ]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue ; } } for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil ]; } if (self .maxCacheSize > 0 && currentCacheSize > self .maxCacheSize) { const NSUInteger desiredCacheSize = self .maxCacheSize / 2 ; NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions: NSSortConcurrent usingComparator:^NSComparisonResult (id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey ] compare:obj2[NSURLContentModificationDateKey ]]; }]; for (NSURL *fileURL in sortedFiles) { if ([_fileManager removeItemAtURL:fileURL error:nil ]){ NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey ]; currentCacheSize -= [totalAllocatedSize unsignedIntegerValue]; if (currentCacheSize < desiredCacheSize) { break ; } } } } if (completionBlock) { dispatch_async (dispatch_get_main_queue(), ^{ completionBlock(); }); } }); }
3.后记 SD 是一个优秀的网络图片下载缓存库,让我们在视图中加载网络图片或缓存图片到本地变得很轻松,有很多值得学习的地方:
库中各类职责清晰;
网络图片的下载、解码、缓存等都是异步进行,保证了主线程的流畅性;
大量使用 NSOperation 和 GCD,为我们处理线程同步问题提供了范例;
图片加载前,先在异步线程中解码成位图,极大优化了应用性能;
设计良好的缓存策略,提高了应用的性能表现;
本文是根据原项目中使用的 3.7.3 版本来分析的,目前SD的最新版本号为 4.4.1。大致看了下最新的版本,网络下载部分已经使用了 URLSession,也新增了很多新的类,后面有时间会继续研究一下~
相关参考:
#©AppleDev-UIImage