最近在做一个老项目的图片下载模块优化,希望整体替换成SDWebImage。大体的逻辑分析看来,替换应该还是比较好做的,无非就是异步下载接口全部用SDWebImage,但其中有一项需求却遇到了困难:
原代码的逻辑是这样的:
- 异步发起下载图片任务;
- 图片下到后,不获取NSData或UIImage,写入本地文件;
- 图片下载器delegate方法出一个本地图片的filePath给发起任务对象;
- 发起方用这个filePath来随时使用这张图。
先不论这种方式的效率如何,要SDWebImage来适配,首先想到的是使用SDWebImage的Cache机能,即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
}
}];
结果发现地址时有时无,一时不得要领。
于是去看downloadImageWithURL的源码实现,有以下代码段:
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的实现:
- (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在保存图片到缓存并不是同步的,而是在一个ioQueue里异步完成。图片本地文件的读取,对SDWebImage来说应该是一种补充,而非使用图片必需存在的前提条件。对我们这种需求,可以考虑两种方案:
- 重建一个缓存,在SDWebImage之上,自己实现业务逻辑。
- 设法让SDWebImage的completeBlock在图片本地文件写完后回调。
第一种方案等于放弃了SDWebImage的各种优势,相当不可取。第二种方式的难点在于,如何在不动SDWebImage源码的情况下,让storeImageDataToDisk执行后再调completeBlock。于是我们想到了运行时:
@implementation SDWebImageManager (DiskCacheEnsured)
- (id )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改了这个变量名字
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变量引出,先派发到这个保存文件的queue,确保保存完后再在主线程操作图片本地文件。
使用这个Category非常简单:
[[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
}
}];
这样就在不侵入第三方源代码的情况下满足了需求。