SDWebImageCache管理著SDWebImage的快取,其中記憶體快取採用NSCache,同時會建立一個ioQueue負責對硬碟的讀寫,並且會新增觀察者,在收到記憶體警告、關閉或進入後臺時完成對應的處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (id)init { _memCache = [[NSCache alloc] init]; _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); //收到記憶體警告時,清除NSCache:[self.memCache removeAllObjects]; [[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]; } |
查詢圖片
每次向SDWebImageCache索取圖片的時候,會先根據圖片URL對應的key值先檢查記憶體中是否有對應的圖片,如果有則直接返回;如果沒有則在ioQueue中去硬碟中查詢,其中檔名是是根據URL生成的MD5值,找到之後先將圖片快取在記憶體中,然後在把圖片返回:
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 |
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { /*...*/ // 首先查詢記憶體快取 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } //硬碟查詢 NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ //建立自動釋放池,記憶體及時釋放 @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale; //快取到NSCache中 [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation; } |
在硬碟查詢的時候,會在後臺將NSData轉成UIImage,並完成相關的解碼工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (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; } } |
儲存圖片
當下載完圖片後,會先將圖片儲存到NSCache中,並把圖片畫素大小作為該物件的cost值,同時如果需要儲存到硬碟,會先判斷圖片的格式,PNG或者JPEG,並儲存對應的NSData到快取路徑中,檔名為URL的MD5值:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (NSString *)cachedFileNameForKey:(NSString *)key { //根據key生成對應的MD5值作為檔名 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]]; return filename; } |
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 |
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { //儲存到NSCache,cost為畫素值 [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale]; if (toDisk) { dispatch_async(self.ioQueue, ^{ NSData *data = imageData; if (image && (recalculate || !data)) { //判斷圖片格式 BOOL imageIsPng = YES; // 檢視imagedata的字首是否是PNG的字首格式 if ([imageData length] >= [kPNGSignatureData length]) { imageIsPng = ImageDataHasPNGPreffix(imageData); } if (imageIsPng) { data = UIImagePNGRepresentation(image); } else { data = UIImageJPEGRepresentation(image, (CGFloat)1.0); } } if (data) { if (![_fileManager fileExistsAtPath:_diskCachePath]) { [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; } //儲存data到指定的路徑中 [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil]; } }); } } |
硬碟檔案的管理
在程式退出或者進入後臺時,會出圖片檔案進行管理,具體的策略:
- 清除過期的檔案,預設一星期
- 如果設定了最大快取,並且當前快取的檔案超過了這個限制,則刪除最舊的檔案,直到當前快取檔案的大小為最大快取大小的一半
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 |
- (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 |
總結
- 介面設計簡單
通常我們使用較多的UIImageView分類:
12[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]placeholderImage:[UIImage imageNamed:@"placeholder"]];
一個簡單的介面將其中複雜的實現細節全部隱藏:簡單就是美。 - 採用NSCache作為記憶體緩
- 耗時較長的請求,都採用非同步形式,在回撥函式塊中處理請求結果
- NSOperation和NSOperationQueue:可以取消任務處理佇列中的任務,設定最大併發數,設定operation之間的依賴關係。
- 圖片快取清理的策略
- dispatch_barrier_sync:前面的任務執行結束後它才執行,而且它後面的任務要等它執行完成之後才會執行。
- 使用weak self strong self 防止retain circle
- 如果子執行緒進需要不斷處理一些事件,那麼設定一個Run Loop是最好的處理方式