SDWebImage源码阅读笔记

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 <SDWebImageOperation>)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源码阅读笔记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容