Merge pull request #3362 from dreampiggy/behavior_thumbnail_store_cache

Feature: Change thumbnail cache behavior as expected, share cache through different loading pipeline without extra download
This commit is contained in:
DreamPiggy 2022-06-26 15:29:55 +08:00 committed by GitHub
commit d58a1006c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 314 additions and 76 deletions

View File

@ -373,7 +373,11 @@ static NSString * _defaultDiskCacheDirectory;
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
if (diskImage && self.config.shouldCacheImagesInMemory && shouldCacheToMomery) {
if (context[SDWebImageContextImageThumbnailPixelSize]) {
// Query full size cache key which generate a thumbnail, should not write back to full size memory cache
shouldCacheToMomery = NO;
}
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
@ -581,6 +585,10 @@ static NSString * _defaultDiskCacheDirectory;
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
// Query full size cache key which generate a thumbnail, should not write back to full size memory cache
shouldCacheToMomery = NO;
}
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {

View File

@ -10,6 +10,7 @@
#import "SDWebImageCompat.h"
#import "SDWebImageOperation.h"
#import "SDWebImageDefine.h"
#import "SDImageCoder.h"
/// Image Cache Type
typedef NS_ENUM(NSInteger, SDImageCacheType) {
@ -54,6 +55,12 @@ typedef void(^SDImageCacheContainsCompletionBlock)(SDImageCacheType containsCach
*/
FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context);
/// Get the decode options from the loading context options and cache key. This is the built-in translate between the web loading part to the decoding part (which does not depens on).
/// @param context The options arg from the input
/// @param options The context arg from the input
/// @param cacheKey The image cache key from the input. Should not be nil
FOUNDATION_EXPORT SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey);
/**
This is the image cache protocol to provide custom image cache for `SDWebImageManager`.
Though the best practice to custom image cache, is to write your own class which conform `SDMemoryCache` or `SDDiskCache` protocol for `SDImageCache` class (See more on `SDImageCacheConfig.memoryCacheClass & SDImageCacheConfig.diskCacheClass`).

View File

@ -13,8 +13,7 @@
#import "UIImage+Metadata.h"
#import "SDInternalMacros.h"
UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
UIImage *image;
SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) {
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
@ -38,6 +37,17 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
mutableCoderOptions[SDImageCoderWebImageContext] = context;
SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
return coderOptions;
}
UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
NSCParameterAssert(imageData);
NSCParameterAssert(cacheKey);
UIImage *image;
SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
// Grab the image coder
id<SDImageCoder> imageCoder;
if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
@ -79,6 +89,8 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
// assign the decode options, to let manager check whether to re-decode if needed
image.sd_decodeOptions = coderOptions;
}
return image;

View File

@ -311,11 +311,14 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
// Which decode frames in time and reduce memory usage
if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
return animatedImage;
if (imageRep) {
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
animatedImage.sd_imageFormat = self.class.imageFormat;
return animatedImage;
}
}
#endif

View File

@ -13,6 +13,7 @@
#import "SDAnimatedImage.h"
#import "UIImage+Metadata.h"
#import "SDInternalMacros.h"
#import "SDImageCacheDefine.h"
#import "objc/runtime.h"
SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage";
@ -41,28 +42,9 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
} else {
cacheKey = imageURL.absoluteString;
}
SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
NSValue *thumbnailSizeValue;
BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
if (shouldScaleDown) {
CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
CGFloat dimension = ceil(sqrt(thumbnailPixels));
thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
}
SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
mutableCoderOptions[SDImageCoderWebImageContext] = context;
SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
// Grab the image coder
id<SDImageCoder> imageCoder;
@ -106,6 +88,8 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
// assign the decode options, to let manager check whether to re-decode if needed
image.sd_decodeOptions = coderOptions;
}
return image;
@ -204,6 +188,8 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
}
// mark the image as progressive (completed one are not mark as progressive)
image.sd_isIncremental = !finished;
// assign the decode options, to let manager check whether to re-decode if needed
image.sd_decodeOptions = coderOptions;
}
return image;

View File

@ -112,6 +112,26 @@ static id<SDImageLoader> _defaultImageLoader;
return key;
}
- (nullable NSString *)originalCacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
if (!url) {
return @"";
}
NSString *key;
// Cache Key Filter
id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
if (context[SDWebImageContextCacheKeyFilter]) {
cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
}
if (cacheKeyFilter) {
key = [cacheKeyFilter cacheKeyForURL:url];
} else {
key = url.absoluteString;
}
return key;
}
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
if (!url) {
return @"";
@ -278,10 +298,14 @@ static id<SDImageLoader> _defaultImageLoader;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
// Have a chance to query original cache instead of downloading
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
} else if (!cachedImage) {
BOOL mayInOriginalCache = context[SDWebImageContextImageTransformer] || context[SDWebImageContextImageThumbnailPixelSize];
// Have a chance to query original cache instead of downloading, then applying transform
// Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform
if (mayInOriginalCache) {
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}
}
// Continue download process
@ -321,10 +345,8 @@ static id<SDImageLoader> _defaultImageLoader;
// Check whether we should query original cache
BOOL shouldQueryOriginalCache = (originalQueryCacheType != SDImageCacheTypeNone);
if (shouldQueryOriginalCache) {
// Disable transformer for original cache key generation
SDWebImageMutableContext *tempContext = [context mutableCopy];
tempContext[SDWebImageContextImageTransformer] = [NSNull null];
NSString *key = [self cacheKeyForURL:url context:tempContext];
// Get original cache key generation without transformer/thumbnail
NSString *key = [self originalCacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:originalQueryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
@ -333,20 +355,20 @@ static id<SDImageLoader> _defaultImageLoader;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
} else if (!cachedImage) {
// Original image cache miss. Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
return;
}
// Use the store cache process instead of downloading, and ignore .refreshCached option for now
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:cachedImage downloadedData:cachedData finished:YES progress:progressBlock completed:completedBlock];
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:cachedImage downloadedData:cachedData cacheType:cacheType finished:YES completed:completedBlock];
[self safelyRemoveOperationFromRunning:operation];
}];
} else {
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
@ -420,7 +442,7 @@ static id<SDImageLoader> _defaultImageLoader;
SD_UNLOCK(self->_failedURLsLock);
}
// Continue store cache process
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
}
if (finished) {
@ -444,8 +466,8 @@ static id<SDImageLoader> _defaultImageLoader;
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use, choose standalone original cache firstly
id<SDImageCache> imageCache;
@ -459,6 +481,7 @@ static id<SDImageLoader> _defaultImageLoader;
imageCache = self.imageCache;
}
}
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
// the target image store cache type
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
@ -469,10 +492,6 @@ static id<SDImageLoader> _defaultImageLoader;
if (context[SDWebImageContextOriginalStoreCacheType]) {
originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
}
// Disable transformer for original cache key generation
SDWebImageMutableContext *tempContext = [context mutableCopy];
tempContext[SDWebImageContextImageTransformer] = [NSNull null];
NSString *key = [self cacheKeyForURL:url context:tempContext];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
transformer = nil;
@ -482,31 +501,41 @@ static id<SDImageLoader> _defaultImageLoader;
BOOL shouldTransformImage = downloadedImage && transformer;
shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage));
BOOL shouldCacheOriginal = downloadedImage && finished;
BOOL shouldCacheOriginal = downloadedImage && finished && cacheType == SDImageCacheTypeNone;
// if available, store original image to cache
if (shouldCacheOriginal) {
// Get original cache key generation without transformer/thumbnail
NSString *key = [self originalCacheKeyForURL:url context:context];
// normally use the store cache type, but if target image is transformed, use original store cache type instead
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
UIImage *originalImage = downloadedImage;
BOOL thumbnailed = context[SDWebImageContextImageThumbnailPixelSize] != nil;
if (thumbnailed) {
// Thumbnail decoding does not keep original image
// Here we only store the original data to disk for original cache key
// Store thumbnail image to memory for thumbnail cache key later in `storeTransformCacheProcess`
originalImage = nil;
}
if (originalImage && cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
[self storeImage:downloadedImage imageData:cacheData forKey:key imageCache:imageCache cacheType:targetStoreCacheType options:options context:context completion:^{
NSData *cacheData = [cacheSerializer cacheDataWithImage:originalImage originalData:downloadedData imageURL:url];
[self storeImage:originalImage imageData:cacheData forKey:key imageCache:imageCache cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:cacheType finished:finished completed:completedBlock];
}];
}
});
} else {
[self storeImage:downloadedImage imageData:downloadedData forKey:key imageCache:imageCache cacheType:targetStoreCacheType options:options context:context completion:^{
[self storeImage:originalImage imageData:downloadedData forKey:key imageCache:imageCache cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:cacheType finished:finished completed:completedBlock];
}];
}
} else {
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:cacheType finished:finished completed:completedBlock];
}
}
@ -517,39 +546,54 @@ static id<SDImageLoader> _defaultImageLoader;
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
originalData:(nullable NSData *)originalData
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// the target image store cache type
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}
// transformed cache key
NSString *key = [self cacheKeyForURL:url context:context];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
transformer = nil;
}
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// transformer check
BOOL shouldTransformImage = originalImage && transformer;
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
// if available, store transformed image to cache
// thumbnail check
// This exist when previous thumbnail pipeline callback into next full size pipeline, because we share the same URL download but need different image
// Actually this is a hack, we attach the metadata into image object, which should design a better concept like `ImageInfo` and keep that around
// Redecode need the full size data (progressive decoding or third-party loaders may callback nil data)
BOOL shouldRedecodeFullImage = originalData && cacheType == SDImageCacheTypeNone;
if (shouldRedecodeFullImage) {
// If the retuened image decode options exist (some loaders impl does not use `SDImageLoaderDecode`) but does not match the options we provide, redecode
SDImageCoderOptions *returnedDecodeOptions = originalImage.sd_decodeOptions;
if (returnedDecodeOptions) {
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, options, url.absoluteString);
shouldRedecodeFullImage = ![returnedDecodeOptions isEqualToDictionary:decodeOptions];
} else {
shouldRedecodeFullImage = NO;
}
}
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
// transformed/thumbnailed cache key
NSString *key = [self cacheKeyForURL:url context:context];
// Case that transformer one thumbnail, which this time need full pixel image
UIImage *fullSizeImage = originalImage;
if (shouldRedecodeFullImage) {
fullSizeImage = SDImageCacheDecodeImageData(originalData, key, options, context) ?: originalImage;
}
UIImage *transformedImage = [transformer transformedImageWithImage:fullSizeImage forKey:key];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:originalImage];
BOOL imageWasTransformed = ![transformedImage isEqual:fullSizeImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
@ -557,16 +601,71 @@ static id<SDImageLoader> _defaultImageLoader;
} else {
cacheData = (imageWasTransformed ? nil : originalData);
}
[self storeImage:transformedImage imageData:cacheData forKey:key imageCache:imageCache cacheType:storeCacheType options:options context:context completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}];
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:transformedImage data:cacheData cacheType:cacheType transformed:imageWasTransformed finished:finished completed:completedBlock];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:fullSizeImage data:originalData cacheType:cacheType transformed:NO finished:finished completed:completedBlock];
}
}
});
} else if (shouldRedecodeFullImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
// Re-decode because the returned image does not match current request pipeline's context
UIImage *fullSizeImage = SDImageCacheDecodeImageData(originalData, url.absoluteString, options, context) ?: originalImage;
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:fullSizeImage data:originalData cacheType:cacheType transformed:NO finished:finished completed:completedBlock];
}
});
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:originalImage data:originalData cacheType:cacheType transformed:NO finished:finished completed:completedBlock];
}
}
- (void)callStoreTransformCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
image:(nullable UIImage *)image
data:(nullable NSData *)data
cacheType:(SDImageCacheType)cacheType
transformed:(BOOL)transformed
finished:(BOOL)finished
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
// the target image store cache type
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}
// Hack: SDImageCache's queryImage API handle the thumbnail context option (in `SDImageCacheDecodeImageData`)
// but the storeImage does not handle the thumbnail context option
// to keep exist SDImageCache's impl compatible, we introduce this helper
NSData *cacheData = data;
BOOL thumbnailed = context[SDWebImageContextImageThumbnailPixelSize] != nil;
if (thumbnailed) {
// Thumbnail decoding already stored original data before in `storeCacheProcess`
// Here we only store the thumbnail image to memory for thumbnail cache key
cacheData = nil;
}
BOOL shouldCache = transformed || thumbnailed;
if (shouldCache) {
// transformed/thumbnailed cache key
NSString *key = [self cacheKeyForURL:url context:context];
[self storeImage:image imageData:cacheData forKey:key imageCache:imageCache cacheType:storeCacheType waitStoreCache:waitStoreCache completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished url:url];
}];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished url:url];
}
}
@ -586,10 +685,8 @@ static id<SDImageLoader> _defaultImageLoader;
forKey:(nullable NSString *)key
imageCache:(nonnull id<SDImageCache>)imageCache
cacheType:(SDImageCacheType)cacheType
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
waitStoreCache:(BOOL)waitStoreCache
completion:(nullable SDWebImageNoParamsBlock)completion {
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
// Check whether we should wait the store cache finished. If not, callback immediately
[imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
if (waitStoreCache) {

View File

@ -8,6 +8,7 @@
#import "SDWebImageCompat.h"
#import "NSData+ImageContentType.h"
#import "SDImageCoder.h"
/**
UIImage category for image metadata, including animation, loop count, format, incremental, etc.
@ -65,4 +66,12 @@
*/
@property (nonatomic, assign) BOOL sd_isIncremental;
/**
A dictionary value contains the decode options when decoded from SDWebImage loading system (say, `SDImageCacheDecodeImageData/SDImageLoaderDecode[Progressive]ImageData`)
It may not always available and only image decoding related options will be saved. (including [.decodeScaleFactor, .decodeThumbnailPixelSize, .decodePreserveAspectRatio, .decodeFirstFrameOnly])
@note This is used to identify and check the image from downloader when multiple different request (which want different image thumbnail size, image class, etc) share the same URLOperation.
@warning This API exist only because of current SDWebImageDownloader bad design which does not callback the context we call it. There will be refactory in future (API break) and you SHOULD NOT rely on this property at all.
*/
@property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;
@end

View File

@ -186,4 +186,16 @@
return value.boolValue;
}
- (void)setSd_decodeOptions:(SDImageCoderOptions *)sd_decodeOptions {
objc_setAssociatedObject(self, @selector(sd_decodeOptions), sd_decodeOptions, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (SDImageCoderOptions *)sd_decodeOptions {
SDImageCoderOptions *value = objc_getAssociatedObject(self, @selector(sd_decodeOptions));
if ([value isKindOfClass:NSDictionary.class]) {
return value;
}
return nil;
}
@end

View File

@ -18,6 +18,7 @@ void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable
}
// Image Metadata
target.sd_isIncremental = source.sd_isIncremental;
target.sd_decodeOptions = source.sd_decodeOptions;
target.sd_imageLoopCount = source.sd_imageLoopCount;
target.sd_imageFormat = source.sd_imageFormat;
// Force Decode

View File

@ -265,6 +265,7 @@
NSUInteger defaultLimitBytes = SDImageCoderHelper.defaultScaleDownLimitBytes;
SDImageCoderHelper.defaultScaleDownLimitBytes = 1000 * 1000 * 4; // Limit 1000x1000 pixel
// From v5.5.0, the `SDWebImageScaleDownLargeImages` translate to `SDWebImageContextImageThumbnailPixelSize`, and works for progressive loading
[SDImageCache.sharedImageCache removeImageFromDiskForKey:originalImageURL.absoluteString];
[SDWebImageManager.sharedManager loadImageWithURL:originalImageURL options:SDWebImageScaleDownLargeImages | SDWebImageProgressiveLoad progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
expect(image).notTo.beNil();
expect(image.size).equal(CGSizeMake(1000, 1000));
@ -417,6 +418,108 @@
[self waitForExpectationsWithTimeout:kAsyncTestTimeout * 10 handler:nil];
}
- (void)test17ThatThumbnailCacheQueryNotWriteToWrongKey {
// 1. When query thumbnail decoding for SDImageCache, the thumbnailed image should not stored into full size key
XCTestExpectation *expectation = [self expectationWithDescription:@"Thumbnail for cache should not store the wrong key"];
// 500x500
CGSize fullSize = CGSizeMake(500, 500);
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:fullSize];
UIImage *fullSizeImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextFillRect(context, CGRectMake(0, 0, fullSize.width, fullSize.height));
}];
expect(fullSizeImage.size).equal(fullSize);
NSString *fullSizeKey = @"kTestRectangle";
// Disk only
[SDImageCache.sharedImageCache storeImageDataToDisk:fullSizeImage.sd_imageData forKey:fullSizeKey];
CGSize thumbnailSize = CGSizeMake(100, 100);
NSString *thumbnailKey = SDThumbnailedKeyForKey(fullSizeKey, thumbnailSize, YES);
// thumbnail size key miss, full size key hit
[SDImageCache.sharedImageCache queryCacheOperationForKey:fullSizeKey options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(thumbnailSize)} done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
expect(image.size).equal(thumbnailSize);
expect(cacheType).equal(SDImageCacheTypeDisk);
// Currently, thumbnail decoding does not write back to the original key's memory cache
// But this may change in the future once I change the API for `SDImageCacheProtocol`
expect([SDImageCache.sharedImageCache imageFromMemoryCacheForKey:fullSizeKey]).beNil();
expect([SDImageCache.sharedImageCache imageFromMemoryCacheForKey:thumbnailKey]).beNil();
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
- (void)test18ThatThumbnailLoadingCanUseFullSizeCache {
// 2. When using SDWebImageManager to load thumbnail image, it will prefers the full size image and thumbnail decoding on the fly, no network
XCTestExpectation *expectation = [self expectationWithDescription:@"Thumbnail for loading should prefers full size cache when thumbnail cache miss, like Transformer behavior"];
// 500x500
CGSize fullSize = CGSizeMake(500, 500);
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:fullSize];
UIImage *fullSizeImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextFillRect(context, CGRectMake(0, 0, fullSize.width, fullSize.height));
}];
expect(fullSizeImage.size).equal(fullSize);
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/500x500.png"];
NSString *fullSizeKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
[SDImageCache.sharedImageCache storeImageDataToDisk:fullSizeImage.sd_imageData forKey:fullSizeKey];
CGSize thumbnailSize = CGSizeMake(100, 100);
NSString *thumbnailKey = SDThumbnailedKeyForKey(fullSizeKey, thumbnailSize, YES);
[SDImageCache.sharedImageCache removeImageFromDiskForKey:thumbnailKey];
// Load with thumbnail, should use full size cache instead to decode and scale down
[SDWebImageManager.sharedManager loadImageWithURL:url options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(thumbnailSize)} progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
expect(image.size).equal(thumbnailSize);
expect(cacheType).equal(SDImageCacheTypeDisk);
expect(finished).beTruthy();
// The thumbnail one should stored into memory and disk cache with thumbnail key as well
expect([SDImageCache.sharedImageCache imageFromMemoryCacheForKey:thumbnailKey].size).equal(thumbnailSize);
expect([SDImageCache.sharedImageCache imageFromDiskCacheForKey:thumbnailKey].size).equal(thumbnailSize);
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
- (void)test19ThatDifferentThumbnailLoadShouldCallbackDifferentSize {
// 3. Current SDWebImageDownloader use the **URL** as primiary key to bind operation, however, different loading pipeline may ask different image size for same URL, this design does not match
// We use a hack logic to do a re-decode check when the callback image's decode options does not match the loading pipeline provided, it will re-decode the full data with global queue :)
// Ugly unless we re-define the design of SDWebImageDownloader, maybe change that `addHandlersForProgress` with context options args as well. Different context options need different callback image
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/501x501.png"];
NSString *fullSizeKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:fullSizeKey];
for (int i = 490; i < 500; i++) {
// 490x490, ..., 499x499
CGSize thumbnailSize = CGSizeMake(i, i);
NSString *thumbnailKey = SDThumbnailedKeyForKey(fullSizeKey, thumbnailSize, YES);
[SDImageCache.sharedImageCache removeImageFromDiskForKey:thumbnailKey];
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Different thumbnail loading for same URL should callback different image size: (%dx%d)", i, i]];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:url.absoluteString];
__block SDWebImageCombinedOperation *operation;
operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(thumbnailSize)} progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
expect(image.size).equal(thumbnailSize);
expect(cacheType).equal(SDImageCacheTypeNone);
expect(finished).beTruthy();
NSURLRequest *request = ((SDWebImageDownloadToken *)operation.loaderOperation).request;
NSLog(@"thumbnail image size: (%dx%d) loaded with the shared request: %p", i, i, request);
[expectation fulfill];
}];
}
[self waitForExpectationsWithTimeout:kAsyncTestTimeout * 5 handler:nil];
}
- (NSString *)testJPEGPath {
NSBundle *testBundle = [NSBundle bundleForClass:[self class]];
return [testBundle pathForResource:@"TestImage" ofType:@"jpg"];