Merge branch '5.x' into replace-valueforkey

This commit is contained in:
zhongwuzw 2018-08-09 15:41:29 +08:00
commit 65f72743cc
33 changed files with 92389 additions and 22896 deletions

View File

@ -1,3 +1,16 @@
## [5.0.0-beta2 - Customizable SDWebImage, on Jul 31st, 2018](https://github.com/rs/SDWebImage/releases/tag/5.0.0-beta2)
See [all tickets marked for the 5.0.0 release](https://github.com/rs/SDWebImage/milestone/15)
#### Features
- Add `SDImageCoderWebImageContext` coder option, which allow custom coder plugin, to receive the context option from top-level API #2405
- Updated all existing diagrams for 5.0 release + added new ones (small detailed diagrams for the most important components) #2407
#### Fixes
- Fix nullable key for `sd_imageLoadOperationForKey` #2389
- Replace `__bridge_transfer` with `__bridge` when convert from `CFStringRef` to `NSString` #2392
- Rename `sd_UTTypeFromSDImageFormat` to `sd_UTTypeFromImageFormat` #2395
- Change `SDImageFormat` to use `NS_TYPED_EXTENSIBLE_ENUM` instead of fixed enum, to allow custom coder plugins to extend it #2400
## [4.4.2 - 4.4 patch, on July 18th, 2018](https://github.com/rs/SDWebImage/releases/tag/4.4.2) ## [4.4.2 - 4.4 patch, on July 18th, 2018](https://github.com/rs/SDWebImage/releases/tag/4.4.2)
See [all tickets marked for the 4.4.2 release](https://github.com/rs/SDWebImage/milestone/27) See [all tickets marked for the 4.4.2 release](https://github.com/rs/SDWebImage/milestone/27)

92026
Docs/Diagrams/SDWebImage.mdj Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

View File

@ -178,10 +178,28 @@ All source code is licensed under the [MIT License](https://raw.github.com/rs/SD
## Architecture ## Architecture
#### High Level Diagram
<p align="center" > <p align="center" >
<img src="Docs/SDWebImageClassDiagram.png" title="SDWebImage class diagram"> <img src="Docs/Diagrams/SDWebImageHighLevelDiagram.jpeg" title="SDWebImage high level diagram">
</p> </p>
#### Overall Class Diagram
<p align="center" > <p align="center" >
<img src="Docs/SDWebImageSequenceDiagram.png" title="SDWebImage sequence diagram"> <img src="Docs/Diagrams/SDWebImageClassDiagram.png" title="SDWebImage overall class diagram">
</p> </p>
#### Top Level API Diagram
<p align="center" >
<img src="Docs/Diagrams/SDWebImageTopLevelClassDiagram.png" title="SDWebImage top level API diagram">
</p>
#### Main Sequence Diagram
<p align="center" >
<img src="Docs/Diagrams/SDWebImageSequenceDiagram.png" title="SDWebImage sequence diagram">
</p>
#### More detailed diagrams
- [Manager API Diagram](Docs/Diagrams/SDWebImageManagerClassDiagram.png)
- [Coders API Diagram](Docs/Diagrams/SDWebImageCodersClassDiagram.png)
- [Loader API Diagram](Docs/Diagrams/SDWebImageLoaderClassDiagram.png)
- [Cache API Diagram](Docs/Diagrams/SDWebImageCacheClassDiagram.png)

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'SDWebImage' s.name = 'SDWebImage'
s.version = '5.0.0-beta' s.version = '5.0.0-beta2'
s.osx.deployment_target = '10.10' s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0' s.ios.deployment_target = '8.0'

View File

@ -10,15 +10,18 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "SDWebImageCompat.h" #import "SDWebImageCompat.h"
typedef NS_ENUM(NSInteger, SDImageFormat) { /**
SDImageFormatUndefined = -1, You can use switch case like normal enum. It's also recommended to add a default case. You should not assume anything about the raw value.
SDImageFormatJPEG = 0, For custom coder plugin, it can also extern the enum for supported format. See `SDImageCoder` for more detailed information.
SDImageFormatPNG, */
SDImageFormatGIF, typedef NSInteger SDImageFormat NS_TYPED_EXTENSIBLE_ENUM;
SDImageFormatTIFF, static const SDImageFormat SDImageFormatUndefined = -1;
SDImageFormatWebP, static const SDImageFormat SDImageFormatJPEG = 0;
SDImageFormatHEIC static const SDImageFormat SDImageFormatPNG = 1;
}; static const SDImageFormat SDImageFormatGIF = 2;
static const SDImageFormat SDImageFormatTIFF = 3;
static const SDImageFormat SDImageFormatWebP = 4;
static const SDImageFormat SDImageFormatHEIC = 5;
@interface NSData (ImageContentType) @interface NSData (ImageContentType)
@ -37,7 +40,7 @@ typedef NS_ENUM(NSInteger, SDImageFormat) {
* @param format Format as SDImageFormat * @param format Format as SDImageFormat
* @return The UTType as CFStringRef * @return The UTType as CFStringRef
*/ */
+ (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format CF_RETURNS_NOT_RETAINED; + (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format CF_RETURNS_NOT_RETAINED NS_SWIFT_NAME(sd_UTType(from:));
/** /**
* Convert UTTyppe to SDImageFormat * Convert UTTyppe to SDImageFormat

View File

@ -104,4 +104,15 @@
- (nonnull instancetype)init NS_UNAVAILABLE; - (nonnull instancetype)init NS_UNAVAILABLE;
/**
Move the cache directory from old location to new location, the old location will be removed after finish.
If the old location does not exist, does nothing.
If the new location does not exist, only do a movement of directory.
If the new location does exist, will move and merge the files from old location.
@param srcPath old location of cache directory
@param dstPath new location of cache directory
*/
- (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath;
@end @end

View File

@ -228,6 +228,39 @@
return [path stringByAppendingPathComponent:filename]; return [path stringByAppendingPathComponent:filename];
} }
- (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath {
NSParameterAssert(srcPath);
NSParameterAssert(dstPath);
// Check if old path is equal to new path
if ([srcPath isEqualToString:dstPath]) {
return;
}
BOOL isDirectory;
// Check if old path is directory
if (![self.fileManager fileExistsAtPath:srcPath isDirectory:&isDirectory] || !isDirectory) {
return;
}
// Check if new path is directory
if (![self.fileManager fileExistsAtPath:dstPath isDirectory:&isDirectory] || !isDirectory) {
if (!isDirectory) {
// New path is not directory, remove file
[self.fileManager removeItemAtPath:dstPath error:nil];
}
// New directory does not exist, rename directory
[self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil];
} else {
// New directory exist, merge the files
NSDirectoryEnumerator *dirEnumerator = [self.fileManager enumeratorAtPath:srcPath];
NSString *file;
while ((file = [dirEnumerator nextObject])) {
// Don't handle error, just try to move.
[self.fileManager moveItemAtPath:[srcPath stringByAppendingPathComponent:file] toPath:[dstPath stringByAppendingPathComponent:file] error:nil];
}
// Remove the old path
[self.fileManager removeItemAtPath:srcPath error:nil];
}
}
#pragma mark - Hash #pragma mark - Hash
static inline NSString * _Nullable SDDiskCacheFileNameForKey(NSString * _Nullable key) { static inline NSString * _Nullable SDDiskCacheFileNameForKey(NSString * _Nullable key) {

View File

@ -86,6 +86,9 @@
NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol"); NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol");
_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config]; _diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];
// Check and migrate disk cache directory if need
[self migrateDiskCacheDirectory];
#if SD_UIKIT #if SD_UIKIT
// Subscribe to app events // Subscribe to app events
@ -123,9 +126,22 @@
return [self.diskCache cachePathForKey:key]; return [self.diskCache cachePathForKey:key];
} }
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace { - (nullable NSString *)makeDiskCachePath:(nonnull NSString *)fullNamespace {
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace]; return [paths.firstObject stringByAppendingPathComponent:fullNamespace];
}
- (void)migrateDiskCacheDirectory {
if ([self.diskCache isKindOfClass:[SDDiskCache class]]) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *newDefaultPath = [[self makeDiskCachePath:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDImageCache.default"];
NSString *oldDefaultPath = [[self makeDiskCachePath:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDWebImageCache.default"];
dispatch_async(self.ioQueue, ^{
[((SDDiskCache *)self.diskCache) moveCacheDirectoryFromPath:oldDefaultPath toPath:newDefaultPath];
});
});
}
} }
#pragma mark - Store Ops #pragma mark - Store Ops

View File

@ -31,7 +31,13 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
} }
} }
if (!image) { if (!image) {
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:@{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}]; SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)};
if (context) {
SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy];
[mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext];
coderOptions = [mutableCoderOptions copy];
}
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
} }
if (image) { if (image) {
BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0; BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0;

View File

@ -12,6 +12,7 @@
typedef NSString * SDImageCoderOption NS_STRING_ENUM; typedef NSString * SDImageCoderOption NS_STRING_ENUM;
typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions; typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
typedef NSMutableDictionary<SDImageCoderOption, id> SDImageCoderMutableOptions;
#pragma mark - Coder Options #pragma mark - Coder Options
// These options are for image decoding // These options are for image decoding
@ -38,6 +39,14 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeFirstFrame
*/ */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality; FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality;
/**
A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext)
This option is ignored for all built-in coders and take no effect.
But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data inforamtion only.
See `SDWebImageContext` for more detailed information.
*/
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext;
#pragma mark - Coder #pragma mark - Coder
/** /**
This is the image coder protocol to provide custom image decoding/encoding. This is the image coder protocol to provide custom image decoding/encoding.
@ -72,11 +81,15 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressio
/** /**
Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder. Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder.
For custom coder which introduce new image format, you'd better define a new `SDImageFormat` using like this. If you're creating public coder plugin for new image format, also update `https://github.com/rs/SDWebImage/wiki/Coder-Plugin-List` to avoid same value been defined twice.
* @code
static const SDImageFormat SDImageFormatHEIF = 10;
* @endcode
@param format The image format @param format The image format
@return YES if this coder can encode the image, NO otherwise @return YES if this coder can encode the image, NO otherwise
*/ */
- (BOOL)canEncodeToFormat:(SDImageFormat)format; - (BOOL)canEncodeToFormat:(SDImageFormat)format NS_SWIFT_NAME(canEncode(to:));
/** /**
Encode the image to image data. Encode the image to image data.

View File

@ -13,3 +13,5 @@ SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor";
SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext";

View File

@ -45,7 +45,13 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
} }
} }
if (!image) { if (!image) {
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:@{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}]; SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)};
if (context) {
SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy];
[mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext];
coderOptions = [mutableCoderOptions copy];
}
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
} }
if (image) { if (image) {
BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0; BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0;
@ -115,7 +121,13 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
} }
} }
if (!image) { if (!image) {
image = [progressiveCoder incrementalDecodedImageWithOptions:@{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}]; SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)};
if (context) {
SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy];
[mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext];
coderOptions = [mutableCoderOptions copy];
}
image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions];
} }
if (image) { if (image) {
BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0; BOOL shouldDecode = (options & SDWebImageAvoidDecodeImage) == 0;

View File

@ -84,6 +84,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
// `setObject:forKey:` just call this with 0 cost. Override this is enough // `setObject:forKey:` just call this with 0 cost. Override this is enough
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g { - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g]; [super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) { if (key && obj) {
// Store weak cache // Store weak cache
LOCK(self.weakCacheLock); LOCK(self.weakCacheLock);
@ -94,6 +97,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
- (id)objectForKey:(id)key { - (id)objectForKey:(id)key {
id obj = [super objectForKey:key]; id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) { if (key && !obj) {
// Check weak cache // Check weak cache
LOCK(self.weakCacheLock); LOCK(self.weakCacheLock);
@ -113,6 +119,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
- (void)removeObjectForKey:(id)key { - (void)removeObjectForKey:(id)key {
[super removeObjectForKey:key]; [super removeObjectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key) { if (key) {
// Remove weak cache // Remove weak cache
LOCK(self.weakCacheLock); LOCK(self.weakCacheLock);
@ -123,6 +132,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
- (void)removeAllObjects { - (void)removeAllObjects {
[super removeAllObjects]; [super removeAllObjects];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
// Manually remove should also remove weak cache // Manually remove should also remove weak cache
LOCK(self.weakCacheLock); LOCK(self.weakCacheLock);
[self.weakCache removeAllObjects]; [self.weakCache removeAllObjects];

View File

@ -154,88 +154,6 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
context:(nullable SDWebImageContext *)context context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^NSOperation<SDWebImageDownloaderOperation> *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.config.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
mutableRequest.allHTTPHeaderFields = sself.HTTPHeaders;
id<SDWebImageDownloaderRequestModifier> requestModifier = context[SDWebImageContextDownloadRequestModifier];
if (!requestModifier) {
requestModifier = self.requestModifier;
}
NSURLRequest *request;
if (requestModifier) {
NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
// If modified request is nil, early return
if (!modifiedRequest) {
return nil;
} else {
request = [modifiedRequest copy];
}
} else {
request = [mutableRequest copy];
}
Class operationClass = sself.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
// Custom operation class
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:sself.session options:options context:context];
if (sself.config.urlCredential) {
operation.credential = sself.config.urlCredential;
} else if (sself.config.username && sself.config.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.config.username password:sself.config.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
if (sself.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
NSURL *url = token.url;
if (!url) {
return;
}
LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (operation) {
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:url];
}
}
UNLOCK(self.operationsLock);
}
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(NSOperation<SDWebImageDownloaderOperation> *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) { if (url == nil) {
if (completedBlock) { if (completedBlock) {
@ -248,8 +166,8 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
LOCK(self.operationsLock); LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url]; NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished) { if (!operation || operation.isFinished) {
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`. // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
operation = createCallback(); operation = [self createDownloaderOperationWithUrl:url options:options context:context];
if (!operation) { if (!operation) {
UNLOCK(self.operationsLock); UNLOCK(self.operationsLock);
if (completedBlock) { if (completedBlock) {
@ -274,7 +192,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
[self.downloadQueue addOperation:operation]; [self.downloadQueue addOperation:operation];
} }
UNLOCK(self.operationsLock); UNLOCK(self.operationsLock);
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new]; SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
@ -283,10 +201,96 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
token.request = operation.request; token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken; token.downloadOperationCancelToken = downloadOperationCancelToken;
token.downloader = self; token.downloader = self;
return token; return token;
} }
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
NSTimeInterval timeoutInterval = self.config.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}
NSURLRequest *request;
if (requestModifier) {
NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
// If modified request is nil, early return
if (!modifiedRequest) {
return nil;
} else {
request = [modifiedRequest copy];
}
} else {
request = [mutableRequest copy];
}
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
// Custom operation class
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
if ([operation respondsToSelector:@selector(setCredential:)]) {
if (self.config.urlCredential) {
operation.credential = self.config.urlCredential;
} else if (self.config.username && self.config.password) {
operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
}
}
if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
NSTimeInterval minimumProgressInterval = self.config.minimumProgressInterval;
minimumProgressInterval = MIN(MAX(minimumProgressInterval, 0), 1);
operation.minimumProgressInterval = minimumProgressInterval;
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
return operation;
}
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
NSURL *url = token.url;
if (!url) {
return;
}
LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (operation) {
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:url];
}
}
UNLOCK(self.operationsLock);
}
- (void)cancelAllDownloads { - (void)cancelAllDownloads {
[self.downloadQueue cancelAllOperations]; [self.downloadQueue cancelAllOperations];
} }
@ -480,6 +484,9 @@ didReceiveResponse:(NSURLResponse *)response
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates; if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority; if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages; if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
if (cachedImage && options & SDWebImageRefreshCached) { if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing // force progressive off if image already cached but forced refreshing

View File

@ -41,6 +41,15 @@ typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
*/ */
@property (nonatomic, assign) NSTimeInterval downloadTimeout; @property (nonatomic, assign) NSTimeInterval downloadTimeout;
/**
* The minimum interval about progress percent during network downloading. Which means the next progress callback and current progress callback's progress percent difference should be larger or equal to this value.
* The value should be 0.0-1.0.
* @note If you're using progressive decoding feature, this will also effect the image refresh rate.
* @note This value may enhance the performance if you don't want progress callback too frequently.
* Defaults to 0, which means each time we receive the new data from URLSession, we callback the progressBlock immediately.
*/
@property (nonatomic, assign) NSTimeInterval minimumProgressInterval;
/** /**
* The custom session configuration in use by NSURLSession. If you don't provide one, we will use `defaultSessionConfiguration` instead. * The custom session configuration in use by NSURLSession. If you don't provide one, we will use `defaultSessionConfiguration` instead.
* Defatuls to nil. * Defatuls to nil.

View File

@ -34,6 +34,7 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
SDWebImageDownloaderConfig *config = [[[self class] allocWithZone:zone] init]; SDWebImageDownloaderConfig *config = [[[self class] allocWithZone:zone] init];
config.maxConcurrentDownloads = self.maxConcurrentDownloads; config.maxConcurrentDownloads = self.maxConcurrentDownloads;
config.downloadTimeout = self.downloadTimeout; config.downloadTimeout = self.downloadTimeout;
config.minimumProgressInterval = self.minimumProgressInterval;
config.sessionConfiguration = [self.sessionConfiguration copyWithZone:zone]; config.sessionConfiguration = [self.sessionConfiguration copyWithZone:zone];
config.operationClass = self.operationClass; config.operationClass = self.operationClass;
config.executionOrder = self.executionOrder; config.executionOrder = self.executionOrder;

View File

@ -36,9 +36,6 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
- (BOOL)cancel:(nullable id)token; - (BOOL)cancel:(nullable id)token;
- (nullable NSURLRequest *)request; - (nullable NSURLRequest *)request;
@ -46,6 +43,10 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification
@optional @optional
- (nullable NSURLSessionTask *)dataTask; - (nullable NSURLSessionTask *)dataTask;
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)credential;
- (NSTimeInterval)minimumProgressInterval;
- (void)setMinimumProgressInterval:(NSTimeInterval)minimumProgressInterval;
@end @end
@ -74,6 +75,15 @@ FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification
*/ */
@property (nonatomic, strong, nullable) NSURLCredential *credential; @property (nonatomic, strong, nullable) NSURLCredential *credential;
/**
* The minimum interval about progress percent during network downloading. Which means the next progress callback and current progress callback's progress percent difference should be larger or equal to this value.
* The value should be 0.0-1.0.
* @note If you're using progressive decoding feature, this will also effect the image refresh rate.
* @note This value may enhance the performance if you don't want progress callback too frequently.
* Defaults to 0, which means each time we receive the new data from URLSession, we callback the progressBlock immediately.
*/
@property (assign, nonatomic) NSTimeInterval minimumProgressInterval;
/** /**
* The options for the receiver. * The options for the receiver.
*/ */

View File

@ -37,9 +37,11 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (assign, nonatomic, getter = isFinished) BOOL finished; @property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (strong, nonatomic, nullable) NSMutableData *imageData; @property (strong, nonatomic, nullable) NSMutableData *imageData;
@property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse` @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
@property (assign, nonatomic, readwrite) NSUInteger expectedSize; @property (assign, nonatomic) NSUInteger expectedSize; // may be 0
@property (assign, nonatomic) NSUInteger receivedSize;
@property (strong, nonatomic, nullable, readwrite) NSURLResponse *response; @property (strong, nonatomic, nullable, readwrite) NSURLResponse *response;
@property (strong, nonatomic, nullable) NSError *responseError; @property (strong, nonatomic, nullable) NSError *responseError;
@property (assign, nonatomic) double previousProgress; // previous progress percent
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation // the task associated with this operation
@ -336,17 +338,37 @@ didReceiveResponse:(NSURLResponse *)response
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize]; self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
} }
[self.imageData appendData:data]; [self.imageData appendData:data];
self.receivedSize = self.imageData.length;
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);
}
return;
}
// Get the finish status
BOOL finished = (self.receivedSize >= self.expectedSize);
// Get the current progress
double currentProgress = (double)self.receivedSize / (double)self.expectedSize;
double previousProgress = self.previousProgress;
// Check if we need callback progress
if (currentProgress - previousProgress < self.minimumProgressInterval) {
return;
}
self.previousProgress = currentProgress;
if ((self.options & SDWebImageDownloaderProgressiveLoad) && self.expectedSize > 0) { if (self.options & SDWebImageDownloaderProgressiveLoad) {
// Get the image data // Get the image data
NSData *imageData = [self.imageData copy]; NSData *imageData = [self.imageData copy];
// Get the total bytes downloaded
const NSUInteger totalSize = imageData.length;
// Get the finish status
BOOL finished = (totalSize >= self.expectedSize);
// progressive decode the image in coder queue // progressive decode the image in coder queue
dispatch_async(self.coderQueue, ^{ dispatch_async(self.coderQueue, ^{
// If all the data has already been downloaded, earily return to avoid further decoding
if (self.receivedSize >= self.expectedSize) {
return;
}
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
if (image) { if (image) {
// 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. // 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.
@ -355,9 +377,9 @@ didReceiveResponse:(NSURLResponse *)response
} }
}); });
} }
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL); progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
} }
} }

View File

@ -21,7 +21,8 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png";
@end @end
@interface SDImageCacheTests : SDTestCase @interface SDImageCacheTests : SDTestCase <NSFileManagerDelegate>
@end @end
@implementation SDImageCacheTests @implementation SDImageCacheTests
@ -359,6 +360,25 @@ static NSString *kTestImageKeyPNG = @"TestImageKey.png";
expect([diskCache isKindOfClass:[SDWebImageTestDiskCache class]]).to.beTruthy(); expect([diskCache isKindOfClass:[SDWebImageTestDiskCache class]]).to.beTruthy();
} }
- (void)test44DiskCacheMigrationFromOldVersion {
SDImageCacheConfig *config = [[SDImageCacheConfig alloc] init];
NSFileManager *fileManager = [[NSFileManager alloc] init];
config.fileManager = fileManager;
// Fake to store a.png into old path
NSString *newDefaultPath = [[self makeDiskCachePath:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDImageCache.default"];
NSString *oldDefaultPath = [[self makeDiskCachePath:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDWebImageCache.default"];
[fileManager createDirectoryAtPath:oldDefaultPath withIntermediateDirectories:YES attributes:nil error:nil];
[fileManager createFileAtPath:[oldDefaultPath stringByAppendingPathComponent:@"a.png"] contents:[NSData dataWithContentsOfFile:[self testPNGPath]] attributes:nil];
// Call migration
SDDiskCache *diskCache = [[SDDiskCache alloc] initWithCachePath:newDefaultPath config:config];
[diskCache moveCacheDirectoryFromPath:oldDefaultPath toPath:newDefaultPath];
// Expect a.png into new path
BOOL exist = [fileManager fileExistsAtPath:[newDefaultPath stringByAppendingPathComponent:@"a.png"]];
expect(exist).beTruthy();
}
#pragma mark - SDImageCache & SDImageCachesManager #pragma mark - SDImageCache & SDImageCachesManager
- (void)test50SDImageCacheQueryOp { - (void)test50SDImageCacheQueryOp {
XCTestExpectation *expectation = [self expectationWithDescription:@"SDImageCache query op works"]; XCTestExpectation *expectation = [self expectationWithDescription:@"SDImageCache query op works"];

View File

@ -21,11 +21,6 @@
@interface SDWebImageDownloader () @interface SDWebImageDownloader ()
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue; @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(NSOperation<SDWebImageDownloaderOperation> *(^)(void))createCallback;
@end @end
@ -104,16 +99,15 @@
[downloader invalidateSessionAndCancel:YES]; [downloader invalidateSessionAndCancel:YES];
} }
- (void)test07ThatAddProgressCallbackCompletedBlockWithNilURLCallsTheCompletionBlockWithNils { - (void)test07ThatDownloadImageWithNilURLCallsCompletionWithNils {
XCTestExpectation *expectation = [self expectationWithDescription:@"Completion is called with nils"]; XCTestExpectation *expectation = [self expectationWithDescription:@"Completion is called with nils"];
[[SDWebImageDownloader sharedDownloader] addProgressCallback:nil completedBlock:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) { [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:nil options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (!image && !data && error) { expect(image).to.beNil();
[expectation fulfill]; expect(data).to.beNil();
} else { expect(error.code).equal(SDWebImageErrorInvalidURL);
XCTFail(@"All params except error should be nil"); [expectation fulfill];
} }];
} forURL:nil createCallback:nil]; [self waitForExpectationsWithCommonTimeout];
[self waitForExpectationsWithTimeout:0.5 handler:nil];
} }
- (void)test08ThatAHTTPAuthDownloadWorks { - (void)test08ThatAHTTPAuthDownloadWorks {
@ -265,6 +259,34 @@
[self waitForExpectationsWithCommonTimeout]; [self waitForExpectationsWithCommonTimeout];
} }
- (void)test17ThatMinimumProgressIntervalWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Minimum progress interval"];
SDWebImageDownloaderConfig *config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
config.minimumProgressInterval = 0.51; // This will make the progress only callback once
SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] initWithConfig:config];
NSURL *imageURL = [NSURL URLWithString:@"http://www.ioncannon.net/wp-content/uploads/2011/06/test2.webp"];
__block NSUInteger allProgressCount = 0; // All progress (including operation start / first HTTP response, etc)
__block NSUInteger validProgressCount = 0; // Only progress from `URLSession:dataTask:didReceiveData:`
[downloader downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
allProgressCount++;
if (expectedSize <= 0 || receivedSize <= 0) {
// ignore the progress callback until we receive data
return;
}
validProgressCount++;
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (allProgressCount > 1 && validProgressCount == 1) {
[expectation fulfill];
} else {
XCTFail(@"Progress callback more than once");
}
}];
[self waitForExpectationsWithCommonTimeoutUsingHandler:^(NSError * _Nullable error) {
[downloader invalidateSessionAndCancel:YES];
}];
}
/** /**
* Per #883 - Fix multiple requests for same image and then canceling one * Per #883 - Fix multiple requests for same image and then canceling one
* Old SDWebImage (3.x) could not handle correctly multiple requests for the same image + cancel * Old SDWebImage (3.x) could not handle correctly multiple requests for the same image + cancel

View File

@ -15,7 +15,6 @@
*/ */
@interface SDWebImageTestDownloadOperation : NSOperation <SDWebImageDownloaderOperation> @interface SDWebImageTestDownloadOperation : NSOperation <SDWebImageDownloaderOperation>
@property (nonatomic, strong, nullable) NSURLCredential *credential;
@property (nonatomic, strong, nullable) NSURLRequest *request; @property (nonatomic, strong, nullable) NSURLRequest *request;
@property (nonatomic, strong, nullable) NSURLResponse *response; @property (nonatomic, strong, nullable) NSURLResponse *response;

View File

@ -15,11 +15,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>FMWK</string> <string>FMWK</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>5.0.0-beta</string> <string>5.0.0-beta2</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>5.0.0-beta</string> <string>5.0.0-beta2</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string></string> <string></string>
</dict> </dict>