SDWebImage源碼解析(一)

源碼地址:https://github.com/rs/SDWebImage

版本:3.7

SDWebImage是一個開源的第三方庫,它提供了UIImageView的一個分類,以支持從遠程服務器下載并緩存圖片的功能。它具有以下功能:

  • 提供UIImageView的一個分類,以支持網絡圖片的加載與緩存管理

  • 一個異步的圖片加載器

  • 一個異步的內存+磁盤圖片緩存,并具有自動緩存過期處理功能

  • 支持GIF圖片

  • 支持WebP圖片

  • 后臺圖片解壓縮處理

  • 確保同一個URL的圖片不被下載多次

  • 確保虛假的URL不會被反復加載

  • 確保下載及緩存時,主線程不被阻塞

  • 優良的性能

  • 使用GCD和ARC

  • 支持Arm64

在這個SDWebImage源碼解析的第一篇,我們將先關注它的異步圖片緩存部分。SDWebImage的模塊化非常出色,能獨立的使用包括異步圖片緩存在內的很多模塊。
下面就從類的實例開始,對SDImageCache模塊源碼進行分析,本文源碼解析的順序為自己閱讀順序。

實例

SDImageCache類管理著內存緩存和可選的磁盤緩存,提供了一個方便的單例sharedImageCache。如果你不想使用default緩存空間,而想創建另外的空間,你可以創建你自己的SDImageCache對象來管理緩存。

+ (SDImageCache *)sharedImageCache {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

經典的iOS單例實現,使用dispatch_once防止多線程環境下生成多個實例。

- (id)init {
    return [self initWithNamespace:@"default"];
}

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

- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];

        // 初始化PNG標記數據
        kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];

        // 創建ioQueue串行隊列負責對硬盤的讀寫
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

        // 初始化默認的最大緩存時間
        _maxCacheAge = kDefaultCacheMaxCacheAge;

        // 初始化內存緩存,詳見接下來解析的內存緩存類
        _memCache = [[AutoPurgeCache alloc] init];
        _memCache.name = fullNamespace;

        // 初始化磁盤緩存
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

        // 設置默認解壓縮圖片
        _shouldDecompressImages = YES;

        // 設置默認開啟內存緩存
        _shouldCacheImagesInMemory = YES;

        // 設置默認不使用iCloud
        _shouldDisableiCloud = YES;

        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

#if TARGET_OS_IPHONE
        // app事件注冊,內存警告事件,程序被終止事件,已經進入后臺模式事件,詳見后文的解析:app事件注冊。
        [[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;
}

單例sharedImageCache使用default的命名空間,而我們自己也可以通過使用initWithNamespace:或initWithNamespace:diskCacheDirectory:來創建另外的命名空間。

內存緩存類

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (id)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

}

@end

這就是上面初始化的內存緩存類AutoPurgeCache,使用NSCache派生得到。整個類中只有一個邏輯,就是添加觀察者,在內存警告時,調用NSCache的@selector(removeAllObjects),清空內存緩存。

app事件注冊

app事件注冊使用經典的觀察者模式,當觀察到內存警告、程序被終止、程序進入后臺這些事件時,程序將自動調用相應的方法處理。

內存警告

- (void)clearMemory {
    [self.memCache removeAllObjects];
}

上面是收到UIApplicationDidReceiveMemoryWarningNotification時,調用的@selector(clearMemory),在方法中調用內存緩存類AutoPurgeCache的方法removeAllObject。

程序被終止

- (void)cleanDisk {
    [self cleanDiskWithCompletionBlock:nil];
}

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 使用目錄枚舉器獲取緩存文件的三個重要屬性:(1)URL是否為目錄;(2)內容最后更新日期;(3)文件總的分配大小。
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        
        // 計算過期日期,默認為一星期前的緩存文件認為是過期的。
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 枚舉緩存目錄的所有文件,此循環有兩個目的:
        //
        //  1. 清除超過過期日期的文件。
        //  2. 為以大小為基礎的第二輪清除保存文件屬性。
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            // 跳過目錄.
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 記錄超過過期日期的文件;
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // 保存保留下來的文件的引用并計算文件總的大小。
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        //清除記錄的過期緩存文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果我們保留下來的磁盤緩存文件仍然超過了配置的最大大小,那么進行第二輪以大小為基礎的清除。我們首先刪除最老的文件。前提是我們設置了最大緩存
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 此輪清除的目標是最大緩存的一半。
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // 用它們最后更新時間排序保留下來的緩存文件(最老的最先被清除)。
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // 刪除文件,直到我們達到期望的總的緩存大小。
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

當收到UIApplicationWillTerminateNotification時,SDImageCache將會使用ioQueue異步地清理磁盤緩存。

具體清理邏輯:

  1. 先清除已超過最大緩存時間的緩存文件(最大緩存時間默認為一星期)
  2. 在第一輪清除的過程中保存文件屬性,特別是緩存文件大小
  3. 在第一輪清除后,如果設置了最大緩存并且保留下來的磁盤緩存文件仍然超過了配置的最大緩存,那么進行第二輪以大小為基礎的清除。
  4. 首先刪除最老的文件,直到達到期望的總的緩存大小,即最大緩存的一半。

程序進入后臺

- (void)backgroundCleanDisk {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // 清理任何未完成的任務作業,標記完全停止或結束任務。
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // 開始長時間后臺運行的任務并且立即return。
    [self cleanDiskWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

當收到UIApplicationDidEnterBackgroundNotification時,在手機系統后臺進行如上面描述的異步磁盤緩存清理。這里利用Objective-C的動態語言特性,得到UIApplication的單例sharedApplication,使用sharedApplication開啟后臺任務cleanDiskWithCompletionBlock:。

查詢圖片

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 首先查詢內存緩存...
    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) {
                //將圖片保存到NSCache中,并把圖片像素大小作為該對象的cost值
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

查詢緩存,默認使用方法queryDiskCacheForKey:done:,如果此方法返回nil,則說明緩存中現在還沒有這張照片,因此你需要得到并緩存這張圖片。緩存key是緩存圖片的程序唯一的標識符,一般使用圖片的完整URL。

如果不想SDImageCache查詢磁盤緩存,你可以調用另一個方法:imageFromMemoryCacheForKey:。

返回值為NSOpration,單獨使用SDImageCache沒用,但是使用SDWebImageManager就可以對多個任務的優先級、依賴,并且可以取消。

自定義@autoreleasepool,autoreleasepool代碼段里面有大量的內存消耗操作,自定義autoreleasepool可以及時地釋放掉內存。

//返回緩存完整路徑,其中文件名是根據key值生成的MD5值,具體生成方法見后文解析
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

- (NSString *)defaultCachePathForKey:(NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

//從默認路徑和只讀的bundle路徑中搜索圖片
- (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;
}

- (UIImage *)diskImageForKey:(NSString *)key {
    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;
    }
}

上面代碼段是從磁盤獲取圖片的代碼。得到圖片對應的UIData后,還要經過如下步驟,才能返回對應的圖片:

  1. 根據圖片的不同種類,生成對應的UIImage
  2. 根據key值,調整image的scale值
  3. 如果設置圖片需要解壓縮,則還需對UIImage進行解碼

MD5計算

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

iOS經典的MD5值計算方法,這段代碼大家可以拿去重用。

保存圖片

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;
}

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // if memory cache is enabled
    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
                // 我們需要判斷圖片是PNG還是JPEG格式。PNG圖片很容易檢測,因為它們擁有一個獨特的簽名<http://www.w3.org/TR/PNG-Structure.html>。PNG文件的前八字節經常包含如下(十進制)的數值:137 80 78 71 13 10 26 10
                // 如果imageData為nil(也就是說,如果試圖直接保存一個UIImage或者圖片是由下載轉換得來)并且圖片有alpha通道,我們將認為它是PNG文件以避免丟失透明度信息。
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;

                // 但是如果我們有image data,我們將查詢數據前綴
                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];
                }

                // 獲得對應圖像key的完整緩存路徑
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // 轉換成NSUrl
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // 關閉iCloud備份
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

- (void)storeImage:(UIImage *)image forKey:(NSString *)key {
    [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES];
}

- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
    [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk];
}

存儲一個圖片到緩存中,可以使用方法storeImage:forKey:method:,默認,圖片既會存儲到內存緩存中,也會異步地保存到磁盤緩存中。如果只想使用內存緩存,可以使用另外一個方法storeImage:forKey:toDisk,第三個參數傳入false值就好了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容