SDWebImage

1.导入

1
2
3
4
5
platform :ios, '10.0'
//ASDF替换为自己项目的target名
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分类

作用:对外暴露设置图片的接口。

  1. 在新的图片加载前,取消本视图之前正在进行的下载任务;
  2. 交由 SDWebImageManager 接着处理图片下载逻辑;
  3. 保存当前下载任务;
  4. 图片下载成功或失败,回到主线程回调数据并给视图设置图片;

源码摘要:

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
// UIImageView+WebCache.m

- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock
{
//step.1 取消之前正在下载的操作
[self sd_cancelCurrentImageLoad];
//保存该视图用到的图片URL
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/*...*/
if (url) {
__weak __typeof(self)wself = self;
//step.2 由SDWebImageManager下载图片
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager
downloadImageWithURL:url
options:options
progress:progressBlock
completed:^(UIImage *image,
NSError *error, SDImageCacheType cacheType,
BOOL finished, NSURL *imageURL)
{
/*step.4 图片下载结果返回,回到主线层*/
}];
//step.3 保存此次下载操作
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
}
}

实现细节:

  • 1、创建下载任务

图片的下载任务是由 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;
}
  • 2、保存下载任务

上面返回的 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
// 属性Getter
- (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字典属性中。

  • 3、取消上一个正在进行的下载任务;

场景: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 {
// Cancel in progress downloader from queue
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();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}

所以,最终取消的是真正的图片下载任务cacheOperation对象,这是一个 NSOperation 实例。

2.Manager

作用:做下载前的各项检查,缓存下载完成后的图片并返回。

  1. 检测URL是否下载失败过,失败过的URL不再重复下载;
  2. 根据URL生成的key查询图片缓存,有缓存且不要求强制刷新缓存时使用缓存图片;
  3. 没有缓存时,交给 SDWebImageDownloader,封装请求并创建任务和队列,发起网络请求;
  4. 缓存下载完成后的图片,返回主线程;

源码摘要:

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
// SDWebImageManager.m

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
{
/*...*/
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];

//step.1 检测URL之前是否下载失败
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//URL错误或之前下载失败过,则不再重复下载失效的URL
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = ...;
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
//将当前下载 operation 加入到数组中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//step.2 根据URL生成对应的key查询缓存 没有缓存则开始下载
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]))
{
/*如果有缓存但采用了 RefreshCached 策略,
则执行回调并继续下载,以便让服务器刷新 NSURLCache 里的内容。*/
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
completedBlock(image, nil, cacheType, YES, url);
});
}
//step.4 使用imageDownloader开启网络下载
id <SDWebImageOperation> subOperation = [self.imageDownloader
downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)
{
//step.5 图片下载完成,根据设置进行图片转换和缓存
/.../
if (downloadedImage && finished) {
//step.5 图片下载完成,不需要转换时,将图片保存到缓存中并返回主线程
[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) {
//step.3 有缓存且不要求强制刷新缓存时,在缓存中找到图片,直接返回
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
}
}];
return operation;
}

3.Downloader

作用:配置Request,设置下载任务优先级与依赖关系。

  1. 设置 URLRequest;
  2. 创建 SDWebImageDownloaderOperation 任务,加入到并发数=6的 Queue 中,开始下载;
  3. 根据配置信息设置下载任务Operation的优先级和依赖关系;
  4. 下载成功,调用 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
// SDWebImageDownloader.m

- (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
{
/*...*/
//step.1 设置request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:url
cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?
NSURLRequestUseProtocolCachePolicy :NSURLRequestReloadIgnoringLocalCacheData)
timeoutInterval:timeoutInterval];

request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
request.allHTTPHeaderFields = wself.HTTPHeaders;

//step.2 创建下载 Operation
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;
//step.3 下载成功,执行完成的callback
__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与解码。

  • 重写了start函数;
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];
});
// 在默认模式下运行当前runlooprun,直到调用CFRunLoopStop停止运行
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
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];

// 图片解码
// 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...], 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];
}
//根据图片URL,判断图片比例,重新绘制新图并返回
else {
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
CGFloat scale = [UIScreen mainScreen].scale;

// "@2x.png" 或者 "@2x.png" 字符串的长度=7,加上@符号之前的名字,必定>=8
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {//URL包含@2x,推测为2倍图
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {//URL包含@3x,推测为3倍图
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 {
// while downloading huge amount of images
// 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{
// do not decode animated images
if (image.images) { return image; }

CGImageRef imageRef = image.CGImage;

//图片如果有alpha通道,就返回原始image,因为jpg图片有alpha的话,就不压缩
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);

// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

bool unsupportedColorSpace = (imageColorSpaceModel == 0 ||
imageColorSpaceModel == -1 ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace)//如果属于上述不支持的ColorSpace,则ColorSpace就使用RGB
colorspaceRef = CGColorSpaceCreateDeviceRGB();

CGContextRef context = CGBitmapContextCreate(NULL, width,
height,
CGImageGetBitsPerComponent(imageRef),
0,
colorspaceRef,
kCGBitmapByteOrderDefault |
kCGImageAlphaPremultipliedFirst);

// Draw the image into the context and retrieve the new image, which will now have an alpha layer
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 实现的。这一步的目的是提高加载图片的速度和效率,需要注意的是,它在解码时会占用相当一部分内存,是典型的空间换时间。如果你遇到了内存问题,可以尝试禁止自动解码或者清空缓存:

1
2
3
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] clearMemory];

7.图片缓存

写这篇文章时最新SD版本为3.7.3,缓存相关功能主要由 SDImageCache 类实现。这里的缓存包括了内存和磁盘两种方式。其中,内存缓存使用的是继承自 NSCache 的 AutoPurgeCache 类;磁盘缓存的操作被单独放在一个串行队列 ioQueue 中。这个类中主要的方法及实现如下:

1.查询缓存
  1. 从内存中检测;
  2. 从磁盘中检测(异步+串行队列);
  3. 磁盘中检测到缓存后,根据需要缓存到内存中;
  4. 返回结果;
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
{
...
//step.1 先从内存中检测
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}

//step.2 内存中没有再从磁盘中检测
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}

@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
//step.3 磁盘中检测到缓存后 根据需要缓存到内存中
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];
// 重置图片scale
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
{
...
//保存到内存 NSCache,cost为像素值
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;

//检测 图片data 的前缀是否是 PNG 格式
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
}

// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
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


SDWebImage
https://davidlii.cn/2018/01/28/sd.html
作者
Davidli
发布于
2018年1月28日
许可协议