0%

SDWebImage源码浅析

SDWebImage这个第三方库大家都不陌生吧,一个网络请求图片用的。其官方解释是:作为UIImageView的类别,支持缓存的异步下载图片。

它有一些特点:

一个带有管理网络图片下载和缓存的UIImageView类别
一个异步图片下载器
一个提供内存和磁盘缓存图片,并且能够自动清理过期的缓存
支持GIF图片
支持WebP
图片后台解压图片
保证通一个URL不会下载多次
保证假的URL不会返回加载
保证主线程不会堵塞
高性能
使用GCDARC
支持Arm64

从它在github上面的星星数量(已过万),说明它在图片下载和缓存方面还是可以的,记得我13年刚搞iOS的时候就有用过这个库。那个时候年少无知,学习能力不行,只要会用就行,还管它内部怎么个实现法呢……前段时间有同事问我,怎么用SD下载Image,我当时就凌乱了,尼玛,这么有名的库都不会,其实,我当时只是在需要改变它的缓存时间时,初略看过(然后,老大说别动第三方库,所以处理缓存时间那事就完了)。最近,有在细看SD,本文中,会对SD源码(3.7.3版本)做解析,当然只针对自己常用的做分析,什么gif、webP图片格式就算了哈……如果想对图片有深入的了解,请移步至YY

SD目录结构

以上是SD的目录结构,很清晰,一目了然!

Downloader下载图片
Cache缓存图片
Utils管理图片(SDWebImageManager管理SDWebImageDownloaderSDImageCache,是SD的管理者;SDWebImageDecoder用来解压图片; SDWebImagePrefetcher预加载图片)
Categories分类

Cache

SDImageCache用来维持内存缓存和可选的磁盘缓存,异步的磁盘缓存不会堵塞UI。该类就做这些事吧:缓存数据、查询获取数据、清理数据、获取存储容量相关数据。

缓存图片

用图片的URL作为key来存储,用MD5加密获取的字符串来作为数据名。
在头文件中可以看到关于存储数据的相关函数:

1
2
3
4
5
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;

- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
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
/**
 *  缓存图片
 *
 *  @param image       图片
 *  @param recalculate 是否重新计算
 *  @param imageData   imageData
 *  @param key         缓存的key
 *  @param toDisk      是否缓存到磁盘
 */
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 缓存到内存
    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
                // We need to determine if the image is a PNG or a JPEG
                // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                // The first eight bytes of a PNG file always contain the following (decimal) values:
                // 137 80 78 71 13 10 26 10

                // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
                // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
                // 判断是PNG还是JPEG
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;

                // But if we have an image data, we will look at the preffix
                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
            }
            
            // 有数据 则缓存
            if (data) {
                // 缓存路径是否存在 没有存在则创建
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                // 根据URL key 来获取存储的路径
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // 转换成URL
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
                // 存储数据
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // disable iCloud backup
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

kPNGSignatureData 是PNG格式图片的签名数据,在初始化的时候,它被赋值为

1
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];

kPNGSignatureBytes数组,则是PNG图片格式的签名字节;ImageDataHasPNGPreffix C方法用来判断数据是否为PNG图片;相关关系可看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// PNG signature bytes and data (below)
static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
static NSData *kPNGSignatureData = nil;

BOOL ImageDataHasPNGPreffix(NSData *data);

BOOL ImageDataHasPNGPreffix(NSData *data) {
    NSUInteger pngSignatureLength = [kPNGSignatureData length];
    if ([data length] >= pngSignatureLength) {
        if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) {
            return YES;
        }
    }

    return NO;
}

查询获取数据

获取内存存储数据,直接用NSCache对象去查询即可。而获取磁盘存储数据时却做了优化(取出的时候根据需求有解压)。

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
/**
 *  从内存中查找图片
 *
 *  @param key key
 *
 *  @return return value description
 */
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

/**
 *  从磁盘中查找图片
 *
 *  @param key key description
 *
 *  @return return value description
 */
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {

    // First check the in-memory cache...
    // 从内存中取
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }

    // Second check the disk cache...
    // 从磁盘中取
    UIImage *diskImage = [self diskImageForKey:key];
    // 如果图片存在 并且要缓存到内存中 则将图片缓存到内存
    if (diskImage && self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

/**
 *  从磁盘中获取数据
 *
 *  @param key key description
 *
 *  @return return value description
 */
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
    // 默认路径
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }
    // 自定义路径
    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}

/**
 *  根据key在磁盘中获取图片
 *
 *  @param key key
 *
 *  @return <#return value description#>
 */
- (UIImage *)diskImageForKey:(NSString *)key {
    // 获取 data
    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;
    }
}

当然该类也提供了一个包含block参数的多线程查询方法。

SDImageCacheTypeNone 没有缓存,即该图片还没有下载
SDImageCacheTypeDisk 缓存在磁盘的图片
SDImageCacheTypeMemory 缓存在内存的图片

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
/**
 *  从磁盘查询数据
 *
 *  @param key       key
 *  @param doneBlock block回调
 *
 *  @return return value description
 */
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }
    // 如果key为空
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    // 首先查询内存
    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;
}

清理数据

移除数据只要移除缓存的文件即可。

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
/**
 *  移除文件
 *
 *  @param key        key
 *  @param fromDisk   是否从磁盘移除
 *  @param completion block回调
 */
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
    
    if (key == nil) {
        return;
    }
    // 如果有缓存 则从缓存中移除
    if (self.shouldCacheImagesInMemory) {
        [self.memCache removeObjectForKey:key];
    }
    // 从磁盘移除 异步操作
    if (fromDisk) {
        dispatch_async(self.ioQueue, ^{
            // 直接删除文件
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            // 回调
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
    
}

清理数据分为全部清理( clear打头)和部分清理(clean打头)。

全部清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)clearDisk {
    [self clearDiskOnCompletion:nil];
}

- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
    dispatch_async(self.ioQueue, ^{
        // 清理文件
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        // 创建文件路径
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];

        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

部分清理

部分清理主要表现在:1、清理过期的文件;2、当缓存空间超过最大缓存阀值时,按照时间顺序删除超过最先缓存的图片,使缓存空间达到最大缓存阀值的一半。
部分清理可以手动清理,即调用cleanDiskWithCompletionBlock:方法即可;当然在APP进入后台的时候该类会自动清理,它调用backgroundCleanDisk 方法,从而调用cleanDiskWithCompletionBlock:方法。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
 *  清理磁盘
 *
 *  @param completionBlock <#completionBlock description#>
 */
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        // 获取存储路径
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        // 获取相关属性数组
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        // 预取缓存文件中有用的属性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        // 计算出过期的时间
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        // 缓存的文件
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        // 去掉过期缓存文件后 缓存大小
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        // 需要删除的文件 如果是目录则跳过 将没有过期的文件下来并且计算它们的大小(如果该大小超过最大的阀值 则得删除一些缓存)
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            // Skip directories.
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // Store a reference to this file and account for its total size.
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        // 删掉过期的文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        // 如果重新需要缓存的文件大小超过已经设定的缓存大小阀值 就得按时间来删除一些文件 知道缓存的文件大小小于已设定值的一半
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // Delete files until we fall below our desired cache size.
            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();
            });
        }
    });
}

/**
 *  在后台清理
 */
- (void)backgroundCleanDisk {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    [self cleanDiskWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

获取存储容量相关数据

查询缓存图片的数量以及大小。理解该三个方法需要NSFileManager玩得比较熟练。

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
/**
 *  得到缓存容量大小
 *
 *  @return return value description
 */
- (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;
}

/**
 *  得到缓存图片的数量
 *
 *  @return return value description
 */
- (NSUInteger)getDiskCount {
    __block NSUInteger count = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        count = [[fileEnumerator allObjects] count];
    });
    return count;
}

/**
 *  获取缓存图片数量及总容量
 *
 *  @param completionBlock block 回调
 */
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

    dispatch_async(self.ioQueue, ^{
        NSUInteger fileCount = 0;
        NSUInteger totalSize = 0;

        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:@[NSFileSize]
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        for (NSURL *fileURL in fileEnumerator) {
            NSNumber *fileSize;
            [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
            totalSize += [fileSize unsignedIntegerValue];
            fileCount += 1;
        }

        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(fileCount, totalSize);
            });
        }
    });
}

Downloader

下载管理主要由SDWebImageDownloader类来完成,而主要进行下载操作的则是SDWebImageDownloaderOperation 类。

SDWebImageDownloader

SDWebImageDownloader是个图片下载管理类。

下载选项

对于下载图片,我们有各自的需求,有的需要适时获取下载的进度;有的需要在后台下载;有的需要图片下载处于一个高优先级。。。。。在这里你能够得到相关选择来完成自己的操作。ps:在tableviewcell中,我们一般使用SDWebImageDownloaderLowPriority低优先级,这样cell滑动的时候就不会那么卡顿。

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
/// 下载选项枚举
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    /// 低优先权
    SDWebImageDownloaderLowPriority = 1 << 0,
    /// 下载显示进度
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /**
     * By default, request prevent the of NSURLCache. With this flag, NSURLCache
     * is used with default policies.
     */
    /// 默认情况下请求是不用NSURLCache的。但是如果使用该标志,默认的缓存策略就是使用NSURLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /**
     * Call completion block with nil image/imageData if the image was read from NSURLCache
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
     */
    /// 如果图片是从NSURLCache里面读取,则使用nil来作为回调block的入参(与`SDWebImageDownloaderUseNSURLCache`组合使用)
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    /// 在iOS4+ 能够在程序进入后台后继续下图片,通过向系统申请额外的时间在后台来完成数据请求操作。如果后台任务终止,则该操作会被取消
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting 
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    /// 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES 来处理存储在 NSHTTPCookieStore 的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
    /// 允许使用不信任的SSL证书 这主要是用于测试的目的 在生产环境中得小心
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /**
     * Put the image in the high priority queue.
     */
    /// 将图片下载置于优先级较高的队列
    SDWebImageDownloaderHighPriority = 1 << 7,
};

下载顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 执行顺序
执行顺序有先进先出、先进后出两种方式。
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    /**
     * Default value. All download operations will execute in queue style (first-in-first-out).
     */
    // 先进先出 默认操作顺序
    SDWebImageDownloaderFIFOExecutionOrder,

    /**
     * All download operations will execute in stack style (last-in-first-out).
     */
    // 先进后出
    SDWebImageDownloaderLIFOExecutionOrder
};

回调block

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
/**
 *  下载进度block
 *
 *  @param receivedSize 已收到数据大小
 *  @param expectedSize 应该受到数据大小
 */
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);

/**
 *  下载完成block
 *
 *  @param image    下载好的图片
 *  @param data     下载的数据
 *  @param error    错误信息
 *  @param finished 是否完成
 */
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);

/**
 *  过滤请求头部信息block
 *
 *  @param url     URL
 *  @param headers 请求头部信息
 *
 *  @return return value description
 */
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

SDWebImageDownloader源码分析

并发请求图片,最大请求图片的并发数为6;上面提到了进度和完成block,SDWebImageDownloader 有一个字典URLCallbacks来根据URL来存储每一个图片请求的回调block;图片请求类默认为SDWebImageDownloaderOperation类(后面细说)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface SDWebImageDownloader ()
// 下载操作队列
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
// 最后添加的操作 先进后出顺序顺序
@property (weak, nonatomic) NSOperation *lastAddedOperation;
// 图片下载类
@property (assign, nonatomic) Class operationClass;
// URL回调字典 以URL为key,你装有URL下载的进度block和完成block的数组为value
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks;
// HTTP请求头
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
// 并行的处理所有下载操作的网络响应
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

@end

允许多张图片并行同时下载,为了保证URLCallbacks的安全,确保在同一时间只能有一个线程操作URLCallbacks,所以将URLCallbacks的相关操作(赋值、删除)放在了barrierQueue队列中。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
 *  下载操作
 *
 *  @param url            下载URL
 *  @param options        下载操作选项
 *  @param progressBlock  进度block
 *  @param completedBlock 完成block
 *
 *  @return 遵循SDWebImageOperation协议的对象
 */
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    // 下载对象
    __block SDWebImageDownloaderOperation *operation;
    // weak self
    __weak __typeof(self)wself = self;
    // 添加设置回调
    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        // 设置延时时长 为 15.0秒
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 创建请求 并根据下载选项设置请求的相关属性
        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 为了防止潜在的重复缓存(NSURLCache + SDImageCache),如果没有告诉我们需要缓存,我们不使用图片请求的缓存操作
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        // 是否处理cookies
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        // 创建下载对象 在这里是 SDWebImageDownloaderOperation 类
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             // strong self
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             // URL回调数组
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 // 根据URL获取 相关回调的数组
                                                                 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];
                                                                }
                                                            });
                                                            // 处理下载完成回调
                                                            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];
                                                            });
                                                        }];
        // 是否解压图片
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        // URL证书、用户名密码
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        // 设置队列的优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        // 将下载操作添加到下载队列中
        [wself.downloadQueue addOperation:operation];
        // 如果是先进后出操作顺序 则将该操作置为最后一个操作
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];

    return operation;
}

/**
 *  添加设置回调
 *
 *  @param progressBlock  下载进度block
 *  @param completedBlock 下载完成block
 *  @param url            下载URL
 *  @param createCallback 创建回调block
 */
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    // URL不能为空
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    // dispatch_barrier_sync 保证同一时间只有一个线程操作 URLCallbacks
    dispatch_barrier_sync(self.barrierQueue, ^{
        // 是否第一次操作
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        // 处理 同一个URL的单个下载
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        // 将 进度block和完成block赋值
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        // 已URL为key进行赋值
        self.URLCallbacks[url] = callbacksForURL;
        
        // 如果是第一次下载 则回调
        if (first) {
            createCallback();
        }
    });
}

downloadImageWithURL: options: progress: completed:方法是该类的核心,它返回一个遵循SDWebImageOperation 协议的对象。

1
2
3
4
5
@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

该协议只定义了一个cancel方法,用来取消图片下载请求。

SDWebImageDownloaderOperation

SDWebImageDownloaderOperation是一个继承自NSOperation并遵循SDWebImageOperation协议的类。

1
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageOperation>

SDWebImageDownloaderOperation主要通过NSURLConnection来获取数据。
通过 NSNotificationCenter来告诉其他类下载的相关进程。

1
2
3
4
5
6
7
8
/// 开始下载
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
/// 开始接受到数据
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
/// 停止下载
NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
/// 完成下载
NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";

本类中用到了NSURLConnectionDataDelegateNSURLConnectionDelegate的几个代理方法:

NSURLConnectionDataDelegate

1
2
3
4
5
6
7
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

- (void)connectionDidFinishLoading:(NSURLConnection *)connection;

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;

NSURLConnectionDelegate

1
2
3
4
5
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

我现在只分析- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;两个主要方法,其他相关的方法,在研究AFNetworking(虽然现在已用NSURLSession来处理)再细看。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
 *  接受到服务端反应
 *
 *  @param connection connection description
 *  @param response   response description
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
    //'304 Not Modified' is an exceptional one
    // '304 Not Modified' 特殊处理
    if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
        // 下载图片的期望大小
        NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
        self.expectedSize = expected;
        // 进度block回调
        if (self.progressBlock) {
            self.progressBlock(0, expected);
        }
        // 创建imageData
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        // response赋值
        self.response = response;
        // 通知 已经接受到消息相应
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        // 获取状态码
        NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
        
        //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
        //In case of 304 we need just cancel the operation and return cached image from the cache.
        // 如果为304 服务端图片没有修改 则取消内部相关操作
        if (code == 304) {
            [self cancelInternal];
        } else { // 取消网络请求图片
            [self.connection cancel];
        }
        // 通知 停止下载图片
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
        // 完成block回调 下载错误
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
        }
        CFRunLoopStop(CFRunLoopGetCurrent());
        [self done];
    }
}

/**
 *  接收到数据
 *
 *  @param connection <#connection description#>
 *  @param data       <#data description#>
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // 追加数据
    [self.imageData appendData:data];
    
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf

        // Get the total bytes downloaded
        // 获取已下载的图片大小
        const NSInteger totalSize = self.imageData.length;

        // Update the data source, we must pass ALL the data, not just the new bytes
        // 更新数据源,我们必须传入所有的数据 并不是这次接受到的新数据
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        // 如果宽和高都为0 即第一次接受到数据
        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);

                // When we draw to Core Graphics, we lose orientation information,
                // which means the image below born of initWithCGIImage will be
                // oriented incorrectly sometimes. (Unlike the image born of initWithData
                // in connectionDidFinishLoading.) So save it here and pass it on later.
                // 当我们绘制 Core Graphics 时,我们将会失去图片方向的信息,这意味着有时候由initWithCGIImage方法所创建的图片的方向会不正确(不像在 connectionDidFinishLoading 代理方法里面 用 initWithData 方法创建),所以我们先在这里保存这个信息并在后面使用。
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }
        // 已经接受到数据 图片还没下载完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            // 先去第一张 部分图片
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // Workaround for iOS anamorphic image
            // 对iOS变形图片工作(不是很理解)
            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
            // 存储图片
            if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                // 获取key
                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);
                // 完成block回调
                dispatch_main_sync_safe(^{
                    if (self.completedBlock) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }

        CFRelease(imageSource);
    }
    // 进度block回调
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

- connection:didReceiveData:方法里面的数据处理有点费解。

再来看看start开始请求方法,在该方法中启动了runloop,当然在其他代理方法,不管请求数据是否成功,都得停止。

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
- (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) {
        // 进度block回调
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        // 通知 开始下载
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
        // 开始运行 runloop
        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
            // Make sure to run the runloop in our background thread so it can process downloaded data
            // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
            //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }
        //  没有完成 则取消
        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        // connection对象不存在 回调相关错误信息
        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
}

本类有两个变量来标识请求状态,在外层可以根据这两个标志能够来做相关处理。

1
2
3
4
// 是否正在请求数据中
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
// 请求是否已完成
@property (assign, nonatomic, getter = isFinished) BOOL finished;

Utils

该文件夹里面主要是对图片进行操作的相关工具类,SDWebImageManagerSD的核心所在,管理图片的缓存和下载,我们项目中熟悉的[imagevie sd_setImageWithURL:]则主要用到了该类;SDWebImageDecoder是强制解压图片的,该方法主要是防止图片加载时有延时,但是用这个方法会导致内存暴涨等问题;SDWebImagePrefetcher是预取图片类。

SDWebImageManager

SDWebImageManager管理着缓存SDImageCache和下载SDWebImageDownloader类的对象。我们在这个类可以得到关于下载和缓存的相关状态。本类有12个关于管理的SDWebImageOptions操作类型,与SDWebImageDownloaderSDWebImageDownloaderOptions操作类型相似。

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
/// 管理操作类型
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
     * This flag disable this blacklisting.
     */
    // 默认情况下,当一个URL下载失败后,这个URL将会加入黑名单,下次不会尝试下载。这个标志可以不让它加入黑名单
    SDWebImageRetryFailed = 1 << 0,

    /**
     * By default, image downloads are started during UI interactions, this flags disable this feature,
     * leading to delayed download on UIScrollView deceleration for instance.
     */
    // 默认情况下,图片在UI交互的时候下载,这个标志可以取消这个特点。下载图片延时到UIScrollView减速的时候
    SDWebImageLowPriority = 1 << 1,

    /**
     * This flag disables on-disk caching
     */
    // 只存储在内存 不存储在磁盘
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * This flag enables progressive download, the image is displayed progressively during download as a browser would do.
     * By default, the image is only displayed once completely downloaded.
     */
    // 让图片逐渐的下载,在下载过程中图片像浏览器一样逐渐的显示出来。默认情况下,图片在下载完成以后才显示出来。
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
     * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
     * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
     * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
     *
     * Use this flag only if you can't make your URLs static with embedded cache busting parameter.
     */
    // 尽管图片有缓存,但是希望能够响应缓存控制,并且在需要的情况下从服务端刷新图片。 磁盘缓存将会被NSURLCache处理,而不是SDWebImage处理,因为SDWebImage将会带来轻微的性能下降。这个操作能够在相同URL后面帮助改变图片,像Facebook的profile pics一样。如果缓存图片被刷新了,那完成block将会用被缓存的图片调用一次,并在图片下载完后再调用一次。使用该标志只有在你需要刷新缓存图片的时候
    SDWebImageRefreshCached = 1 << 4,

    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    // 后台请求
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * By default, image are loaded in the order they were queued. This flag move them to
     * the front of the queue and is loaded immediately instead of waiting for the current queue to be loaded (which 
     * could take a while).
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    // 在默认情况下,占位符图片在图片下载的时候就会被加载。使用这个标志,将会延时加载占位符图片直到图片下载成功。
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * We usually don't call transformDownloadedImage delegate method on animated images,
     * as most transformation code would mangle it.
     * Use this flag to transform them anyway.
     */
    // 我们平常不调用动画图片的 transformDownloadedImage 代理方法,大部分的转换代码会损坏它,使用这个标志能在任何情况下转换它们
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * By default, image is added to the imageView after download. But in some cases, we want to
     * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
     * Use this flag if you want to manually set the image in the completion when success
     */
    // 默认情况下,图片下载完成以后就添加到imageview上。但是在某些情况下,我们想着设置图片之前做一些操作(比如增加一些动画什么之类的)如果你想图片下载成功后在完成block里面手动设置图片,那就使用这个标志
    SDWebImageAvoidAutoSetImage = 1 << 11
};

同时也定义了关于下载完成和获取缓存key的block回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 *  下载图片block
 *
 *  @param image     图片
 *  @param error     错误信息
 *  @param cacheType 缓存类型
 *  @param imageURL  图片URL
 */
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);

/**
 *  根据URL过滤获取缓存的key
 *
 *  @param url url description
 *
 *  @return return value description
 */
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

本来还定义了一个协议 SDWebImageManagerDelegate,用来设置是否下载图片以及在缓存图片之前对图片进行转换。
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
@protocol SDWebImageManagerDelegate <NSObject>

@optional

/**
 * Controls which image should be downloaded when the image is not found in the cache.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param imageURL     The url of the image to be downloaded
 *
 * @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
 */
/// 在图片没有在缓存里的时候,控制图片是否下载。 返回NO,如果图片没有在缓存里面的时候不下载图片。如果该方法没有被实现,那就为YES。
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

/**
 * Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
 * NOTE: This method is called from a global queue in order to not to block the main thread.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param image        The image to transform
 * @param imageURL     The url of the image to transform
 *
 * @return The transformed image object.
 */
/// 允许马上转换图片在图片下载完成之后,并在在缓存图片之前。注意,该方法在全局队列里面完成 为了不堵塞主线程。
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

该类除了下载对象和缓存对象,还有两个集合容器来装失败的URL以及管理图片下载的操作线程。
1
2
3
4
5
6
7
8
9
10
11
@interface SDWebImageManager ()
// 缓存对象
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
// 下载对象
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
// 失败的URL集合
@property (strong, nonatomic) NSMutableSet *failedURLs;
// 操作线程数组
@property (strong, nonatomic) NSMutableArray *runningOperations;

@end

其他准备工作已做好,来瞧瞧下载管理图片的代码吧。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
 *  下载管理图片
 *
 *  @param url            URL
 *  @param options        操作选项
 *  @param progressBlock  进度block
 *  @param completedBlock 完成block
 *
 *  @return <#return value description#>
 */
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    // 一个非常普通的错误就是把NSString对象当做NSURL来使用,但是Xcode却不能提示这种类型的错误。所以这里做一下处理
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 防止URL为空时 程序崩溃
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    // 创建对象 并弱引用
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    // 是否是失败的URL
    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
    // url长度为0 获取 该url已下载失败过,并且操作类型没有禁用掉黑名单列表 那么该图片就下载失败,返回失败block
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }
    // 将该操作线程添加到数值里面
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    // 根据URL获取缓存的key
    NSString *key = [self cacheKeyForURL:url];
    // 根据key从缓存里面找
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        // 如果线程被取消 则删除该操作
        if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }

            return;
        }
        // 没有图片 或者 操作类型会刷新缓存 并且 delegate允许下载图片
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            // 有图片 并且操作类型为 SDWebImageRefreshCached (先用缓存图片回调block 图片下载完成后再回调block)
            if (image && options & SDWebImageRefreshCached) {
                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);
                });
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            // 根据SDWebImageOptions的类型 得到SDWebImageDownloaderOptions 都是一些位运算
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (image && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            // 下载图片
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                if (weakOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                    // 操作被取消 则不做任何操作
                }
                else if (error) { // 出错
                    dispatch_main_sync_safe(^{
                        if (!weakOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });

                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        @synchronized (self.failedURLs) { // 添加到失败URL数组
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else { // 下载成功
                    if ((options & SDWebImageRetryFailed)) { // 操作类型为失败重新刷新
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    // 是否存储在磁盘
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    // 图片下载成功 并且需要转换图片
                    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 (!weakOperation.isCancelled) { // block回调
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    else { // 存储图片 并且 block回调
                        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);
                            }
                        });
                    }
                }

                if (finished) { // 如果已完成 则将该请求操作从数组中删除
                    @synchronized (self.runningOperations) {
                        [self.runningOperations removeObject:operation];
                    }
                }
            }];
            // 取消操作 从数组中删除该操作
            operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:weakOperation];
                }
            };
        }
        else if (image) { // 有图片 并且线程没有被取消 则返回有图片的block
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
            // Image not in cache and download disallowed by delegate
            // 图片没有被缓存 并且没有被代理通知可以下载图片 则返回没有图片的block
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
    }];

    return operation;
}

SDWebImageDecoder

SDWebImageDecoder主要是用来强制解压图片的。这里代码有些底层,不是很理解,后面在问题中有相关的描述。

SDWebImagePrefetcher

SDWebImagePrefetcher用来预取图片的。它用GCD在主线程中通过SDWebImageManager来预取图片,并通过delegateblock回到相关状态和进度。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* 开始预取URL数组的第几张图片
*
* @param index index description
*/
- (void)startPrefetchingAtIndex:(NSUInteger)index {
// 判断index是否越界
if (index >= self.prefetchURLs.count) return;
// 请求个数 +1
self.requestedCount++;
// 用SDWebImageManager 下载图片
[self.manager downloadImageWithURL:self.prefetchURLs[index] options:self.options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!finished) return;
// 完成个数 +1
self.finishedCount++;
// 有图片
if (image) {
// 进度block回调
if (self.progressBlock) {
self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
}
}
else {
// 进度block回调
if (self.progressBlock) {
self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
}
// 下载完成 但是没图片 跳过个数 +1
// Add last failed
self.skippedCount++;
}
// delegate 回调
if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]) {
[self.delegate imagePrefetcher:self
didPrefetchURL:self.prefetchURLs[index]
finishedCount:self.finishedCount
totalCount:self.prefetchURLs.count
];
}
// 如果完成个数与请求个数相等 则下载已完成
else if (self.finishedCount == self.requestedCount) {
// delegate报告完成状态
[self reportStatus];
// 完成block回调
if (self.completionBlock) {
self.completionBlock(self.finishedCount, self.skippedCount);
self.completionBlock = nil;
}
self.progressBlock = nil;
}
}];
}

/**
* 报告完成的状态
*/
- (void)reportStatus {
NSUInteger total = [self.prefetchURLs count];
// delegate回调
if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didFinishWithTotalCount:skippedCount:)]) {
[self.delegate imagePrefetcher:self
didFinishWithTotalCount:(total - self.skippedCount)
skippedCount:self.skippedCount
];
}
}

/**
* 开始预取URL
*
* @param urls URL数组
*/
- (void)prefetchURLs:(NSArray *)urls {
[self prefetchURLs:urls progress:nil completed:nil];
}

/**
* 预取URL
*
* @param urls url数组
* @param progressBlock 进度block
* @param completionBlock 完成block
*/
- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock {
// 取消预取 防止重复的操作
[self cancelPrefetching]; // Prevent duplicate prefetch request
// 开始时间
self.startedTime = CFAbsoluteTimeGetCurrent();
self.prefetchURLs = urls;
self.completionBlock = completionBlock;
self.progressBlock = progressBlock;

__weak SDWebImagePrefetcher *weakSelf = self;
// 如果URL为空
if (urls.count == 0) {
if (completionBlock) {
completionBlock(0,0);
}
} else {
// http://oleb.net/blog/2013/07/parallelize-for-loops-gcd-dispatch_apply/
// Optimize the maxConcurrentdownloads for effeciency. Since caching operations are involved that are non-trivial using
// dispatch_apply might be helpful.
// 用dispatch_apply 优化并发下载的性能
NSInteger maxNumberOfImages = self.prefetchURLs.count;

dispatch_apply(maxNumberOfImages/self.maxConcurrentDownloads, self.prefetcherQueue, ^(size_t index) {
size_t i = index * self.maxConcurrentDownloads;
size_t stop = i + self.maxConcurrentDownloads;
do {
[weakSelf startPrefetchingAtIndex:i++];
} while (i < stop);
});
// 现在剩余的图片
// Download remaining images.
for (size_t i = maxNumberOfImages - (maxNumberOfImages % self.maxConcurrentDownloads); i < (size_t)maxNumberOfImages; i++) {
[self startPrefetchingAtIndex:i];
}
}
}

看到它预取图片的代码,我就想起了我在项目中上传多张图片时的循环代码,虽然我的那段代码能够完成功能,但是还是不够高大上哈。

Categories

Categories文件夹里面全都是类别文件,可以分为两类吧:1、显示图片;2、操作Image数据。
本文主要分析与显示图片的UIView+WebCacheOperation基础文件和UIImageView+WebCache大众文件,因为其他关于是图片的扩张和UIImageView+WebCache类似。

UIView+WebCacheOperation

UIView+WebCacheOperation类别主要存储下载操作,这样其他继承自View需要显示Image的视图,就可以使用这些方法。

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
/**
 *  runtime获取存储操作线程的字典
 *
 *  @return <#return value description#>
 */
- (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;
}
/**
 *  设置下载图片的线程
 *
 *  @param operation 线程
 *  @param key       key
 */
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    // 在设置之前先cancel
    [self sd_cancelImageLoadOperationWithKey:key];
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}

/**
 *  根据key取消相关的下载操作
 *
 *  @param key <#key description#>
 */
- (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)]){ // 实现了 SDWebImageOperation 协议
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

/**
 *  移除下载操作 并没有取消该操作
 *
 *  @param key <#key description#>
 */
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary removeObjectForKey:key];
}

UIImageView+WebCache

UIImageView+WebCache类别是我们在项目用到最多的,谁叫项目中有那么多地方需要显示图片呢。。。

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
/**
 *  下载Image
 *
 *  @param url            URL
 *  @param placeholder    占位符图片
 *  @param options        操作类型
 *  @param progressBlock  进度block
 *  @param completedBlock 完成block
 */
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    // 取消正在下载的线程
    [self sd_cancelCurrentImageLoad];
    // runtime设置 URL属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 如果不需要延时设置占位符图片 则先显示占位符图片
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    
    if (url) { // URL存在
        // check if activityView is enabled or not
        if ([self showActivityIndicatorView]) { // 是否在下载图片的时候显示指示器
            [self addActivityIndicator];
        }

        __weak __typeof(self)wself = self;
        // 用SDWebImageManager 来下载管理图片
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            // 下载完成 移除指示器
            [wself removeActivityIndicator];
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                // 不自动设置图片
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                // 设置图片 并Layout
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    // 延时设置占位符图片
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                // 下载完成并且回调
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 存储设置操作对象
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{ // 移除指示器 并回调错误 block
            [self removeActivityIndicator];
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            if (completedBlock) {
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

所遇问题

SDScaledImageForKey 方法

为什么在block回调图片之前要使用SDScaledImageForKey方法缩放图片呢?

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
/**
 *  缩放图片
 *
 *  @param key   key URL
 *  @param image 图片
 *
 *  @return return value description
 */
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
    if (!image) {
        return nil;
    }
    // animation Image
    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;
    }
}

举个例子吧,我们在命名图片的时候都是以xxxx@2x.pngxxxx@3x.png结尾的,但是我们在创建 Image的时候并不需要在后面添加倍数,只需调用[UIImage imageNamed:@"xxxx"]即可,因为Xcode会帮我们根据当前分比率自动添加后缀倍数。如果你有一张72*72的二倍图片,当你以[UIImage imageNamed:@"xxxx@2x"]的方法加载图片的时候,你会发现图片被拉伸了,它的大小变为144*144了,这是因为Xcode自动添加二部后,以xxxx@2x@2x去查找图片,当它找不到的时候就把xxxx@2x图片当做一倍图片处理,所以图片的size就变大了,所以SDScaledImageForKey方法就是解决这件事情,以防url里面包含@"2x"、@"3x"等字符串,从而使图片size变大。

decodedImageWithImage: 方法

为什么从磁盘里面取出图片后,block回调之前要解压图片呢?因为图片在ImageView上面显示的时候需要解压,而这个解压操作是在主线程里面进行的,比较耗时,这样就会产生延时效果,而用decodedImageWithImage:在后台解压能够解决这一问题,但是这种用空间换时间的方法也存在着内存暴增甚至崩溃等问题,所以自己得权衡一下。这就是为什么SDImageCache、SDWebImageDownloader、SDWebImageDownloaderOperation类中都有shouldDecompressImages (是否解压图片)值存在的原因。
关于图片解压的问题,可以看Avoiding Image Decompression Sickness这篇博客。解压图片而产生的相关问题,以及解决方案的由来可以看How to solve the memory consumption for the decodedImageWithImage这个issue。

addReadOnlyCachePath: 方法

增加一个只读的缓存的路径,从diskImageDataBySearchingAllPathsForKey:方法看出,当SDImageCache找不到缓存的时候,才会尝试到这个路径下去查找。在从缓存中查找的时候,数组customPaths的字符串是通过addReadOnlyCachePath:方法添加进去,在Demo的AppDelegate里面有写这段代码。但是我还没搞清楚这段代码具体作用,因为在该类中只要在磁盘查找时才有操作,而缓存图片的时候并没有暴露出设置缓存路径的方法。所以我提了个issue.

NS_OPTIONS vs NS_ENUM

这两个都是用来定义枚举的,但是他们的区别、优缺点在哪里呢?

清理缓存的逻辑

设置最大缓存空间大小。在程序即将退出以及进入后台的时候清理缓存。首先清理缓存时间过期的资源,如果清理完成以后,所占的大小还是大于最大阀值,则按时间顺序来清理,使清理后的空间大小是最大阀值的一半。当然最重要的还是设置最大阀值。

clear vs clean

哈哈,其实这个不算是什么问题,或许是我英语差吧,总感觉这两个词的意思差不过。在看完源码以后我才知道。clear:清除(清除干净,除掉嘛,清除所有的缓存文件)。 clean:清理(就简单清理一下,清理过期的缓存文件)。

位运算操作

依稀记得在大学学计算机网络的时候有接触过,后面就一直没有接触了,第一次看到还是有点懵的(好菜的说)。当然Xcodelldb状态下可以进行位运算。详情请看

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
取反(NOT) ~  (数字1成为0,0成为1)
~ 0111
= 1000

按位或 (OR)| (两个相应的二进位中只要有一个为1,该位的结果就为1)
0101
| 0011
= 0111

按位异或 (XOR) ^ (如果某位不同则该位为1,否则该位为0)
0101
^ 0011
= 0110

按位与 (AND) & (两个相应的二进位都为1,该位的结果值才为1,否则为0)
0101
& 0011
= 0001

左移
0001
<< 3 (左移3位)
= 1000

右移
1010
>> 2 (右移2位)
= 0010

技术点归纳

与图片相关的,这些知识都是与图片相关的,无需一定要掌握(除非你要深度研究图片)。
1、PNG图片的判断。可以看SDImageCacheImageDataHasPNGPreffix方法。
2、Image Data判断图片类型以及根据data创建图片。可以查看NSData+ImageContentTypeUIImage+MultiFormat类。
3、图片解压(以及解压带来内存问题)。SDWebImageDecoder类中的decodedImageWithImage:方法的实现,牵扯到底层图片相关的操作。
4、gif图片的处理。虽然SDGIF的支持比较差劲。但是老外的纯技术精神,不得不佩服。请看issue#945
iOS开发技术,这些东西都是项目中比较常用的技术,一定要掌握。
1、NSFileManager的操作。在获取缓存大小相关内容时,需要我们熟练掌握NSFileManager类的相关用法。
2、NSCache类。在SDissue上面,由NSCache缓存引起的问题(如内存警告等)还是有很多的,后面才得到改善。
3、NSOperationNSOperationQueueNSThread@synchronized线程及操作队列相关类。
4、GCDdispatch_barrier_syncdispatch_apply等函数。
5、NSURLRequestNSURLResponseNSURLConnectionNSURLCache等网络请求相关类。
6、后台操作。
7、Runloop
8、Runtime
9、KVO

总结

SD的源码大概仔细的看了3次吧,然后也有看到它的一些issue,一个好的项目都是一点点完善的。看完以后,感触挺大的,毕竟第一次嘛。
1、个人位置。最近我们社区比较火的YY,今天上午初略看了他的代码,C基础、代码规范、编码功底都牛逼的不行,厚积薄发!一鸣惊人!感觉自己两年的iOS开发经验白搭了(当然了,其中也有个人因素,也有其他环境因素)。我个人觉得还是得去正规点的公司,至少产品经理要比较专业,因为如果一个项目都不能吸引你,那你也完全在浪费时间。还有小公司自己的好好考虑,因为我以前待过的纯互联网公司,就没有规模上过20人的,你能想象我以前的名片还是产品经理么?!现在想起来,好想笑。这里并没有贬低我以前东家的意思!!!
2、执行力。执行力还得大大加强,从这个月15号到25号,虽然这段时间也有加班,有在研究新的东西,但貌似搞技术最晚就昨晚吧,凌晨1点半才睡。蠢可以,但是懒就不行!
3、技术掌握范围。从上面归纳的技术点来看,我真的菜很菜非常菜!
4、学习方法。focus!focus!!focus!!!