Merge pull request #2191 from dreampiggy/refactor_prefetcher

Refactor the implementation of SDWebImagePrefetcher
This commit is contained in:
DreamPiggy 2018-03-09 19:12:00 +08:00 committed by GitHub
commit a6fc140f36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 288 additions and 104 deletions

View File

@ -11,12 +11,18 @@
@class SDWebImagePrefetcher;
@interface SDWebImagePrefetchToken : NSObject <SDWebImageOperation>
@property (nonatomic, copy, readonly, nullable) NSArray<NSURL *> *urls;
@end
@protocol SDWebImagePrefetcherDelegate <NSObject>
@optional
/**
* Called when an image was prefetched.
* Called when an image was prefetched. Which means it's called when one URL from any of prefetching finished.
*
* @param imagePrefetcher The current image prefetcher
* @param imageURL The image url that was prefetched
@ -26,7 +32,7 @@
- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;
/**
* Called when all images are prefetched.
* Called when all images are prefetched. Which means it's called when all URLs from all of prefetching finished.
* @param imagePrefetcher The current image prefetcher
* @param totalCount The total number of images that were prefetched (whether successful or not)
* @param skippedCount The total number of images that were skipped
@ -54,15 +60,23 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
@property (nonatomic, assign) NSUInteger maxConcurrentDownloads;
/**
* SDWebImageOptions for prefetcher. Defaults to SDWebImageLowPriority.
* The options for prefetcher. Defaults to SDWebImageLowPriority.
*/
@property (nonatomic, assign) SDWebImageOptions options;
/**
* Queue options for Prefetcher. Defaults to Main Queue.
* The context for prefetcher. Defaults to nil.
*/
@property (nonatomic, copy, nullable) SDWebImageContext *context;
/**
* Queue options for prefetcher when call the progressBlock, completionBlock and delegate methods. Defaults to Main Queue.
*/
@property (strong, nonatomic, nonnull) dispatch_queue_t prefetcherQueue;
/**
* The delegate for the prefetcher.
*/
@property (weak, nonatomic, nullable) id <SDWebImagePrefetcherDelegate> delegate;
/**
@ -76,35 +90,35 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
- (nonnull instancetype)initWithImageManager:(nonnull SDWebImageManager *)manager NS_DESIGNATED_INITIALIZER;
/**
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching,
* currently one image is downloaded at a time,
* and skips images for failed downloads and proceed to the next image in the list.
* Any previously-running prefetch operations are canceled.
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property.
* Prefetching is seperate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls.
* Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch.
*
* @param urls list of URLs to prefetch
* @return the token to cancel the current prefetching.
*/
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls;
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls;
/**
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching,
* currently one image is downloaded at a time,
* and skips images for failed downloads and proceed to the next image in the list.
* Any previously-running prefetch operations are canceled.
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property.
* Prefetching is seperate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls.
* Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch.
*
* @param urls list of URLs to prefetch
* @param progressBlock block to be called when progress updates;
* first parameter is the number of completed (successful or not) requests,
* second parameter is the total number of images originally requested to be prefetched
* @param completionBlock block to be called when prefetching is completed
* @param completionBlock block to be called when the current prefetching is completed
* first param is the number of completed (successful or not) requests,
* second parameter is the number of skipped requests
* @return the token to cancel the current prefetching.
*/
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
/**
* Remove and cancel queued list
* Remove and cancel all the prefeching for the prefetcher.
*/
- (void)cancelPrefetching;

View File

@ -7,17 +7,27 @@
*/
#import "SDWebImagePrefetcher.h"
#import <libkern/OSAtomic.h>
@interface SDWebImagePrefetchToken () {
@public
int64_t _skippedCount;
int64_t _finishedCount;
}
@property (nonatomic, copy, readwrite) NSArray<NSURL *> *urls;
@property (nonatomic, assign) int64_t totalCount;
@property (nonatomic, strong) NSMutableArray<id<SDWebImageOperation>> *operations;
@property (nonatomic, weak) SDWebImagePrefetcher *prefetcher;
@property (nonatomic, copy, nullable) SDWebImagePrefetcherCompletionBlock completionBlock;
@property (nonatomic, copy, nullable) SDWebImagePrefetcherProgressBlock progressBlock;
@end
@interface SDWebImagePrefetcher ()
@property (strong, nonatomic, nonnull) SDWebImageManager *manager;
@property (strong, atomic, nullable) NSArray<NSURL *> *prefetchURLs; // may be accessed from different queue
@property (assign, nonatomic) NSUInteger requestedCount;
@property (assign, nonatomic) NSUInteger skippedCount;
@property (assign, nonatomic) NSUInteger finishedCount;
@property (assign, nonatomic) NSTimeInterval startedTime;
@property (copy, nonatomic, nullable) SDWebImagePrefetcherCompletionBlock completionBlock;
@property (copy, nonatomic, nullable) SDWebImagePrefetcherProgressBlock progressBlock;
@property (strong, atomic, nonnull) NSMutableSet<SDWebImagePrefetchToken *> *runningTokens;
@end
@ -39,6 +49,7 @@
- (nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager {
if ((self = [super init])) {
_manager = manager;
_runningTokens = [NSMutableSet set];
_options = SDWebImageLowPriority;
_prefetcherQueue = dispatch_get_main_queue();
self.maxConcurrentDownloads = 3;
@ -54,91 +65,169 @@
return self.manager.imageDownloader.maxConcurrentDownloads;
}
- (void)startPrefetchingAtIndex:(NSUInteger)index {
NSURL *currentURL;
@synchronized(self) {
if (index >= self.prefetchURLs.count) return;
currentURL = self.prefetchURLs[index];
self.requestedCount++;
}
[self.manager loadImageWithURL:currentURL options:self.options progress:nil completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!finished) return;
self.finishedCount++;
if (self.progressBlock) {
self.progressBlock(self.finishedCount,(self.prefetchURLs).count);
}
if (!image) {
// Add last failed
self.skippedCount++;
}
if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]) {
[self.delegate imagePrefetcher:self
didPrefetchURL:currentURL
finishedCount:self.finishedCount
totalCount:self.prefetchURLs.count
];
}
if (self.prefetchURLs.count > self.requestedCount) {
dispatch_async(self.prefetcherQueue, ^{
// we need dispatch to avoid function recursion call. This can prevent stack overflow even for huge urls list
[self startPrefetchingAtIndex:self.requestedCount];
});
} else if (self.finishedCount == self.requestedCount) {
[self reportStatus];
if (self.completionBlock) {
self.completionBlock(self.finishedCount, self.skippedCount);
self.completionBlock = nil;
}
self.progressBlock = nil;
}
}];
#pragma mark - Prefetch
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls {
return [self prefetchURLs:urls progress:nil completed:nil];
}
- (void)reportStatus {
NSUInteger total = (self.prefetchURLs).count;
if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didFinishWithTotalCount:skippedCount:)]) {
[self.delegate imagePrefetcher:self
didFinishWithTotalCount:(total - self.skippedCount)
skippedCount:self.skippedCount
];
}
}
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls {
[self prefetchURLs:urls progress:nil completed:nil];
}
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
[self cancelPrefetching]; // Prevent duplicate prefetch request
self.startedTime = CFAbsoluteTimeGetCurrent();
self.prefetchURLs = urls;
self.completionBlock = completionBlock;
self.progressBlock = progressBlock;
if (urls.count == 0) {
if (!urls || urls.count == 0) {
if (completionBlock) {
completionBlock(0,0);
completionBlock(0, 0);
}
} else {
// Starts prefetching from the very first image on the list with the max allowed concurrency
NSUInteger listCount = self.prefetchURLs.count;
for (NSUInteger i = 0; i < self.maxConcurrentDownloads && self.requestedCount < listCount; i++) {
[self startPrefetchingAtIndex:i];
return nil;
}
SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
token.prefetcher = self;
token->_skippedCount = 0;
token->_finishedCount = 0;
token.urls = urls;
token.totalCount = urls.count;
token.operations = [NSMutableArray arrayWithCapacity:urls.count];
token.progressBlock = progressBlock;
token.completionBlock = completionBlock;
[self addRunningToken:token];
for (NSURL *url in urls) {
__weak typeof(self) wself = self;
id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
if (!finished) {
return;
}
OSAtomicIncrement64(&(token->_finishedCount));
if (error) {
// Add last failed
OSAtomicIncrement64(&(token->_skippedCount));
}
// Current operation finished
[sself callProgressBlockForToken:token imageURL:imageURL];
if (token->_finishedCount + token->_skippedCount == token.totalCount) {
// All finished
[sself callCompletionBlockForToken:token];
[sself removeRunningToken:token];
}
} context:self.context];
[token.operations addObject:operation];
}
return token;
}
#pragma mark - Cancel
- (void)cancelPrefetching {
@synchronized(self.runningTokens) {
NSSet<SDWebImagePrefetchToken *> *copiedTokens = [self.runningTokens copy];
[copiedTokens makeObjectsPerformSelector:@selector(cancel)];
[self.runningTokens removeAllObjects];
}
}
- (void)cancelPrefetching {
@synchronized(self) {
self.prefetchURLs = nil;
self.skippedCount = 0;
self.requestedCount = 0;
self.finishedCount = 0;
- (void)callProgressBlockForToken:(SDWebImagePrefetchToken *)token imageURL:(NSURL *)url {
if (!token) {
return;
}
[self.manager cancelAll];
BOOL shouldCallDelegate = [self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)];
dispatch_queue_async_safe(self.prefetcherQueue, ^{
if (shouldCallDelegate) {
[self.delegate imagePrefetcher:self didPrefetchURL:url finishedCount:[self tokenFinishedCount] totalCount:[self tokenTotalCount]];
}
if (token.progressBlock) {
token.progressBlock((NSUInteger)token->_finishedCount, (NSUInteger)token.totalCount);
}
});
}
- (void)callCompletionBlockForToken:(SDWebImagePrefetchToken *)token {
if (!token) {
return;
}
BOOL shoulCallDelegate = [self.delegate respondsToSelector:@selector(imagePrefetcher:didFinishWithTotalCount:skippedCount:)] && ([self countOfRunningTokens] == 1); // last one
dispatch_queue_async_safe(self.prefetcherQueue, ^{
if (shoulCallDelegate) {
[self.delegate imagePrefetcher:self didFinishWithTotalCount:[self tokenTotalCount] skippedCount:[self tokenSkippedCount]];
}
if (token.completionBlock) {
token.completionBlock((NSUInteger)token->_finishedCount, (NSUInteger)token->_skippedCount);
}
});
}
#pragma mark - Helper
- (NSUInteger)tokenTotalCount {
NSUInteger tokenTotalCount = 0;
@synchronized (self.runningTokens) {
for (SDWebImagePrefetchToken *token in self.runningTokens) {
tokenTotalCount += token.totalCount;
}
}
return tokenTotalCount;
}
- (NSUInteger)tokenSkippedCount {
NSUInteger tokenSkippedCount = 0;
@synchronized (self.runningTokens) {
for (SDWebImagePrefetchToken *token in self.runningTokens) {
tokenSkippedCount += token->_skippedCount;
}
}
return tokenSkippedCount;
}
- (NSUInteger)tokenFinishedCount {
NSUInteger tokenFinishedCount = 0;
@synchronized (self.runningTokens) {
for (SDWebImagePrefetchToken *token in self.runningTokens) {
tokenFinishedCount += token->_finishedCount;
}
}
return tokenFinishedCount;
}
- (void)addRunningToken:(SDWebImagePrefetchToken *)token {
if (!token) {
return;
}
@synchronized (self.runningTokens) {
[self.runningTokens addObject:token];
}
}
- (void)removeRunningToken:(SDWebImagePrefetchToken *)token {
if (!token) {
return;
}
@synchronized (self.runningTokens) {
[self.runningTokens removeObject:token];
}
}
- (NSUInteger)countOfRunningTokens {
NSUInteger count = 0;
@synchronized (self.runningTokens) {
count = self.runningTokens.count;
}
return count;
}
@end
@implementation SDWebImagePrefetchToken
- (void)cancel {
@synchronized (self) {
for (id<SDWebImageOperation> operation in self.operations) {
[operation cancel];
}
}
[self.prefetcher removeRunningToken:self];
}
@end

View File

@ -10,7 +10,12 @@
#import "SDTestCase.h"
#import <SDWebImage/SDWebImagePrefetcher.h>
@interface SDWebImagePrefetcherTests : SDTestCase
@interface SDWebImagePrefetcherTests : SDTestCase <SDWebImagePrefetcherDelegate>
@property (nonatomic, strong) SDWebImagePrefetcher *prefetcher;
@property (nonatomic, assign) NSUInteger finishedCount;
@property (nonatomic, assign) NSUInteger skippedCount;
@property (nonatomic, assign) NSUInteger totalCount;
@end
@ -59,6 +64,82 @@
[self waitForExpectationsWithCommonTimeout];
}
// TODO: test the prefetcher delegate works
- (void)test04PrefetchWithMultipleArrayDifferentQueueWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Prefetch with multiple array at different queue failed"];
NSArray *imageURLs1 = @[@"http://via.placeholder.com/20x20.jpg",
@"http://via.placeholder.com/30x30.jpg"];
NSArray *imageURLs2 = @[@"http://via.placeholder.com/30x30.jpg",
@"http://via.placeholder.com/40x40.jpg"];
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
__block int numberOfPrefetched1 = 0;
__block int numberOfPrefetched2 = 0;
__block BOOL prefetchFinished1 = NO;
__block BOOL prefetchFinished2 = NO;
// Clear the disk cache to make it more realistic for multi-thread environment
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{
dispatch_async(queue1, ^{
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs1 progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
numberOfPrefetched1 += 1;
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
expect(numberOfPrefetched1).to.equal(noOfFinishedUrls);
prefetchFinished1 = YES;
// both completion called
if (prefetchFinished1 && prefetchFinished2) {
[expectation fulfill];
}
}];
});
dispatch_async(queue2, ^{
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs2 progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
numberOfPrefetched2 += 1;
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
expect(numberOfPrefetched2).to.equal(noOfFinishedUrls);
prefetchFinished2 = YES;
// both completion called
if (prefetchFinished1 && prefetchFinished2) {
[expectation fulfill];
}
}];
});
}];
[self waitForExpectationsWithCommonTimeout];
}
- (void)test05PrefecherDelegateWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Prefetcher delegate failed"];
NSArray *imageURLs = @[@"http://via.placeholder.com/20x20.jpg",
@"http://via.placeholder.com/30x30.jpg",
@"http://via.placeholder.com/40x40.jpg"];
self.prefetcher = [SDWebImagePrefetcher new];
self.prefetcher.delegate = self;
// Current implementation, the delegate method called before the progressBlock and completionBlock
[self.prefetcher prefetchURLs:imageURLs progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
expect(self.finishedCount).to.equal(noOfFinishedUrls);
expect(self.totalCount).to.equal(noOfTotalUrls);
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
expect(self.finishedCount).to.equal(noOfFinishedUrls);
expect(self.skippedCount).to.equal(noOfSkippedUrls);
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
- (void)imagePrefetcher:(SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount {
expect(imagePrefetcher).to.equal(self.prefetcher);
self.skippedCount = skippedCount;
self.totalCount = totalCount;
}
- (void)imagePrefetcher:(SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount {
expect(imagePrefetcher).to.equal(self.prefetcher);
self.finishedCount = finishedCount;
self.totalCount = totalCount;
}
@end