SDWebImage目录
- SDWebImage
SDWebImageCompat
SDWebImageOperation
-
Downloader
- SDWebImageDownloader
- SDWebImageDownloaderOperation
-
Cache
- SDImageCache
-
Utils
- SDWebImageManager
- SDWebImageDecoder
- SDWebImagePrefetcher
-
Categories
- MKAnnotationView+WebCache
- NSData+ImageContentType
- UIButton+WebCache
- UIImage+GIF
- UIImage+MultiFormat
- UIImage+WebP
- UIImageView+HighlightedWebCache
- UIImageView+WebCache
- UIView+WebCacheOperation
SDWebImage各个类的功能
- SDWebImageCompat
定义了一系列宏定义以及一个图片处理方法:
extern UIImage *SDScaledImageForKey(NSString *key, UIImage *image);
- SDWebImageOperation
图片操作协议,只有一个方法:
- (void)cancel;
- SDWebImageDownloader
负责下载图片的请求队列处理的类,其中包含了下载队列downloadQueue的封装,队列中图片下载SDWebImageDownloaderOperation的封装,保存网络请求block回调的URLCallbacks字典的封装(其中每个url对应一个数组,数组中包含了下载过程block回调和下载完成block回调)。
其initialize方法中的代码:
+ (void)initialize {
// Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator )
// To use it, just add #import "SDNetworkActivityIndicator.h" in addition to the SDWebImage import
if (NSClassFromString(@"SDNetworkActivityIndicator")) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#pragma clang diagnostic pop
// Remove observer in case it was previously added.
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator selector:NSSelectorFromString(@"startActivity")
name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"stopActivity")
name:SDWebImageDownloadStopNotification object:nil];
}
}
这里有两个知识点:initialize方法与#pragma clang diagnostic强制去掉警告的用法。
1.其中+initialize方法是在向类或者其子类发送第一条消息之前调用,且只调用一次。父类在子类之前接受这个消息。如果子类没有实现initialize方法(runtime会调用继承来的实现或者子类明确地调用[super initialize]),父类的实现可能会被调用多次。如果你想防止调用多次,可以在实现中加判断:
+(void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}
因为initialize方法是一种线程安全方式中调用的,并且不同的类中initialize方法调用的顺序是不能保证的。所以在initialize方法中必须尽量少做一些工作。
一个类,initialize方法只会调用一次,如果你想对类和类的扩展进行独立的初始化,你应该使用load方法。
与其相似的+(void)load方法是在类或者其分类在被动态加载或静态链接到时,并且只在类或者其分类中实现了可以响应的方法时才会调用一次。
类的+load方法会在父类的+load方法调用之后才调用。
类的分类的+load方法会在类本身的+load方法调用之后才调用。
2.#pragma 声明主要由 Xcode 用来完成两个主要任务:整理代码和防止编译器警告。
在项目中将build settings下的Treat Warnings as Errors选项设置为YES,将开启Xcode困难模式。
#pragma clang diagnostic push/pop用于保存和恢复编译器的状态,类似 Core Graphics 或 OpenGL 上下文。
这里为了判断项目中是否引入了SDNetworkActivityIndicator第三方库,引入了就使用。如果项目中加入了SDNetworkActivityIndicator只需要在SDWebImage中引入头文件即可,非常灵活。这里用到了#pragma clang diagnostic ignored "-Warc-performSelector-leaks” 来避免编译器报错‘performSelector may cause a leak because its selector is unknown’。
参考:
Which Clang Warning Is Generating This Message?
Clang Diagnostics
#pragma
其init方法中的代码:
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_URLCallbacks = [NSMutableDictionary new];
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
}
return self;
}
这里是默认配置。由此可见,图片下载器SDWebImageDownloader默认是会解压缩图片的,默认是按照队列FIFO下载的,默认最大并发下载数量是6,默认下载超时时间是15s,默认图片下载处理类是SDWebImageDownloaderOperation(是NSOperation的一个子类)。
再看下其中的最主要两个方法:
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
该方法对SDWebImageDownloaderOperation进行了封装然后加入到下载队列downloadQueue中并把封装好的SDWebImageDownloaderOperation返回给调用者。默认是先加入的先下载,如果设置成了先加入的后下载,这里使用以下代码将请求顺序颠倒:
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;
}
查看文档发现,NSOperation的addDependency:方法调用了之后,前者会在后者执行完了才执行,这里用的很巧妙。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(NSURL *)url
createCallback:(SDWebImageNoParamsBlock)createCallback
这个方法主要是将url作为key,包含下载过程block回调和下载完成block回调的数组作为value,加入到URLCallbacks字典中。然后每新加入一个url,就调用一次creatCallBack回调,将对应的SDWebImageDownloaderOperation加入到请求队列中。
- SDWebImageDownloaderOperation
负责图片下载的具体操作的类,继承自NSOperation。包含一个NSURLRequest对象,负责发起图片地址请求。遵守NSURLConnectionDataDelegate协议,在NSURLConnection请求回调方法中对数据进行处理,并调用相关的block回调。
其中start方法中有段代码如下:
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;
}
}];
}
这里使用到了UIApplication很少用到的方法:
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^)(void))handler
在应用程序保持在后台悬挂的时间即将到0之前调用的处理程序,应该在这个方法中做一些清理工作并且将后台任务标志位结束。后台任务无法结束将导致APP被终止。这个处理程序在主线程中同步调用,当APP收到通知时会暂时阻止APP被悬挂起来。这个方法将返回一个UIBackgroundTaskInvalid,需要在结束后台任务的方法endBackgroundTask:中传入。UIApplication
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier
在后台任务结束后,调用这个方法,总是和上面的方法成对出现。如果你自己不调用,系统可能会将APP强制退出。该方法可以在主线程中安全地调用。
另外UIApplication还有一个属性backgroundTimeRemaining。这个属性是指当APP在被系统强制退出之前可以在后台运行的时间。当程序在前台运行时,这个值很大。当APP使用方法beginBackgroundTaskWithExpirationHandler:在后台开始一个或多个长期运行的任务时,这个属性反映了APP还有多长时间可以运行。
参考:
iOS 7 的多任务
iOS开发:保持程序在后台长时间运行
- SDImageCache
负责对图片进行缓存的类。SDWebImage库是一个异步将图片保存到内存和硬盘的库,并且具有图片过期的自动处理。这个类就是负责将图片缓存到内存和硬盘,并且将过期的图片进行删除的工作的。
它提供了存储图片、查询图片、删除图片、清理内存/硬盘、获取硬盘占用大小、获取图片缓存路径等一系列方法。
其初始化方法:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// initialise PNG signature data
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// Init default values
_maxCacheAge = kDefaultCacheMaxCacheAge;
// Init the memory cache
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// Set decompression to YES
_shouldDecompressImages = YES;
// memory cache enabled
_shouldCacheImagesInMemory = YES;
// Disable iCloud
_shouldDisableiCloud = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
由此可见,SDImageCache默认图片缓存时间为一周,超过一周将被清理。当收到内存警告的时候,会清理内存中加载的图片;当APP要退出的时候,清理硬盘上缓存的超过一周时间的图片。图片缓存地址是Library/Caches/com.hackemist.SDWebImageCache.default目录下。图片文件名默认是URL的absoluteString经过MD5加密后的字符串。默认不开启iCloud备份功能。
将图片缓存到内存中使用的是NSCache的一个子类AutoPurgeCache来实现的,它也对内存警告进行了监听,当收到内存警告时,会清空内存中所有缓存的图片。
将图片缓存到内存中的方法实现:
// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
计算缓存图片占用大小的方法:
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}
将图片缓存到硬盘中是用NSFileManager对文件进行读写移除操作的。
不开启iCloud备份功能的代码:
// disable iCloud backup
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
在程序即将退出时将触发cleanDisk方法,在这个方法中,将首先移除修改时间超过一周的图片,然后再判断当前缓存大小是否超过了最大缓存限制,如果超过了,则按修改时间倒序进行删除一部分图片,直到缓存大小不超过最大缓存限制的一半大小。核心代码如下,主要是NSURL的一些不常用的属性操作:
- (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();
});
}
});
}
- SDWebImageManager
SDWebImageManager是将SDWebImageDownloader与SDImageCache绑定的一个类,可以直接使用该类下载一个图片并进行缓存。
- SDWebImageDecoder
负责图片解码的类。
- SDWebImagePrefetcher
负责图片预先加载的类。
- MKAnnotationView+WebCache
MKAnnotationView类的扩展,负责加载其image。内部调用SDWebImageManager的下载和缓存方法。
- NSData+ImageContentType
提供根据图片的NSData来判断图片格式的方法。
- UIButton+WebCache
提供一系列异步加载UIButton的image或backgroundImage的方法。
- UIImage+GIF
封装了gif图片的一些处理方法。
UIImage+MultiFormat 不同格式的图片处理
UIImage+WebP Webp格式的图片处理
UIImageView+HighlightedWebCache
提供一系列异步加载UIImageView的highlightedImage的方法。
- UIImageView+WebCache
提供一系列异步加载UIImageView的image的方法。
- UIView+WebCacheOperation
UIView的扩展,像UIButton有image和backgroundImage的加载,UIImageView有image与highlightedImage的加载,当同时加载它们时,需要区分不同的加载处理。在这个UIView扩展中用一个字典保存了每个操作和它对应的key。
总结
SDWebImage库中SDWebImageCompat定义了这个库中通用的一些宏定义。SDWebImageOperation协议实现了提供匿名对象的效果,隐藏了具体实现类的细节。 SDWebImageDownloaderOperation负责具体图片的下载,负责发起网络请求并处理得到的数据,调用Block回调。SDWebImageDownloader负责将一批SDWebImageDownloaderOperation加入到队列中进行管理。SDImageCache负责具体的图片加载到内存以及对硬盘的读写,对过期文件的处理等。SDWebImageManager则负责将SDWebImageDownloader与SDImageCache绑定到一起,是进一步的抽象类。UIButton和UIImageView在异步加载图片时,直接使用的是SDWebImageManager的。我们在使用时,也可以直接调用SDWebImageManager的相关方法即可。
原文地址:SDWebImage源码阅读笔记