Merge pull request #3423 from dreampiggy/feature/refactor_thumbnail_hack_with_decode_options

Refactor the hack for multiple thumbnail image request at the same time
This commit is contained in:
DreamPiggy 2022-11-01 00:14:17 +08:00 committed by GitHub
commit 5ee6ac2476
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 452 additions and 248 deletions

View File

@ -409,7 +409,16 @@ static NSString * _defaultDiskCacheDirectory;
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
// Query full size cache key which generate a thumbnail, should not write back to full size memory cache
shouldCacheToMomery = NO;
}
@ -626,7 +635,16 @@ static NSString * _defaultDiskCacheDirectory;
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
// Query full size cache key which generate a thumbnail, should not write back to full size memory cache
shouldCacheToMomery = NO;
}

View File

@ -55,12 +55,18 @@ 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
/// 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 depends on).
/// @param context The context arg from the input
/// @param options The options 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);
/// Set the decode options to the loading context options. This is the built-in translate between the web loading part from the decoding part (which does not depends on).
/// @param mutableContext The context arg to override
/// @param mutableOptions The options arg to override
/// @param decodeOptions The image decoding options
FOUNDATION_EXPORT void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions);
/**
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,6 +13,8 @@
#import "UIImage+Metadata.h"
#import "SDInternalMacros.h"
#import <CoreServices/CoreServices.h>
SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) {
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
@ -29,7 +31,11 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
}
NSString *typeIdentifierHint = context[SDWebImageContextImageTypeIdentifierHint];
NSString *fileExtensionHint = cacheKey.pathExtension; // without dot
NSString *fileExtensionHint;
if (!typeIdentifierHint) {
// UTI has high priority
fileExtensionHint = cacheKey.pathExtension; // without dot
}
// First check if user provided decode options
SDImageCoderMutableOptions *mutableCoderOptions;
@ -50,6 +56,27 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
return [mutableCoderOptions copy];
}
void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions) {
if ([decodeOptions[SDImageCoderDecodeFirstFrameOnly] boolValue]) {
*mutableOptions |= SDWebImageDecodeFirstFrameOnly;
} else {
*mutableOptions &= ~SDWebImageDecodeFirstFrameOnly;
}
mutableContext[SDWebImageContextImageScaleFactor] = decodeOptions[SDImageCoderDecodeScaleFactor];
mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
if (!typeIdentifierHint) {
NSString *fileExtensionHint = decodeOptions[SDImageCoderDecodeFileExtensionHint];
if (fileExtensionHint) {
typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, NULL);
}
}
mutableContext[SDWebImageContextImageTypeIdentifierHint] = typeIdentifierHint;
}
UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
NSCParameterAssert(imageData);
NSCParameterAssert(cacheKey);

View File

@ -19,7 +19,7 @@ typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull i
/// Provide the image data associated to the image and store to disk cache
/// @param image The loaded image
/// @param data The original loaded image data
/// @param data The original loaded image data. May be nil when image is transformed (UIImage.sd_isTransformed = YES)
/// @param imageURL The image URL
- (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL;

View File

@ -241,6 +241,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageC
/**
A id<SDImageTransformer> instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. If you pass NSNull, the transformer feature will be disabled. (id<SDImageTransformer>)
@note When this value is used, we will trigger image transform after downloaded, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTransformer;
@ -269,6 +270,7 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP
A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size.
@note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both.
Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue)
@note When this value is used, we will trigger thumbnail decoding for url, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize;

View File

@ -10,6 +10,8 @@
#import "SDWebImageDownloaderConfig.h"
#import "SDWebImageDownloaderOperation.h"
#import "SDWebImageError.h"
#import "SDWebImageCacheKeyFilter.h"
#import "SDImageCacheDefine.h"
#import "SDInternalMacros.h"
NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
@ -206,6 +208,15 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
SD_LOCK(_operationsLock);
id downloadOperationCancelToken;
// When different thumbnail size download with same url, we need to make sure each callback called with desired size
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:url];
} else {
cacheKey = url.absoluteString;
}
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
if (!operation || operation.isFinished || operation.isCancelled) {
@ -228,9 +239,9 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self->_operationsLock);
};
self.URLOperations[url] = operation;
[self.URLOperations setObject:operation forKey:url];
// Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
[self.downloadQueue addOperation:operation];
@ -238,7 +249,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
// When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
}
if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
@ -260,6 +271,18 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
return token;
}
#pragma mark Helper methods
+ (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions {
SDWebImageOptions options = 0;
if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly;
if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames;
if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage;
if (downloadOptions & SDWebImageDownloaderMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass;
return options;
}
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {

View File

@ -29,6 +29,10 @@
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
- (BOOL)cancel:(nullable id)token;
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
@ -160,6 +164,21 @@
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
/**
* Adds handlers for progress and completion, and optional decode options (which need another image other than the initial one). Returns a token that can be passed to -cancel: to cancel this set of
* callbacks.
*
* @param progressBlock the block executed when a new chunk of data arrives.
* @note the progress block is executed on a background queue
* @param completedBlock the block executed when the download is done.
* @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue
* @param decodeOptions The optional decode options, used when in thumbnail decoding for current completion block callback. For example, request <url1, {thumbnail: 100x100}> and then <url1, {thumbnail: 200x200}>, we may callback these two completion block with different size.
* @return the token to use to cancel this set of handlers
*/
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
/**
* Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
*

View File

@ -11,15 +11,40 @@
#import "SDInternalMacros.h"
#import "SDWebImageDownloaderResponseModifier.h"
#import "SDWebImageDownloaderDecryptor.h"
#import "SDImageCacheDefine.h"
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";
// A handler to represent individual request
@interface SDWebImageDownloaderOperationToken : NSObject
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (nonatomic, copy, nullable) SDWebImageDownloaderCompletedBlock completedBlock;
@property (nonatomic, copy, nullable) SDWebImageDownloaderProgressBlock progressBlock;
@property (nonatomic, copy, nullable) SDImageCoderOptions *decodeOptions;
@end
@implementation SDWebImageDownloaderOperationToken
- (BOOL)isEqual:(id)other {
if (nil == other) {
return NO;
}
if (self == other) {
return YES;
}
if (![other isKindOfClass:[self class]]) {
return NO;
}
SDWebImageDownloaderOperationToken *object = (SDWebImageDownloaderOperationToken *)other;
// warn: only compare decodeOptions, ignore pointer, use `removeObjectIdenticalTo`
BOOL result = [self.decodeOptions isEqualToDictionary:object.decodeOptions];
return result;
}
@end
@interface SDWebImageDownloaderOperation ()
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageDownloaderOperationToken *> *callbackTokens;
@property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
@property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@ -48,6 +73,8 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding
@property (strong, nonatomic, nonnull) NSMapTable<SDImageCoderOptions *, UIImage *> *imageMap; // each variant of image is weak-referenced to avoid too many re-decode during downloading
#if SD_UIKIT
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif
@ -75,7 +102,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
_request = [request copy];
_options = options;
_context = [context copy];
_callbackBlocks = [NSMutableArray new];
_callbackTokens = [NSMutableArray new];
_responseModifier = context[SDWebImageContextDownloadResponseModifier];
_decryptor = context[SDWebImageContextDownloadDecryptor];
_executing = NO;
@ -84,6 +111,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
_unownedSession = session;
_coderQueue = [NSOperationQueue new];
_coderQueue.maxConcurrentOperationCount = 1;
_imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
#if SD_UIKIT
_backgroundTaskId = UIBackgroundTaskInvalid;
#endif
@ -93,33 +121,31 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
@synchronized (self) {
[self.callbackBlocks addObject:callbacks];
}
return callbacks;
return [self addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:nil];
}
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
NSMutableArray<id> *callbacks;
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
decodeOptions:(nullable SDImageCoderOptions *)decodeOptions {
if (!completedBlock && !progressBlock && !decodeOptions) return nil;
SDWebImageDownloaderOperationToken *token = [SDWebImageDownloaderOperationToken new];
token.completedBlock = completedBlock;
token.progressBlock = progressBlock;
token.decodeOptions = decodeOptions;
@synchronized (self) {
callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
[self.callbackTokens addObject:token];
}
// We need to remove [NSNull null] because there might not always be a progress block for each callback
[callbacks removeObjectIdenticalTo:[NSNull null]];
return [callbacks copy]; // strip mutability here
return token;
}
- (BOOL)cancel:(nullable id)token {
if (!token) return NO;
if (![token isKindOfClass:SDWebImageDownloaderOperationToken.class]) return NO;
BOOL shouldCancel = NO;
@synchronized (self) {
NSMutableArray *tempCallbackBlocks = [self.callbackBlocks mutableCopy];
[tempCallbackBlocks removeObjectIdenticalTo:token];
if (tempCallbackBlocks.count == 0) {
NSArray *tokens = self.callbackTokens;
if (tokens.count == 1 && [tokens indexOfObjectIdenticalTo:token] != NSNotFound) {
shouldCancel = YES;
}
}
@ -129,9 +155,9 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
} else {
// Only callback this token's completion block
@synchronized (self) {
[self.callbackBlocks removeObjectIdenticalTo:token];
[self.callbackTokens removeObjectIdenticalTo:token];
}
SDWebImageDownloaderCompletedBlock completedBlock = [token valueForKey:kCompletedCallbackKey];
SDWebImageDownloaderCompletedBlock completedBlock = ((SDWebImageDownloaderOperationToken *)token).completedBlock;
dispatch_main_async_safe(^{
if (completedBlock) {
completedBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}], YES);
@ -218,8 +244,11 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
NSArray<SDWebImageDownloaderOperationToken *> *tokens = [self.callbackTokens copy];
for (SDWebImageDownloaderOperationToken *token in tokens) {
if (token.progressBlock) {
token.progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
}
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
@ -275,7 +304,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
- (void)reset {
@synchronized (self) {
[self.callbackBlocks removeAllObjects];
[self.callbackTokens removeAllObjects];
self.dataTask = nil;
if (self.ownedSession) {
@ -374,8 +403,14 @@ didReceiveResponse:(NSURLResponse *)response
}
if (valid) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@synchronized (self) {
tokens = [self.callbackTokens copy];
}
for (SDWebImageDownloaderOperationToken *token in tokens) {
if (token.progressBlock) {
token.progressBlock(0, expected, self.request.URL);
}
}
} else {
// Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
@ -398,10 +433,16 @@ didReceiveResponse:(NSURLResponse *)response
[self.imageData appendData:data];
self.receivedSize = self.imageData.length;
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@synchronized (self) {
tokens = [self.callbackTokens copy];
}
if (self.expectedSize == 0) {
// Unknown expectedSize, immediately call progressBlock and return
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
for (SDWebImageDownloaderOperationToken *token in tokens) {
if (token.progressBlock) {
token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}
}
return;
}
@ -420,6 +461,8 @@ didReceiveResponse:(NSURLResponse *)response
// Using data decryptor will disable the progressive decoding, since there are no support for progressive decrypt
BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
// When multiple thumbnail decoding use different size, this progressive decoding will cause issue because each callback assume called with different size's image, can not share the same decoding part
// We currently only pick the first thumbnail size, see #3423 talks
// Progressive decoding Only decode partial image, full image in `URLSession:task:didCompleteWithError:`
if (supportProgressive && !finished) {
// Get the image data
@ -444,8 +487,10 @@ didReceiveResponse:(NSURLResponse *)response
}
}
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
for (SDWebImageDownloaderOperationToken *token in tokens) {
if (token.progressBlock) {
token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}
}
}
@ -471,7 +516,9 @@ didReceiveResponse:(NSURLResponse *)response
// If we already cancel the operation or anything mark the operation finished, don't callback twice
if (self.isFinished) return;
@synchronized(self) {
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@synchronized (self) {
tokens = [self.callbackTokens copy];
self.dataTask = nil;
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
@ -491,7 +538,7 @@ didReceiveResponse:(NSURLResponse *)response
[self callCompletionBlocksWithError:error];
[self done];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
if (tokens.count > 0) {
NSData *imageData = self.imageData;
self.imageData = nil;
// data decryptor
@ -514,28 +561,64 @@ didReceiveResponse:(NSURLResponse *)response
// decode the image in coder queue, cancel all previous decoding process
[self.coderQueue cancelAllOperations];
@weakify(self);
[self.coderQueue addOperationWithBlock:^{
@strongify(self);
if (!self) {
return;
}
// check if we already use progressive decoding, use that to produce faster decoding
id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
UIImage *image;
if (progressiveCoder) {
image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
} else {
image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}];
for (SDWebImageDownloaderOperationToken *token in tokens) {
[self.coderQueue addOperationWithBlock:^{
@strongify(self);
if (!self) {
return;
}
UIImage *image;
// check if we already decode this variant of image for current callback
if (token.decodeOptions) {
image = [self.imageMap objectForKey:token.decodeOptions];
}
if (!image) {
// check if we already use progressive decoding, use that to produce faster decoding
id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
SDWebImageOptions options = [[self class] imageOptionsFromDownloaderOptions:self.options];
SDWebImageContext *context;
if (token.decodeOptions) {
SDWebImageMutableContext *mutableContext = [NSMutableDictionary dictionaryWithDictionary:self.context];
SDSetDecodeOptionsToContext(mutableContext, &options, token.decodeOptions);
context = [mutableContext copy];
} else {
context = self.context;
}
if (progressiveCoder) {
image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, options, context);
} else {
image = SDImageLoaderDecodeImageData(imageData, self.request.URL, options, context);
}
if (image && token.decodeOptions) {
[self.imageMap setObject:image forKey:token.decodeOptions];
}
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
} else {
[self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
}
}];
}
if (@available(iOS 13.0, *)) {
[self.coderQueue addBarrierBlock:^{
@strongify(self);
if (!self) {
return;
}
[self done];
}];
} else {
dispatch_barrier_async(self.coderQueue.underlyingQueue, ^{
@strongify(self);
if (!self) {
return;
}
[self done];
});
}
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
@ -604,13 +687,30 @@ didReceiveResponse:(NSURLResponse *)response
}
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@synchronized (self) {
tokens = [self.callbackTokens copy];
}
for (SDWebImageDownloaderOperationToken *token in tokens) {
dispatch_main_async_safe(^{
if (token.completedBlock) {
token.completedBlock(image, imageData, error, finished);
}
});
}
}
- (void)callCompletionBlockWithToken:(nonnull SDWebImageDownloaderOperationToken *)token
image:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
if (token.completedBlock) {
token.completedBlock(image, imageData, error, finished);
}
});
}

View File

@ -228,7 +228,19 @@ static id<SDImageLoader> _defaultImageLoader;
// Preprocess the options and context arg to decide the final the result for manager
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// Start the entry to load image from cache
// Start the entry to load image from cache, the longest steps are below
// Steps without transformer:
// 1. query image from cache, miss
// 2. download data and image
// 3. store image to cache
// Steps with transformer:
// 1. query transformed image from cache, miss
// 2. query original image from cache, miss
// 3. download data and image
// 4. do transform in CPU
// 5. store original image to cache
// 6. store transformed image to cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
@ -289,6 +301,7 @@ static id<SDImageLoader> _defaultImageLoader;
// Check whether we should query cache
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// transformed cache key
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@ -299,7 +312,8 @@ static id<SDImageLoader> _defaultImageLoader;
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (!cachedImage) {
BOOL mayInOriginalCache = context[SDWebImageContextImageTransformer] || context[SDWebImageContextImageThumbnailPixelSize];
NSString *originKey = [self originalCacheKeyForURL:url context:context];
BOOL mayInOriginalCache = ![key isEqualToString:originKey];
// 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) {
@ -307,7 +321,6 @@ static id<SDImageLoader> _defaultImageLoader;
return;
}
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
@ -345,7 +358,7 @@ static id<SDImageLoader> _defaultImageLoader;
// Check whether we should query original cache
BOOL shouldQueryOriginalCache = (originalQueryCacheType != SDImageCacheTypeNone);
if (shouldQueryOriginalCache) {
// Get original cache key generation without transformer/thumbnail
// Get original cache key generation without transformer
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) {
@ -361,8 +374,8 @@ static id<SDImageLoader> _defaultImageLoader;
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 cacheType:cacheType finished:YES completed:completedBlock];
// Skip downloading and continue transform process, and ignore .refreshCached option for now
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:cachedImage originalData:cachedData cacheType:cacheType finished:YES completed:completedBlock];
[self safelyRemoveOperationFromRunning:operation];
}];
@ -446,8 +459,8 @@ static id<SDImageLoader> _defaultImageLoader;
[self.failedURLs removeObject:url];
SD_UNLOCK(self->_failedURLsLock);
}
// Continue store cache process
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
}
if (finished) {
@ -464,89 +477,6 @@ static id<SDImageLoader> _defaultImageLoader;
}
}
// Store cache process
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use, choose standalone original cache firstly
id<SDImageCache> imageCache;
if ([context[SDWebImageContextOriginalImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextOriginalImageCache];
} else {
// if no standalone cache available, use default cache
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];
}
// the original store image cache type
SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk;
if (context[SDWebImageContextOriginalStoreCacheType]) {
originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
}
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
transformer = nil;
}
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// transformer check
BOOL shouldTransformImage = downloadedImage && transformer;
shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage));
// thumbnail check
BOOL shouldThumbnailImage = context[SDWebImageContextImageThumbnailPixelSize] != nil || downloadedImage.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize] != nil;
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 || shouldThumbnailImage) ? originalStoreCacheType : storeCacheType;
UIImage *fullSizeImage = downloadedImage;
if (shouldThumbnailImage) {
// 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`
fullSizeImage = nil;
}
if (fullSizeImage && cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = [cacheSerializer cacheDataWithImage:fullSizeImage originalData:downloadedData imageURL:url];
[self storeImage:fullSizeImage 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 cacheType:cacheType finished:finished completed:completedBlock];
}];
}
});
} else {
[self storeImage:fullSizeImage 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 cacheType:cacheType finished:finished completed:completedBlock];
}];
}
} else {
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:cacheType finished:finished completed:completedBlock];
}
}
// Transform process
- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
@ -561,88 +491,112 @@ static id<SDImageLoader> _defaultImageLoader;
if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
transformer = nil;
}
// transformer check
BOOL shouldTransformImage = originalImage && transformer;
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
// 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;
}
CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = originalImage.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
BOOL shouldEncodeThumbnail = thumbnailSize.width > 0 && thumbnailSize.height > 0;
NSData *cacheData = originalData;
UIImage *cacheImage = originalImage;
if (shouldEncodeThumbnail) {
cacheData = nil; // thumbnail don't store full size data
originalImage = nil; // thumbnail don't have full size image
}
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
// transformed/thumbnailed cache key
// transformed cache key
NSString *key = [self cacheKeyForURL:url context:context];
// Case that transformer one thumbnail, which this time need full pixel image
UIImage *fullSizeImage = originalImage;
BOOL imageWasRedecoded = NO;
if (shouldRedecodeFullImage) {
fullSizeImage = SDImageCacheDecodeImageData(originalData, key, options, context);
if (fullSizeImage) {
imageWasRedecoded = YES;
} else {
imageWasRedecoded = NO;
fullSizeImage = originalImage; // Fallback
}
}
UIImage *transformedImage = [transformer transformedImageWithImage:fullSizeImage forKey:key];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:fullSizeImage];
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:transformedImage data:originalData cacheType:cacheType finished:finished transformed:imageWasTransformed || imageWasRedecoded completed:completedBlock];
// Case that transformer on thumbnail, which this time need full pixel image
UIImage *transformedImage = [transformer transformedImageWithImage:cacheImage forKey:key];
if (transformedImage) {
transformedImage.sd_isTransformed = YES;
[self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:transformedImage originalData:originalData cacheData:nil cacheType:cacheType finished:finished completed:completedBlock];
} else {
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:fullSizeImage data:originalData cacheType:cacheType finished:finished transformed:imageWasRedecoded completed:completedBlock];
[self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType 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);
BOOL imageWasRedecoded = NO;
if (fullSizeImage) {
imageWasRedecoded = YES;
} else {
imageWasRedecoded = NO;
fullSizeImage = originalImage; // Fallback
}
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:fullSizeImage data:originalData cacheType:cacheType finished:finished transformed:imageWasRedecoded completed:completedBlock];
}
});
} else {
// Continue store transform cache process
[self callStoreTransformCacheProcessForOperation:operation url:url options:options context:context image:originalImage data:originalData cacheType:cacheType finished:finished transformed:NO completed:completedBlock];
[self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType 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
finished:(BOOL)finished
transformed:(BOOL)transformed
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Store origin cache process
- (void)callStoreOriginCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
cacheImage:(nullable UIImage *)cacheImage
originalData:(nullable NSData *)originalData
cacheData:(nullable NSData *)cacheData
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use, choose standalone original cache firstly
id<SDImageCache> imageCache;
if ([context[SDWebImageContextOriginalImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextOriginalImageCache];
} else {
// if no standalone cache available, use default cache
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
}
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
// the original store image cache type
SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk;
if (context[SDWebImageContextOriginalStoreCacheType]) {
originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
}
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// Get original cache key generation without transformer
NSString *key = [self originalCacheKeyForURL:url context:context];
if (finished && cacheSerializer && (originalStoreCacheType == SDImageCacheTypeDisk || originalStoreCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *newOriginalData = [cacheSerializer cacheDataWithImage:originalImage originalData:originalData imageURL:url];
// Store original image and data
[self storeImage:originalImage imageData:newOriginalData forKey:key imageCache:imageCache cacheType:originalStoreCacheType finished:finished waitStoreCache:waitStoreCache completion:^{
// Continue store cache process, transformed data is nil
[self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
}];
}
});
} else {
// Store original image and data
[self storeImage:originalImage imageData:originalData forKey:key imageCache:imageCache cacheType:originalStoreCacheType finished:finished waitStoreCache:waitStoreCache completion:^{
// Continue store cache process, transformed data is nil
[self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
}];
}
}
// Store normal cache process
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
image:(nullable UIImage *)image
data:(nullable NSData *)data
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
@ -657,25 +611,22 @@ static id<SDImageLoader> _defaultImageLoader;
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// thumbnail check
BOOL shouldThumbnailImage = context[SDWebImageContextImageThumbnailPixelSize] != nil || image.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize] != nil;
// Store the transformed/thumbnail image into the cache
if (image && (transformed || shouldThumbnailImage)) {
NSData *cacheData;
// pass nil if the image was transformed/thumbnailed, so we can recalculate the data from the image
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
cacheData = [cacheSerializer cacheDataWithImage:image originalData:nil imageURL:url];
} else {
cacheData = nil;
}
// 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:^{
// transformed cache key
NSString *key = [self cacheKeyForURL:url context:context];
if (finished && cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *newData = [cacheSerializer cacheDataWithImage:image originalData:data imageURL:url];
// Store image and data
[self storeImage:image imageData:newData forKey:key imageCache:imageCache cacheType:storeCacheType finished:finished waitStoreCache:waitStoreCache completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished url:url];
}];
});
} else {
// Store image and data
[self storeImage:image imageData:data forKey:key imageCache:imageCache cacheType:storeCacheType finished:finished 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];
}
}
@ -695,8 +646,16 @@ static id<SDImageLoader> _defaultImageLoader;
forKey:(nullable NSString *)key
imageCache:(nonnull id<SDImageCache>)imageCache
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
waitStoreCache:(BOOL)waitStoreCache
completion:(nullable SDWebImageNoParamsBlock)completion {
// Ignore progressive data cache
if (!finished) {
if (completion) {
completion();
}
return;
}
// 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

@ -66,11 +66,17 @@
*/
@property (nonatomic, assign) BOOL sd_isIncremental;
/**
A bool value indicating that the image is transformed from original image, so the image data may not always match original download one.
*/
@property (nonatomic, assign) BOOL sd_isTransformed;
/**
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.
@note This is used to identify and check the image is from thumbnail decoding, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
@warning You should not store object inside which keep strong reference to image itself, which will cause retain cycle.
@warning This API exist only because of current SDWebImageDownloader bad design which does not callback the context we call it. There will be refactor in future (API break), use with caution.
*/
@property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;

View File

@ -184,6 +184,15 @@
return value.boolValue;
}
- (void)setSd_isTransformed:(BOOL)sd_isTransformed {
objc_setAssociatedObject(self, @selector(sd_isTransformed), @(sd_isTransformed), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)sd_isTransformed {
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isTransformed));
return value.boolValue;
}
- (void)setSd_decodeOptions:(SDImageCoderOptions *)sd_decodeOptions {
objc_setAssociatedObject(self, @selector(sd_decodeOptions), sd_decodeOptions, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

View File

@ -768,6 +768,30 @@
}];
}
- (void)test30ThatDifferentThumbnailLoadShouldCallbackDifferentSize {
// We move the logic into SDWebImageDownloaderOperation, which decode each callback's thumbnail size with different decoding pipeline, and callback independently
// Note the progressiveLoad does not support this and always callback first size
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];
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:url options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(thumbnailSize)} progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
expect(image.size).equal(thumbnailSize);
[expectation fulfill];
}];
}
[self waitForExpectationsWithTimeout:kAsyncTestTimeout * 5 handler:nil];
}
#pragma mark - SDWebImageLoader
- (void)test30CustomImageLoaderWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom image not works"];

View File

@ -389,6 +389,7 @@
[SDWebImageManager.sharedManager loadImageWithURL:url options:SDWebImageFromCacheOnly context:@{SDWebImageContextImageTransformer : transformer, SDWebImageContextOriginalQueryCacheType : @(SDImageCacheTypeAll)} progress:nil completed:^(UIImage * _Nullable image2, NSData * _Nullable data2, NSError * _Nullable error2, SDImageCacheType cacheType2, BOOL finished2, NSURL * _Nullable imageURL2) {
// Get the transformed image
expect(image2).equal(transformer.testImage);
expect(data).beNil(); // Currently, the thumbnailed and transformed image always data is nil, to avoid confuse user (the image and data represent no longer match)
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:originalKey];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:originalKey];
[expectation fulfill];
@ -417,6 +418,7 @@
SDWebImageContextStoreCacheType: @(SDImageCacheTypeMemory)} progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
// Get the transformed image
expect(image).equal(transformer.testImage);
expect(data).beNil(); // Currently, the thumbnailed and transformed image always data is nil, to avoid confuse user (the image and data represent no longer match)
// Now, the original image is stored into originalCache
UIImage *originalImage = [originalCache imageFromMemoryCacheForKey:originalKey];
expect(originalImage.size).equal(CGSizeMake(103, 103));
@ -487,7 +489,8 @@
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];
NSData *fullSizeData = fullSizeImage.sd_imageData;
[SDImageCache.sharedImageCache storeImageDataToDisk:fullSizeData forKey:fullSizeKey];
CGSize thumbnailSize = CGSizeMake(100, 100);
NSString *thumbnailKey = SDThumbnailedKeyForKey(fullSizeKey, thumbnailSize, YES);
@ -495,6 +498,7 @@
// 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(data).beNil(); // Currently, the thumbnailed and transformed image always data is nil, to avoid confuse user (the image and data represent no longer match)
expect(cacheType).equal(SDImageCacheTypeDisk);
expect(finished).beTruthy();
@ -510,8 +514,8 @@
- (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
// We move the logic into SDWebImageDownloaderOperation, which decode each callback's thumbnail size with different decoding pipeline, and callback independently
// Note the progressiveLoad does not support this and always callback first size
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/501x501.png"];
NSString *fullSizeKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
@ -526,6 +530,7 @@
__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(data).beNil(); // Currently, the thumbnailed and transformed image always data is nil, to avoid confuse user (the image and data represent no longer match)
expect(cacheType).equal(SDImageCacheTypeNone);
expect(finished).beTruthy();

View File

@ -67,6 +67,12 @@
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
return [self addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:nil];
}
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
decodeOptions:(nullable SDImageCoderOptions *)decodeOptions {
if (completedBlock) {
[self.completedBlocks addObject:completedBlock];
}