Update the loader protocol to refactor the common image decoding process.

This commit is contained in:
DreamPiggy 2018-04-16 00:23:03 +08:00
parent 57db312cc5
commit 8292c0c1e9
5 changed files with 176 additions and 90 deletions

View File

@ -489,7 +489,11 @@ didReceiveResponse:(NSURLResponse *)response
return YES;
}
- (id<SDWebImageOperation>)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageLoaderProgressBlock)progressBlock completed:(SDWebImageLoaderCompletedBlock)completedBlock context:(SDWebImageContext *)context {
- (id<SDWebImageOperation>)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDWebImageLoaderProgressBlock)progressBlock completed:(SDWebImageLoaderCompletedBlock)completedBlock {
UIImage *cachedImage;
if ([context valueForKey:SDWebImageContextLoaderCachedImage]) {
cachedImage = [context valueForKey:SDWebImageContextLoaderCachedImage];
}
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
@ -500,7 +504,7 @@ didReceiveResponse:(NSURLResponse *)response
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (options & SDWebImageRefreshCached) {
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing

View File

@ -7,12 +7,7 @@
*/
#import "SDWebImageDownloaderOperation.h"
#import "SDWebImageManager.h"
#import "NSImage+Additions.h"
#import "UIImage+WebCache.h"
#import "SDWebImageCodersManager.h"
#import "SDWebImageCoderHelper.h"
#import "SDAnimatedImage.h"
#import "SDWebImageError.h"
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
@ -46,7 +41,6 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (strong, nonatomic, nullable) NSMutableData *imageData;
@property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
@property (copy, nonatomic, nullable) NSString *cacheKey;
@property (assign, nonatomic, readwrite) NSUInteger expectedSize;
@property (strong, nonatomic, nullable, readwrite) NSURLResponse *response;
@ -357,42 +351,11 @@ didReceiveResponse:(NSURLResponse *)response
}
}
}
[self.progressiveCoder updateIncrementalData:imageData finished:finished];
// progressive decode the image in coder queue
dispatch_async(self.coderQueue, ^{
// check whether we should use `SDAnimatedImage`
UIImage *image;
BOOL decodeFirstFrame = self.options & SDWebImageDownloaderDecodeFirstFrameOnly;
NSNumber *scaleValue = [self.context valueForKey:SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(self.cacheKey);
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([self.context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [self.context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [self.progressiveCoder conformsToProtocol:@protocol(SDWebImageAnimatedCoder)]) {
image = [[animatedImageClass alloc] initWithAnimatedCoder:(id<SDWebImageAnimatedCoder>)self.progressiveCoder scale:scale];
}
}
}
if (!image) {
image = [self.progressiveCoder incrementalDecodedImageWithOptions:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDWebImageCoderDecodeScaleFactor : @(scale)}];
}
UIImage *image = SDWebImageLoaderDecodeProgressiveImageData(data, self.request.URL, finished, self.progressiveCoder, [[self class] imageOptionsFromDownloaderOptions:self.options], [[self class] imageContextFromDownloadContext:self.context]);
if (image) {
BOOL shouldDecode = (self.options & SDWebImageDownloaderAvoidDecodeImage) == 0;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
}
// mark the image as progressive (completionBlock one are not mark as progressive)
image.sd_isIncremental = YES;
// We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
@ -456,46 +419,7 @@ didReceiveResponse:(NSURLResponse *)response
} else {
// decode the image in coder queue
dispatch_async(self.coderQueue, ^{
BOOL decodeFirstFrame = self.options & SDWebImageDownloaderDecodeFirstFrameOnly;
NSNumber *scaleValue = [self.context valueForKey:SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(self.cacheKey);
if (scale < 1) {
scale = 1;
}
UIImage *image;
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([self.context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [self.context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
image = [[animatedImageClass alloc] initWithData:imageData scale:scale];
if (self.options & SDWebImageDownloaderPreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
}
}
}
if (!image) {
image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:imageData options:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDWebImageCoderDecodeScaleFactor : @(scale)}];
}
BOOL shouldDecode = (self.options & SDWebImageDownloaderAvoidDecodeImage) == 0;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
if (shouldScaleDown) {
image = [SDWebImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
} else {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
}
}
UIImage *image = SDWebImageLoaderDecodeImageData(imageData, self.request.URL, nil, [[self class] imageOptionsFromDownloaderOptions:self.options], [[self class] imageContextFromDownloadContext:self.context]);
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
@ -546,11 +470,22 @@ didReceiveResponse:(NSURLResponse *)response
}
#pragma mark Helper methods
- (NSString *)cacheKey {
if (!_cacheKey) {
_cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
+ (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions {
SDWebImageOptions options = 0;
if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly;
if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames;
return options;
}
+ (SDWebImageContext *)imageContextFromDownloadContext:(SDWebImageContext *)downloadContext {
SDWebImageContext *context = nil;
if (!downloadContext) {
return context;
}
return _cacheKey;
return context;
}
- (BOOL)shouldContinueWhenAppEntersBackground {

View File

@ -14,19 +14,50 @@ typedef void(^SDWebImageLoaderProgressBlock)(NSInteger receivedSize, NSInteger e
typedef void(^SDWebImageLoaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished);
typedef void(^SDWebImageLoaderDataCompletedBlock)(NSData * _Nullable data, NSError * _Nullable error, BOOL finished);
@protocol SDWebImageCoder, SDWebImageProgressiveCoder;
/**
A `SDImageCacheType` value to specify the cache type information from manager. `SDWebImageManager` will firstly query cache, then if cache miss or `SDWebImageRefreshCached` is set, it will start image loader to load the image.
This can be a hint for image loader to load the image from network and refresh the image from remote location if needed. (NSNumber)
This is the built-in decoding process for image download from network or local file.
@note If you want to implement your custom loader with `loadImageWithURL:options:context:progress:completed:` API, but also want to keep compatible with SDWebImage's behavior, you'd better use this to produce image.
@param imageData The image data from the network. Should not be nil
@param imageURL The image URL from the input. Should not be nil
@param coder The image coder. You can pass nil to use the default `SDWebImageCodersManager`. See `SDWebImageCoder`
@param options The options arg from the input
@param context The context arg from the input
@return The decoded image for current image data load from the network
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextLoaderCacheType;
FOUNDATION_EXPORT UIImage * _Nullable SDWebImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, id<SDWebImageCoder> _Nullable coder, SDWebImageOptions options, SDWebImageContext * _Nullable context);
/**
This is the built-in decoding process for image progressive download from network. It's used when `SDWebImageProgressiveDownload` option is set.
@note If you want to implement your custom loader with `loadImageWithURL:options:context:progress:completed:` API, but also want to keep compatible with SDWebImage's behavior, you'd better use this to produce image.
@param imageData The image data from the network so far. Should not be nil
@param imageURL The image URL from the input. Should not be nil
@param finished Pass NO to specify the download process has not finished. Pass YES when all image data has finished.
@param progressiveCoder The image progressive coder. Should not be nil. You should bind the progressive coder for each of loading operation to avoid conflict. See `SDWebImageProgressiveCoder`.
@param options The options arg from the input
@param context The context arg from the input
@return The decoded progressive image for current image data load from the network
*/
FOUNDATION_EXPORT UIImage * _Nullable SDWebImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished, id<SDWebImageProgressiveCoder> _Nonnull progressiveCoder, SDWebImageOptions options, SDWebImageContext * _Nullable context);
/**
A `UIImage` instance from `SDWebImageManager` when you specify `SDWebImageRefreshCached` and image cache hit.
This can be a hint for image loader to load the image from network and refresh the image from remote location if needed. If the cached image is equal to the remote location one. you should call the completion with all nil args. (UIImage)
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextLoaderCachedImage;
#pragma mark - SDWebImageLoader
@protocol SDWebImageLoader <NSObject>
- (BOOL)canLoadWithURL:(nullable NSURL *)url;
// We provide two ways to allow a image loader to load the image.
// The first one should return the `UIImage` image instance as well as `NSData` image data. This is suitable for the use case such as progressive download from network, or image directlly from Photos framework.
// The second one should return just the `NSData` image data, we will use the common image decoding logic to process the correct image instance, so the image loader itself can concentrate on only data retriving. This is suitable for the use case such as load the data from file.
// The first one should return the `UIImage` image instance as well as `NSData` image data. This is suitable for the use case such as image from third-party SDKs, such as image directlly from Photos framework.
// The second one should return just the `NSData` image data, we will use the common image decoding logic to process the correct image instance, so the image loader itself can concentrate on only data retriving. This is suitable for the use case such as load the data from network or local file.
// Your image loader **MUST** implement at least one of those protocol, or an assert will occur. We will firstlly ask for `loadImageWithURL:options:progress:completed:context:` if you implement it. If this one return nil, we will continue to ask for `loadImageDataWithURL:options:progress:completed:context:` if you implement it.
// @note It's your responsibility to load the image in the desired global queue(to avoid block main queue). We do not dispatch these method call in a global queue but just from the call queue (For `SDWebImageManager`, it typically call from the main queue).

View File

@ -7,3 +7,109 @@
*/
#import "SDWebImageLoader.h"
#import "SDWebImageCodersManager.h"
#import "SDWebImageCoderHelper.h"
#import "SDAnimatedImage.h"
#import "UIImage+WebCache.h"
UIImage * _Nullable SDWebImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, id<SDWebImageCoder> _Nullable coder, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
NSCParameterAssert(imageData);
NSCParameterAssert(imageURL);
UIImage *image;
NSString *cacheKey = imageURL.absoluteString;
BOOL decodeFirstFrame = options & SDWebImageDecodeFirstFrameOnly;
NSNumber *scaleValue = [context valueForKey:SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
if (scale < 1) {
scale = 1;
}
if (!coder) {
coder = [SDWebImageCodersManager sharedManager];
}
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
image = [[animatedImageClass alloc] initWithData:imageData scale:scale];
if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
}
}
}
if (!image) {
image = [coder decodedImageWithData:imageData options:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDWebImageCoderDecodeScaleFactor : @(scale)}];
}
if (image) {
BOOL shouldDecode = YES;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
BOOL shouldScaleDown = options & SDWebImageScaleDownLargeImages;
if (shouldScaleDown) {
image = [SDWebImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
} else {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
}
}
}
return image;
}
UIImage * _Nullable SDWebImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished, id<SDWebImageProgressiveCoder> _Nonnull progressiveCoder, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
NSCParameterAssert(imageData);
NSCParameterAssert(imageURL);
NSCParameterAssert(progressiveCoder);
UIImage *image;
NSString *cacheKey = imageURL.absoluteString;
BOOL decodeFirstFrame = options & SDWebImageDecodeFirstFrameOnly;
NSNumber *scaleValue = [context valueForKey:SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
if (scale < 1) {
scale = 1;
}
[progressiveCoder updateIncrementalData:imageData finished:finished];
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder conformsToProtocol:@protocol(SDWebImageAnimatedCoder)]) {
image = [[animatedImageClass alloc] initWithAnimatedCoder:(id<SDWebImageAnimatedCoder>)progressiveCoder scale:scale];
}
}
}
if (!image) {
image = [progressiveCoder incrementalDecodedImageWithOptions:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDWebImageCoderDecodeScaleFactor : @(scale)}];
}
if (image) {
BOOL shouldDecode = YES;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
}
// mark the image as progressive (completionBlock one are not mark as progressive)
image.sd_isIncremental = YES;
}
return image;
}
SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage";

View File

@ -196,15 +196,25 @@ static SDWebImageDownloader *_defaultImageDownloader;
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
SDWebImageContext *downloadContext = context;
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
SDWebImageMutableContext *mutableContext;
if (downloadContext) {
mutableContext = [downloadContext mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
[mutableContext setValue:cachedImage forKey:SDWebImageContextLoaderCachedImage];
downloadContext = [mutableContext copy];
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadOperation = [self.imageDownloader loadImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
strongOperation.downloadOperation = [self.imageDownloader loadImageWithURL:url options:options context:downloadContext progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// Do nothing if the operation was cancelled