转载

iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)

iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)

年也过完了、决定补完一下入行时就欠下的债。

参拜一下SDWebImage的源码。

并不是说一定要读如何如何、只是觉得源码的阅读是一种很好的学习方式。无论从架构还是技术点方面。


目录

  • 常见疑问(面试大全?)

    • 磁盘目录位于哪里?

    • 最大并发数、超时时长?

    • 图片如何命名?

    • 如何识别图片类型?

    • 所查找到的图片的来源?

    • 所有下载的图片都将被写入缓存?磁盘呢?何时缓存的?

    • 磁盘缓存的时长?清理操作的时间点?

    • 磁盘清理的原则?

    • 下载图片时、会使用缓存协议么?

    • 下载图片的URL必须是NSURL么?

    • 读取缓存以及读取磁盘的时候如何保证线程安全?

  • 相关知识点

    • NS_OPTIONS枚举与位运算

    • 内联函数

  • 准备工作

  • 工作原理

  • 业务层级

  • 核心代码(正常读取下载图片)

    • 最上层:UIView+WebCache

    • 逻辑层:SDWebImageManager

    • 业务层:

      • 缓存&&磁盘操作(SDImageCache)

      • 下载操作(SDWebImageDownloader)

  • 一些启发

    • 分层的接口API设计

    • 线程安全

    • 内联函数

    • 精细的缓存管理原则

    • 回调设计


常见疑问(面试大全?)

虽然我更推荐阅读源码、可如果实在没时间。这一段只要花费几分钟。
我还是比较喜欢把干货放在前面、方便伸手党(比如我)。
不过也不能保证涵盖全部问题、欢迎留言。

  • 磁盘目录位于哪里?

缓存在磁盘沙盒目录下Library/Caches
二级目录为~/Library/Caches/default/com.hackemist.SDWebImageCache.default

- (instancetype)init { return [self initWithNamespace:@"default"]; //   ~Library/Caches/default }

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns { NSString *path = [self makeDiskCachePath:ns]; return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory { if ((self = [super init])) { NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns] // Init the disk cache if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else { NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        } //  _diskCachePath = ~/Library/Caches/default/com.hackemist.SDWebImageCache.default }

你也可以通过[[SDImageCache sharedImageCache] addReadOnlyCachePath:bundledPath];来自定义一个路径。

但这个路径不会被存储使用、是给开发者自定义预装图片的路径。
  • 最大并发数、超时时长?

_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;
  • 图片如何命名?

这里写入缓存和写入磁盘是不同的。
写入缓存时、直接用图片url作为key

//写入缓存 NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];

写入磁盘时、用url的MD5编码作为key。可以防止文件名过长

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { const char *str = key.UTF8String; if (str == NULL) {
        str = "";
    } unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r); NSURL *keyURL = [NSURL URLWithString:key]; NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; NSString *filename = [NSString stringWithFormat:@"xxxxxxxxxxxxxxxx%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]]; return filename; //key == https://gss2.bdstatic.com/-fo3dSag_xI4khGkpoWK1HF6hhy/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=034361ab922397ddc274905638ebd9d2/d31b0ef41bd5ad64dddebb.jpg; //filename == f029945f95894e152771806785bc4f18.jpg; }
  • 如何识别图片类型?

通过NSData数据的第一个字符进行判断。

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { if (!data) { return SDImageFormatUndefined;
    } // File signatures table: http://www.garykessler.net/library/file_sigs.html uint8_t c;
    [data getBytes:&c length:1]; switch (c) { case 0xFF: return SDImageFormatJPEG; case 0x89: return SDImageFormatPNG; case 0x47: return SDImageFormatGIF; case 0x49: case 0x4D: return SDImageFormatTIFF; case 0x52: { if (data.length >= 12) { //RIFF....WEBP NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return SDImageFormatWebP;
                }
            } break;
        } case 0x00: { if (data.length >= 12) { //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding]; if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) { return SDImageFormatHEIC;
                }
            } break;
        }
    } return SDImageFormatUndefined;
}
  • 所查找到的图片的来源?

typedef NS_ENUM(NSInteger, SDImageCacheType) { /**
     * 从网上下载
    */ SDImageCacheTypeNone, /**
     * 从磁盘获得
     */ SDImageCacheTypeDisk, /**
     * 从内存获得
     */ SDImageCacheTypeMemory
};
  • 所有下载的图片都将被写入缓存?磁盘呢?何时缓存的?

磁盘不是强制写入。从枚举SDWebImageOptions可见

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /**
     *  禁用磁盘缓存
     */ SDWebImageCacheMemoryOnly = 1 << 2,
}

而Memory缓存应该是必须写入的(因为我并没找到哪里可以禁止)。
缓存的时间点、有两个(开发者也可以主动缓存)、且都是由SDWebImageManager进行。
其一是下载成功后、自动保存。或者开发者通过代理处理图片并返回后缓存

- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;


=========>>SDWebImageManager //获取转换用户后的图片 UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; //用户处理成功 if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; //用户处理的后若未生成新的图片、则保存下载的二进制文件。 //不然则由imageCache内部生成二进制文件保存 [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}

其二是当缓存中没有、但是从硬盘中查询到了图片。

@autoreleasepool { //搜索硬盘 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage = [self diskImageForKey:key]; //缓存到内存、默认为YES if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //使用NSChache缓存。 [self.memCache setObject:diskImage forKey:key cost:cost];
        } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{
               doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
             });
        }
}
  • 磁盘缓存的时长?清理操作的时间点?

默认为一周
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

能够以时间清除磁盘的方法为

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

调用的时机为

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification object:nil];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification object:nil];

也就是当程序退出到后台、或者被杀死的时候。
这里、还有另外一个点。
Long-Running Task任务

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return;
    } UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)]; //后台任务标识--注册一个后台任务 __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ //超时(大概150秒?)自动结束后台任务 //结束后台任务 [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    
    [self deleteOldFilesWithCompletionBlock:^{ //结束后台任务 [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

正常程序在进入后台后、虽然可以继续执行任务。但是在时间很短内就会被挂起待机。
Long-Running可以让系统为app再多分配一些时间来处理一些耗时任务。

  • 磁盘清理的原则?

首先、通过时间进行清理。(最后修改时间>一周)
然后、根据占据内存大小进行清理。(如果占据内存大于上限、则按时间排序、删除到上限的1/2。)
这里我并没有看到使用频率优先级判断、所以应该是没有。

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { //异步清理超时图片 dispatch_async(self.ioQueue, ^{ //获取磁盘目录 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; //NSURLIsDirectoryKey 判断是否为目录 //NSURLContentModificationDateKey 判断最后修改时间 //NSURLTotalFileAllocatedSizeKey 判断文件大小 NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; //模具器--遍历磁盘路径下的文件 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; //计算一周前(需要释放)、的时间 NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge]; //保存缓存文件Dic NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; //缓存总大小 NSUInteger currentCacheSize = 0; //需要删除的url路径 NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; //遍历磁盘文件枚举器 for (NSURL *fileURL in fileEnumerator) { NSError *error; //获取每个文件所对应的三个参数(resourceKeys) NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error]; // Skip directories and errors. if (error || !resourceValues || [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[fileURL] = resourceValues;
        } //遍历删除文件 for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        } //如果剩余文件大小仍超过阈值 //优先删除最老的文件 if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) { // Target half of our maximum cache size for this cleanup pass. const NSUInteger desiredCacheSize = self.config.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();
            });
        }
    });
}
  • 下载图片时、会使用网络协议缓存逻辑么?

默认情况下不会、由以下代码可见。

NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];

除非将options配置成SDWebImageDownloaderUseNSURLCache、否则每次都会从原地址重新下载、而不是用网络协议的缓存逻辑。

  • 下载图片的URL必须是NSURL么?

不是、在SDWebImageManager中有过容错处理。所以即便你传入一个字符串、依旧可以正确的查找。

if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    } if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

但是由于API暴露出的是(nullable NSURL *)、如果你传入字符串、会有黄色警告


  • 读取缓存以及读取磁盘的时候如何保证线程安全?

  • 读取缓存
    读取缓存的时候是在主线程进行。由于使用NSCache进行存储、所以不需要担心单个value对象的线程安全。

  • 读取磁盘
    磁盘的读取虽然创建了一个NSOperation对象、但据我所见这个对象只是用来标记该操作是否被取消、以及取消之后不再读取磁盘文件的作用。
    真正的磁盘缓存是在另一个IO专属线程中的一个串行队列下进行的。
    如果你搜索self.ioQueue还能发现、不只是读取磁盘内容。
    包括删除、写入等所有磁盘内容都是在这个IO线程进行、以保证线程安全。
    但计算大小、获取文件总数等操作。则是在主线程进行。

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
==========>>>>><<<<<<=========== NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { // do not call the completion if cancelled return;
        } @autoreleasepool { //搜索硬盘 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage = [self diskImageForKey:key]; //缓存到内存、默认为YES if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //使用NSChache缓存。 [self.memCache setObject:diskImage forKey:key cost:cost];
            } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    }); return operation;

相关知识点

如果对一些知识点不了解、可能对代码理解造成困扰。列举一下。

  • NS_OPTIONS枚举与位运算

上文中的SDWebImageOptions便是一个位移枚举

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,
    SDWebImageLowPriority = 1 << 1,
    SDWebImageCacheMemoryOnly = 1 << 2,        
    SDWebImageProgressiveDownload = 1 << 3,
    SDWebImageRefreshCached = 1 << 4,
    SDWebImageContinueInBackground = 1 << 5,
    SDWebImageHandleCookies = 1 << 6,
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    SDWebImageHighPriority = 1 << 8,
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
    SDWebImageAvoidAutoSetImage = 1 << 11,
    SDWebImageScaleDownLargeImages = 1 << 12 };

和我们普通用的枚举

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,
    SDImageCacheTypeDisk,
    SDImageCacheTypeMemory
};

从表面看有两点不同:

  • 枚举声明:NS_ENUM&& NS_OPTIONS
    其实从定义的效果上来讲、二者作用相同。
    更多的是语义化的角度。前者是普通枚举、后者是位移枚举。

  • 枚举中的位运算符号<<.
    位运算中、有三种基本运算符号.

  • 按位与"&"

只有对应的两个二进位均为1时,结果位才为1,否则为0
比如9&5,其实就是1001&0101=0001,因此9&5=1>二进制中,与1相&就保持原位,与0相&就为0

  • 按位或"|"

只要对应的二个二进位有一个为1时,结果位就为1,否则为0。
比如9|5,其实就是1001|0101=1101,因此9|5=13

  • 左移"<<"

把整数a的各二进位全部左移n位,高位丢弃,低位补0。左移n位其实就是乘以2的n次方。
例如1<<2 就是0001左移2为0100,因此1<<2=4

于是、在使用位移枚举的时候、我们就有了这种写法:
options:SDWebImageRetryFailed | SDWebImageCacheMemoryOnly];

上面的意思是。这个操作是如果失败了需要重试、并且只写入缓存。
其中options=SDWebImageRetryFailed | SDWebImageCacheMemoryOnly
也就是0b00000001| 0b00000100 = 0b00000101十进制中 = 5.

在内部判断时候就有了如下写法:
//是否磁盘缓存 BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

等价于0101 & 0100 = 0100结果为真。
倘若

BOOL lowPriority = !(options & SDWebImageLowPriority);

等价于0101 & 0010 = 0000结果为假。

  • 内联函数

在写入缓存时、出现了这样一行代码

NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];

其中SDCacheCostForImage指向一个静态内联函数

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) { #if SD_MAC return image.size.height * image.size.width; #elif SD_UIKIT || SD_WATCH return image.size.height * image.size.width * image.scale * image.scale; #endif }

其中FOUNDATION_STATIC_INLINE作为宏指向static inline、所以也等价于

static __inline__ NSUInteger SDCacheCostForImage(UIImage *image) { #if SD_MAC return image.size.height * image.size.width; #elif SD_UIKIT || SD_WATCH return image.size.height * image.size.width * image.scale * image.scale; #endif }

用宏写方法、我们都用过。但是表达式形式的宏定义有一定的弊端。(比如参数检查、越界等等)。

内联函数完全可以取代表达式形式的宏定义。

顺便谈谈为什么要用内联函数吧。

  • 效率来看

    • 函数之间调用,是内存地址之间的调用、当函数调用完毕之后还会返回原来函数执行的地址。函数调用将会有时间开销。

    • 内联函数在汇编中没有call语句。取消了函数的参数压栈

  • 相比表达式形式的宏定义

    • 需要预编译.因为inline内联函数也是函数、不需要预编译。

    • 调用时候会首先检查它的参数的类型、保证调用正确。

    • 可以使用所在类的保护成员及私有成员。

需要注意的是
  • 内联函数中尽量不要使用诸如循环语句等大量代码、可能会导致编译器放弃内联动作。

  • 内联函数的定义须在调用之前。


准备工作

随手下载了一个最新的 (4.2.3)

GitHub

PODS:
- SDWebImage (4.2.3):
- SDWebImage/Core (= 4.2.3)
- SDWebImage/Core (4.2.3)

DEPENDENCIES:
- SDWebImage

SPEC CHECKSUMS:
SDWebImage: 791bb72962b3492327ddcac4b1880bd1b5458431

PODFILE CHECKSUM: 7fbc0b76fb4d0b0b2afa7d3a90b7bd68dea25abb

COCOAPODS: 1.3.1

工作原理

引用GitHub上一个导图


iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)
  • 1、外部API入口。
    通过UIImageView+WebCache的sd_setImageWithURL方法(等)作为入口来加载图片。

  • 2、内部API汇总。
    通过UIView+WebCache的'sd_internalSetImageWithURL'对UIImageView、UIButton 、MKAnnotationView中图片的下载请求进行汇总。

  • 3、开始加载图片。
    通过SDWebImageManager的loadImageWithURL对图片进行加载。

  • 4、查找本地
    通过SDImageCache的queryCacheOperationForKey查找缓存中是否存在图片。如果不存在再通过diskImageDataBySearchingAllPathsForKey进行磁盘搜索。

  • 5、返回本地图片给SDWebImageManager

  • 6、下载图片
    如果本地查询不到对应图片、则通过SDImageDownloader的downloadImage进行图片下载。

  • 7、下载完毕返回图片给SDWebImageManager

  • 8、由UIView+WebCache通过storeImage将下载图片保存本地

  • 9、返回图片给UIView+WebCache

  • 10、设置图片
    其中。


业务层级

  • 整个架构简单分为三层。

最上层:

负责业务的接入、图片的插入

#import "UIImageView+WebCache.h" #import "UIButton+WebCache.h" #import "UIImageView+HighlightedWebCache.h" //以及其汇总的 #import "UIView+WebCache.h"

逻辑层

负责不同类型业务的分发。
读取(或写入)缓存(或磁盘)、下载等具体逻辑处理。

#import "SDWebImageManager.h"

业务层

负责具体业务的实现

//缓存&&磁盘操作 #import "SDImageCache.h" //下载操作 #import "SDWebImageDownloader.h"

当然、还有其他的工具类。但主要的、就是上面几个。


核心代码(正常读取下载图片)

  • 最上层:UIView+WebCache

所有的代码最终都会汇总到

#import "UIView+WebCache.h" /**
 * @param url            图片地址链接
 * @param placeholder    占位图
 * @param options        下载图片的枚举。包括优先级、是否写入硬盘等
 * @param operationKey   一个记录当前对象正在加载操作的key、保证只有最新的操作在进行、默认为类名。
                         所以如果你想下载多个图片并且都展示一下、可以尝试自定义几个operationKey来操作。(我猜)
 * @param setImageBlock  给开发者自定义set图片的callback
 * @param progressBlock  下载进度callback
 * @param completedBlock 下载完成的callback(sd已经给你set好了、只是会把图片给你罢了)
 * @param context        一些额外的上下文字典。比如你可以搞一个专属的imageManager进来干活。
 */ - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary *)context { //以当前实例的class作为OperationKey NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); //清除当前OperationKey下正在进行的操作。节省无用功 [self sd_cancelImageLoadOperationWithKey:validOperationKey]; //给对象实例绑定imageURLKey = url objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //是否先加载占位图 if (!(options & SDWebImageDelayPlaceholder)) { if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
            dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
            dispatch_group_enter(group);
        } //到主线城更新UI dispatch_main_async_safe(^{ //set 占位图 [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    } if (url) { // 小菊花 if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        } // 允许开发者指定一个manager来进行操作 SDWebImageManager *manager; if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        __weak __typeof(self)wself = self; id  operation = [manager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { //图片下载||读取完成 __strong __typeof (wself) sself = wself; //小菊花 [sself sd_removeActivityIndicator]; if (!sself) { return; } BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); //是否不插入图片 //1、有图片、但是主动配置 //2、没图片、设置了延迟加载占位图 BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{ // if (!sself) { return; } if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                } if (completedBlock && shouldCallCompletedBlock) { //操作完成的回调 completedBlock(image, error, cacheType, url);
                }
            }; // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set // OR // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set if (shouldNotSetImage) { //如果不显示图片、直接回调。 dispatch_main_async_safe(callCompletedBlockClojure); return;
            } /**自动插入图片***/ UIImage *targetImage = nil; NSData *targetData = nil; if (image) { // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) { // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set targetImage = placeholder;
                targetData = nil;
            } if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
                dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
                dispatch_group_enter(group);
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                }); // ensure completion block is called after custom setImage process finish dispatch_group_notify(group, dispatch_get_main_queue(), ^{
                    callCompletedBlockClojure();
                });
            } else {
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    callCompletedBlockClojure();
                });
            }
        }]; //在读取图片之前。向正在进行加载的HashMap中加入当前operation [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

一个简单的流程图


iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)
UIView+WebCache流程图
  • 逻辑层:SDWebImageManager

SDWebImage中最核心的类、调度这图片的下载(SDWebImageDownloader)以及缓存(SDImageCache)。

此外、SDWebImageManager并不依托于UIView+WebCache、完全可以单独使用。
- (id )loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock { NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead"); //所以、我们并不需要在外部把字符串变为NSURL。 if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    } if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    } //下载操作的对象 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; if (url) { @synchronized (self.failedURLs) { //线程安全 isFailedUrl = [self.failedURLs containsObject:url];
        }
    } //url为空 || (未设置失败重试 && 这个url已经失败过) if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { //发出一个获取失败的回调 [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url]; return operation;
    } //将操作添加到正在进行的操作数池 @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    } //默认就是url作为key、也可以自定义mananger的相关block NSString *key = [self cacheKeyForURL:url]; //通过key、查找本地图片 operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { if (operation.isCancelled) { //操作被取消、移除操作池 [self safelyRemoveOperationFromRunning:operation]; return;
        } //本地没有图片 || 刷新缓存 if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { //有本地图片。但需要被刷新 if (cachedImage && options & SDWebImageRefreshCached) { //先回调出去本地图片。再继续下载操作 [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            } //下面是根据调用者传进来的option,来匹配设置了哪些,就给downloaderOptions赋值哪些option 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 (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages; if (cachedImage && 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;
            } //下载图片 SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation; if (!strongOperation || strongOperation.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) {
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url]; if (   error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost && error.code != NSURLErrorNetworkConnectionLost) { @synchronized (self.failedURLs) { //失败记录 [self.failedURLs addObject:url];
                        }
                    }
                } else { if ((options & SDWebImageRetryFailed)) { //失败重新下载 @synchronized (self.failedURLs) { //从失败记录移除 [self.failedURLs removeObject:url];
                        }
                    } //是否磁盘缓存 BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) { //缩放 downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    } if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) { //是否需要转换图片 //成功下载图片、自定义实现了图片处理的代理 } 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]; //用户处理的后若未生成新的图片、则保存下载的二进制文件。 //不然则由imageCache内部生成二进制文件保存 [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            } //回调 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else { //下载成功且未自定义代理--默认保存 if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                } if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }]; @synchronized(operation) {
                operation.cancelBlock = ^{
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            }
        } else if (cachedImage) { //本地有图片--回调、关闭当前操作 __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else { //本地没有、也不下载--回调、关闭当前操作 __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }]; return operation;
}
iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)
SDWebImageManager流程图
  • 业务层:

缓存&&磁盘操作(SDImageCache)

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock { if (!key) { if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        } return nil;
    } // First check the in-memory cache... //搜索磁盘缓存 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { NSData *diskData = nil; if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        } if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        } return nil;
    } NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { // do not call the completion if cancelled return;
        } @autoreleasepool { //搜索硬盘 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage = [self diskImageForKey:key]; //缓存到内存、默认为YES if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //使用NSChache缓存。 [self.memCache setObject:diskImage forKey:key cost:cost];
            } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    }); return operation;
} //查询缓存 - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { //self.memCache  为NSCache实例 return [self.memCache objectForKey:key];
} //查询磁盘 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; if (data) { //图片解码、调整方向 UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data]; //调整图片缩放比例 @2x/@3x image = [self scaledImageForKey:key image:image]; //压缩图片 if (self.config.shouldDecompressImages) {
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
        } return image;
    } else { return nil;
    }
} //写入缓存 && 磁盘 - (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock { if (!image || !key) { if (completionBlock) {
            completionBlock();
        } return;
    } // if memory cache is enabled if (self.config.shouldCacheImagesInMemory) { //写入缓存 NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    } if (toDisk) { //写入磁盘 dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = imageData; if (!data && image) { // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format SDImageFormat format; if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                [self storeImageDataToDisk:data forKey:key];
            } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else { if (completionBlock) {
            completionBlock();
        }
    }
} //正式写入磁盘 - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return;
    }
     
    [self checkIfQueueIsIOQueue]; //如果文件中不存在磁盘缓存路径 则创建 if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    } // get cache Path for image key  得到该key的缓存路径 NSString *cachePathForKey = [self defaultCachePathForKey:key]; // transform to NSUrl  将缓存路径转化为url NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; //将imageData存储起来 [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil]; // disable iCloud backup  如果调用者关闭icloud 关闭iCloud备份 if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

由于此处只归纳正常读取下载流程的代码、所以其余关于图片过期&&释放流程的代码没有列出。后面会逐一进行归纳。


iOS源码补完计划--SDWebImage4.0+源码参阅(附面试题/流程图)
查找本地流程图

下载操作(SDWebImageDownloader)

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self; return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{ //创建下载operation __strong __typeof (wself) sself = wself; //超时时间 NSTimeInterval timeoutInterval = sself.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 //创建下载策略 //SDWebImageDownloaderUseNSURLCache 则使用 NSURLRequestUseProtocolCachePolicy 缓存协议 //默认NSURLRequestReloadIgnoringLocalCacheData从原地址重新下载 NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; //创建下载请求 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES; if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        } else { //默认 image/*;q=0.8 request.allHTTPHeaderFields = sself.HTTPHeaders;
        } //创建下载操作 SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options]; //是否解压 operation.shouldDecompressImages = sself.shouldDecompressImages; //证书 if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) { //默认 账号密码为空的通用证书 operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        } //优先级。默认都不是 if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        } //向下载队列 NSOperationQueue 中 添加本次下载操作 [sself.downloadQueue addOperation:operation]; //设置下载的顺序 是按照队列还是栈 if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically adding new operations as last operation's dependency [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        } return operation;
    }];
} //通过progressBlock&&completedBlock以及Url和SDWebImageDownloaderOperation对token进行包装 - (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))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. if (url == nil) { if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        } return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[url]; if (!operation) {
            operation = createCallback(); //将url作为key、对应的下载操作operation作为value保存。 self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation; if (!soperation) return; if (self.URLOperations[url] == soperation) { //下载完成、移除操作 [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        } //将成progressBlock以及completedBlock组装成SDCallbacksDictionary. id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; //生成下载任务标识。用于manager将来定位对应操作用 token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    }); return token;
}
SDWebImageDownloaderOperation是具体下载操作、设计很多网络层的东西。将来可以单独开一篇、结合AFNetWorking没准会更好。

一些启发

  • 分层的接口API设计。
#import "UIImageView+WebCache.h" #import "UIButton+WebCache.h" #import "UIImageView+HighlightedWebCache.h" //以及其汇总的 #import "UIView+WebCache.h"

所有外层API与具体业务无关。
使得SDWebImageManager可以脱离View层单独运作。

  • 线程安全
@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    } if (url) { @synchronized (self.failedURLs) {
         isFailedUrl = [self.failedURLs containsObject:url];
     }
}
.....

所有可能引起资源抢夺的对象操作、全部有条件锁保护。
但是由于内嵌异常处理代码的存在、条件锁的性能是所有锁中最差的。不知道为什么SD中使用这么多。

  • 内联函数

更高效的短函数执行、替代表达式形式的宏定义。

  • 精细的缓存管理原则

详参上文提到的《磁盘清理的原则?》

  • 回调设计

SDWebImage中使用了两种、Block以及Delegate。

  • Block使用的很多、举两个例子。

======>#import "UIView+WebCache.h" - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary *)context;

======>SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

再来看代理

@protocol SDWebImageManagerDelegate  @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.
 */ - (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

不难看出、SDWebImage对回调的使用倾向于:

  • Block
    单个图片的分类、单个图片的下载。
    每个操作任务中必现的progress以及completed。
    所以、有很强的个体绑定需要或者使用次数不多时、倾向使用block

  • Delegate
    SDWebImageManager下载完成之后的自定义图片处理、是否下载某个url。
    这两个方法如果需要的话都是将会调用多次的。所以、用Delegate更好、可以将方法常驻。

  • 同理
    UITableView的使用Delegate、是用为在滚动途中、代理方法需要被不断的执行。
    UIButton也是将会被多次点击。
    UIView的动画/GCD则可以使用Block、因为只执行一次、用完释放。
    所以、在日常使用中、我们也可以参考上述原则进行设计。

  • NSMapTable

用NSMapTable代替字典来存储当前正在进行的操作、并且将value设置为NSMapTableWeakMemory。防止对应value因为强引用不能自动释放。

作者:kirito_song
链接:https://www.jianshu.com/p/3b8a7ae966d3

正文到此结束
Loading...