SDWebImage是一個開源的iOS第三方庫,主要用于下載并緩存網絡圖片,在我的博文iOS網絡資源緩存ZCLURLCache·上篇提到過SDWebImage。它提供了UIImageView
、MKAnnotationView
、UIButton
的categories(分類),支持網絡圖片的加載與緩存,其中UIImageView
應該是使用最廣泛的。
本文從源碼的角度討論SDWebImage的下載和緩存的實現。
在SDWebImage的使用例子中,給UIImageView
設置圖片的代碼是:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:[_objects objectAtIndex:indexPath.row]] placeholderImage:[UIImage imageNamed:@"placeholder"] options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];
可以看出,SDWebImage的使用非常簡單,只用這一行代碼,就可實現網絡圖片的加載與緩存。在這行代碼的背后,會執行下列操作:
UIImageView+WebCache
categories會從管理類SDWebImageManager
找圖片,并刷新UIImageView
。SDWebImageManager
向從緩存類SDImageCache
找URL對應的圖片緩存,如果沒找到,起用SDWebImageDownloader
下載圖片。SDImageCache
會先在內存NSCache
中找圖片,如果內存中沒有,就在磁盤上找,在磁盤上找到了,把圖片放入內存。SDWebImageDownloader
會創建一個SDWebImageDownloaderOperation
操作隊列下載圖片,下載完后緩存在內存和磁盤上(可選)。SDWebImageDownloaderOperation
操作隊列使用NSURLConnection
在后臺發起請求,下載圖片,反饋進度和下載結果。源碼中有幾個關鍵的類,分別是:
SDImageCache
,緩存類,在內存和磁盤上緩存圖片,并對圖片編碼。SDWebImageDownloader
,下載管理類,下載圖片。SDWebImageDownloaderOperation
,下載操作隊列,繼承自NSOperation
,在后臺發起HTTP請求并下載圖片。SDWebImageManager
,管理類,協調緩存類與下載類。SDImageCache
實現了圖片的緩存機制,使用NSCache
作為內存緩存,默認以com.hackemist.SDWebImageCache.default
為磁盤的緩存命名空間,程序運行后,可以在應用程序的文件夾Library/Caches/default/com.hackemist.SDWebImageCache.default
下看到一些緩存文件。當然,也可以指定其它命名空間初始化:
- (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
會在系統發出低內存警告時釋放內存,并且在程序進入UIApplicationWillTerminateNotification
時,清理磁盤緩存,清理磁盤的機制是:
maxCacheAge
修改過期天數。maxCacheSize
,則繼續刪除緩存,優先刪除最老的圖片,可以通過修改maxCacheSize
來改變最大緩存大小。- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock{ if (!doneBlock){ return nil; } if (!key){ doneBlock(nil, SDImageCacheTypeNone); return nil; } // First check the in-memory cache... 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){ NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation;}
傳人的Block定義是:
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
先從內存中取圖片,內存中沒有的時候再從磁盤中取,通過Block返回取到的圖片和獲取圖片的方式,SDImageCacheType
的定義如下:
typedef NS_ENUM(NSInteger, SDImageCacheType){ /** * The image wasn't available the SDWebImage caches, but was downloaded from the web. */ SDImageCacheTypeNone, /** * The image was obtained from the disk cache. */ SDImageCacheTypeDisk, /** * The image was obtained from the memory cache. */ SDImageCacheTypeMemory};
當然,也可能磁盤也沒有緩存,此時doneBlock
中的diskImage
的值時nil
,處理方式doneBlock
將在SDWebImageManager
講到。
- (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 // We need to determine if the image is a PNG or a JPEG // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html) // The first eight bytes of a PNG file always contain the following (decimal) values: // 137 80 78 71 13 10 26 10 // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download) // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency int alphaInfo = CGImageGetAlphaInfo(image.CGImage); BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); BOOL imageIsPng = hasAlpha; // But if we have an image data, we will look at the preffix 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]; } // get cache Path for image key NSString *cachePathForKey = [self defaultCachePathForKey:key]; // transform to NSUrl NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil]; // disable iCloud backup if (self.shouldDisableiCloud){ [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil]; } } }); }}
如果需要存入內存,則先存入內存,toDisk
標識是否存入磁盤,為Yes
的時候,即要存入磁盤,需要先對圖片編碼,再存入磁盤。
SDWebImageDownloader
是下載管理類,是一個單例類,圖片的下載在一個NSOperationQueue
隊列中完成。
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
默認情況下,隊列最多并發數為6,可以通過修改maxConcurrentOperationCount
來改變最多并發數。
- (id)init{ if ((self = [super init])){ ··· _downloadQueue.maxConcurrentOperationCount = 6; ··· } return self;}- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads { _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;}
下載圖片的消息是:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
該方法會創建一個SDWebImageDownloaderOperation
操作隊列來執行下載操作,傳入的兩個Block用于網絡下載的回調,progressBlock
為下載進度回調,completedBlock
為下載完成回調,回調信息存儲在URLCallbacks
中,為保證只有一個線程操作URLCallbacks
,SDWebImageDownloader
把這些操作放入了一個barrierQueue隊列中。
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback{ ··· dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]){ self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download request for the same URL NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; if (first){ createCallback(); } });}
SDWebImageDownloader
還提供了兩種下載任務調度方式(先進先出和后進先出):
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder){ /** * Default value. All download operations will execute in queue style (first-in-first-out). */ SDWebImageDownloaderFIFOExecutionOrder, /** * All download operations will execute in stack style (last-in-first-out). */ SDWebImageDownloaderLIFOExecutionOrder};
通過修改executionOrder
可改變下載方式:
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock{ ··· [wself.downloadQueue addOperation:operation]; 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; } }]; ···}
SDWebImageDownloaderOperation
是下載操作隊列,繼承自NSOperation
,并采用了SDWebImageOperation
協議,該協議只有一個cancel
方法。只暴露了一個方法:
- (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock;
該方法的progressBlock
與completedBlock
Block與SDWebImageDownloader
下載管理類對應。
SDWebImageDownloaderOperation
使用start
和done
來控制狀態,而不是使用main
。圖片的下載使用NSURLConnection
,在協議中接收數據并回調Block通知下載進度和下載完成。
SDWebImageManager
是一個單例管理類,負責協調圖片緩存和圖片下載,是對 SDImageCache
和SDWebImageDownloader
的封裝。
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
在一般的使用中,我們并不直接使用SDImageCache
和SDWebImageDownloader
,而是使用 SDWebImageManager
。SDWebImageManager
的核心方法是:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
代碼有點長,這里就不貼出來了,代碼控制了圖片的緩存和下載的整個流程,同樣兩個Block與前面的也是一一對應。
在Categories
目錄下實現了多個分類,實現方式都是一致的。使用最多的是UIImageView+WebCache
,針對UIImageView
擴展了一些方法。在使用的時候調用的方法是:
objective-c - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
該方法依賴于SDWebImageManager
,從SDWebImageManager
管理類中獲取到圖片并刷新顯示,至于圖片是從緩存中得到的還是從網絡上下載的對UIImageView
是透明的。
其它的categories就不多說了,當然還可以創建自己的分類。
本文是我讀SDWebImage的源代碼的一點理解,主要集中在圖片的下載和緩存,不包括WebP、GIF和圖片編碼的討論。涉及到得技術有:
dispatch_main_sync_safe
、dispatch_async
、dispatch_barrier_sync
,更多請參考并發編程:API 及挑戰本文討論并不完整,更多的東西還靠以后慢慢發掘。
原文鏈接: http://zh.5long.me/2015/source-sdwebImage/
新聞熱點
疑難解答