最近在做一個(gè)老項(xiàng)目的圖片下載模塊優(yōu)化,希望整體替換成SDWebImage。大體的邏輯分析看來(lái),替換應(yīng)該還是比較好做的,無(wú)非就是異步下載接口全部用SDWebImage,但其中有一項(xiàng)需求卻遇到了困難:
原代碼的邏輯是這樣的:
- 異步發(fā)起下載圖片任務(wù);
- 圖片下到后,不獲取NSData或UIImage,寫(xiě)入本地文件;
- 圖片下載器delegate方法出一個(gè)本地圖片的filePath給發(fā)起任務(wù)對(duì)象;
- 發(fā)起方用這個(gè)filePath來(lái)隨時(shí)使用這張圖。
先不論這種方式的效率如何,要SDWebImage來(lái)適配,首先想到的是使用SDWebImage的Cache機(jī)能,即SDWebCache。于是嘗試以下代碼:
[[SDWebImageManager sharedManager] downloadImageWithURL:url options:options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
NSString *filePath = [[SDWebImageManager sharedManager].imageCache defaultCachePathForKey:[[SDWebImageManager sharedManager] cacheKeyForURL:imageURL]];
//獲得圖片本地文件path
}
}];
結(jié)果發(fā)現(xiàn)地址時(shí)有時(shí)無(wú),一時(shí)不得要領(lǐng)。
于是去看downloadImageWithURL的源碼實(shí)現(xiàn),有以下代碼段:
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
再看storeImage的實(shí)現(xiàn):
- (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
}
[self storeImageDataToDisk:data forKey:key];
});
}
}
這就比較清楚了。SDWebImage在保存圖片到緩存并不是同步的,而是在一個(gè)ioQueue里異步完成。圖片本地文件的讀取,對(duì)SDWebImage來(lái)說(shuō)應(yīng)該是一種補(bǔ)充,而非使用圖片必需存在的前提條件。對(duì)我們這種需求,可以考慮兩種方案:
- 重建一個(gè)緩存,在SDWebImage之上,自己實(shí)現(xiàn)業(yè)務(wù)邏輯。
- 設(shè)法讓SDWebImage的completeBlock在圖片本地文件寫(xiě)完后回調(diào)。
第一種方案等于放棄了SDWebImage的各種優(yōu)勢(shì),相當(dāng)不可取。第二種方式的難點(diǎn)在于,如何在不動(dòng)SDWebImage源碼的情況下,讓storeImageDataToDisk執(zhí)行后再調(diào)completeBlock。于是我們想到了運(yùn)行時(shí):
@implementation SDWebImageManager (DiskCacheEnsured)
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
diskCacheEnsuredCompleted:(SDWebImageCompletionWithFinishedBlock)diskCacheEnsuredCompletedBlock
{
return [self downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (completedBlock){
completedBlock(image, error, cacheType, finished, imageURL);
}
Ivar queueIvar = class_getInstanceVariable([SDImageCache class], "_ioQueue");
dispatch_queue_t _ioQueue = nil;
if (queueIvar){
_ioQueue = object_getIvar([SDImageCache sharedImageCache], queueIvar);
}
//為了防止SDWebImage改了這個(gè)變量名字
if (!_ioQueue){
_ioQueue = dispatch_get_main_queue();
}
@weakify(image)
@weakify(error)
@weakify(imageURL)
dispatch_async(_ioQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(image)
@strongify(error)
@strongify(imageURL)
if (diskCacheEnsuredCompletedBlock){
diskCacheEnsuredCompletedBlock(image, error, cacheType, finished, imageURL);
}
});
});
}];
}
@end
這段代碼的邏輯是把SDImageCache的ioQueue變量引出,先派發(fā)到這個(gè)保存文件的queue,確保保存完后再在主線程操作圖片本地文件。
使用這個(gè)Category非常簡(jiǎn)單:
[[SDWebImageManager sharedManager] downloadImageWithURL:url options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
CGFloat progress = (float)receivedSize*100/(float)expectedSize;
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
//獲得UIImage
}
} diskCacheEnsuredCompleted:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL){
if (image) {
NSString *filePath = [self.imageCache defaultCachePathForKey:[self cacheKeyForURL:imageURL]];
//獲得圖片本地文件path
}
}];
這樣就在不侵入第三方源代碼的情況下滿足了需求。