Skip to content

Commit

Permalink
Have ASNetworkImageNode report whether images were cached or not (Tex…
Browse files Browse the repository at this point in the history
…tureGroup#639)

* Have ASNetworkImageNode report whether images were cached or not.

* Update changelog

* Add fileURL case
  • Loading branch information
Adlai-Holler authored Oct 25, 2017
1 parent 8317c11 commit af99ff5
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [ASCollectionView] Improve index space translation of Flow Layout Delegate methods. [Scott Goodson](https://github.com/appleguy)
- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon)
- [ASCollectionView] Check if batch fetching is needed if batch fetching parameter has been changed. [#624](https://github.com/TextureGroup/Texture/pull/624) [Garrett Moon](https://github.com/garrettmoon)
- [ASNetworkImageNode] New delegate callback to tell the consumer whether the image was loaded from cache or download. [Adlai Holler](https://github.com/Adlai-Holler)

## 2.6
- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon)
Expand Down
27 changes: 27 additions & 0 deletions Source/ASNetworkImageNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,40 @@ NS_ASSUME_NONNULL_BEGIN


#pragma mark -

typedef NS_ENUM(NSInteger, ASNetworkImageSource) {
ASNetworkImageSourceUnspecified = 0,
ASNetworkImageSourceSynchronousCache,
ASNetworkImageSourceAsynchronousCache,
ASNetworkImageSourceFileURL,
ASNetworkImageSourceDownload,
};

/// A struct that carries details about ASNetworkImageNode's image loads.
typedef struct {
/// The source from which the image was loaded.
ASNetworkImageSource imageSource;
} ASNetworkImageNodeDidLoadInfo;

/**
* The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to
* notifications such as finished decoding and downloading an image.
*/
@protocol ASNetworkImageNodeDelegate <NSObject>
@optional

/**
* Notification that the image node finished downloading an image, with additional info.
* If implemented, this method will be called instead of `imageNode:didLoadImage:`.
*
* @param imageNode The sender.
* @param image The newly-loaded image.
* @param info Misc information about the image load.
*
* @discussion Called on a background queue.
*/
- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image info:(ASNetworkImageNodeDidLoadInfo)info;

/**
* Notification that the image node finished downloading an image.
*
Expand Down
40 changes: 33 additions & 7 deletions Source/ASNetworkImageNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ @interface ASNetworkImageNode ()
unsigned int delegateDidFailWithError:1;
unsigned int delegateDidFinishDecoding:1;
unsigned int delegateDidLoadImage:1;
unsigned int delegateDidLoadImageWithInfo:1;
} _delegateFlags;


Expand Down Expand Up @@ -305,6 +306,7 @@ - (void)setDelegate:(id<ASNetworkImageNodeDelegate>)delegate
_delegateFlags.delegateDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)];
_delegateFlags.delegateDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)];
_delegateFlags.delegateDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)];
_delegateFlags.delegateDidLoadImageWithInfo = [delegate respondsToSelector:@selector(imageNode:didLoadImage:info:)];
}

- (id<ASNetworkImageNodeDelegate>)delegate
Expand Down Expand Up @@ -353,8 +355,18 @@ - (void)displayWillStartAsynchronously:(BOOL)asynchronously
if (result) {
[self _locked_setCurrentImageQuality:1.0];
[self _locked__setImage:result];

_imageLoaded = YES;

// Call out to the delegate.
if (_delegateFlags.delegateDidLoadImageWithInfo) {
ASDN::MutexUnlocker l(__instanceLock__);
ASNetworkImageNodeDidLoadInfo info = {};
info.imageSource = ASNetworkImageSourceSynchronousCache;
[_delegate imageNode:self didLoadImage:result info:info];
} else if (_delegateFlags.delegateDidLoadImage) {
ASDN::MutexUnlocker l(__instanceLock__);
[_delegate imageNode:self didLoadImage:result];
}
break;
}
}
Expand Down Expand Up @@ -688,14 +700,19 @@ - (void)_lazilyLoadImageIfNecessary

[self _locked_setCurrentImageQuality:1.0];

if (_delegateFlags.delegateDidLoadImage) {
if (_delegateFlags.delegateDidLoadImageWithInfo) {
ASDN::MutexUnlocker u(__instanceLock__);
ASNetworkImageNodeDidLoadInfo info = {};
info.imageSource = ASNetworkImageSourceFileURL;
[delegate imageNode:self didLoadImage:self.image info:info];
} else if (_delegateFlags.delegateDidLoadImage) {
ASDN::MutexUnlocker u(__instanceLock__);
[delegate imageNode:self didLoadImage:self.image];
}
});
} else {
__weak __typeof__(self) weakSelf = self;
auto finished = ^(id <ASImageContainerProtocol>imageContainer, NSError *error, id downloadIdentifier) {
auto finished = ^(id <ASImageContainerProtocol>imageContainer, NSError *error, id downloadIdentifier, ASNetworkImageSource imageSource) {

__typeof__(self) strongSelf = weakSelf;
if (strongSelf == nil) {
Expand Down Expand Up @@ -732,7 +749,12 @@ - (void)_lazilyLoadImageIfNecessary
strongSelf->_cacheUUID = nil;

if (imageContainer != nil) {
if (strongSelf->_delegateFlags.delegateDidLoadImage) {
if (strongSelf->_delegateFlags.delegateDidLoadImageWithInfo) {
ASDN::MutexUnlocker u(strongSelf->__instanceLock__);
ASNetworkImageNodeDidLoadInfo info = {};
info.imageSource = imageSource;
[delegate imageNode:strongSelf didLoadImage:strongSelf.image info:info];
} else if (strongSelf->_delegateFlags.delegateDidLoadImage) {
ASDN::MutexUnlocker u(strongSelf->__instanceLock__);
[delegate imageNode:strongSelf didLoadImage:strongSelf.image];
}
Expand Down Expand Up @@ -763,10 +785,12 @@ - (void)_lazilyLoadImageIfNecessary
}

if ([imageContainer asdk_image] == nil && _downloader != nil) {
[self _downloadImageWithCompletion:finished];
[self _downloadImageWithCompletion:^(id<ASImageContainerProtocol> imageContainer, NSError *error, id downloadIdentifier) {
finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload);
}];
} else {
as_log_verbose(ASImageLoadingLog(), "Decached image for %@ img: %@ urls: %@", self, [imageContainer asdk_image], URLs);
finished(imageContainer, nil, nil);
finished(imageContainer, nil, nil, ASNetworkImageSourceAsynchronousCache);
}
};

Expand All @@ -780,7 +804,9 @@ - (void)_lazilyLoadImageIfNecessary
completion:completion];
}
} else {
[self _downloadImageWithCompletion:finished];
[self _downloadImageWithCompletion:^(id<ASImageContainerProtocol> imageContainer, NSError *error, id downloadIdentifier) {
finished(imageContainer, error, downloadIdentifier, ASNetworkImageSourceDownload);
}];
}
}
}
Expand Down
109 changes: 58 additions & 51 deletions Source/Details/ASPINRemoteImageDownloader.m
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,11 @@ - (void)cachedImageWithURL:(NSURL *)URL
callbackQueue:(dispatch_queue_t)callbackQueue
completion:(ASImageCacherCompletion)completion
{
// We do not check the cache here and instead check it in downloadImageWithURL to avoid checking the cache twice.
// If we're targeting the main queue and we're on the main thread, complete immediately.
if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) {
completion(nil);
} else {
dispatch_async(callbackQueue, ^{
completion(nil);
});
}
[[self sharedPINRemoteImageManager] imageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode completion:^(PINRemoteImageManagerResult * _Nonnull result) {
[ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{
completion(result.image);
}];
}];
}

- (void)cachedImageWithURLs:(NSArray <NSURL *> *)URLs
Expand Down Expand Up @@ -256,51 +252,38 @@ - (nullable id)downloadImageWithURLs:(NSArray <NSURL *> *)URLs
downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress
completion:(ASImageDownloaderCompletion)completion
{
PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) {
if (downloadProgress == nil) { return; }

/// If we're targeting the main queue and we're on the main thread, call immediately.
if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) {
downloadProgress(completedBytes / (CGFloat)totalBytes);
} else {
dispatch_async(callbackQueue, ^{
downloadProgress(completedBytes / (CGFloat)totalBytes);
});
}
};

PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) {
/// If we're targeting the main queue and we're on the main thread, complete immediately.
if (ASDisplayNodeThreadIsMain() && callbackQueue == dispatch_get_main_queue()) {
#if PIN_ANIMATED_AVAILABLE
if (result.alternativeRepresentation) {
completion(result.alternativeRepresentation, result.error, result.UUID);
} else {
completion(result.image, result.error, result.UUID);
}
#else
completion(result.image, result.error, result.UUID);
#endif
} else {
dispatch_async(callbackQueue, ^{
PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) {
if (downloadProgress == nil) { return; }

[ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{
downloadProgress(completedBytes / (CGFloat)totalBytes);
}];
};

PINRemoteImageManagerImageCompletion imageCompletion = ^(PINRemoteImageManagerResult * _Nonnull result) {
[ASPINRemoteImageDownloader _performWithCallbackQueue:callbackQueue work:^{
#if PIN_ANIMATED_AVAILABLE
if (result.alternativeRepresentation) {
completion(result.alternativeRepresentation, result.error, result.UUID);
} else {
completion(result.image, result.error, result.UUID);
}
if (result.alternativeRepresentation) {
completion(result.alternativeRepresentation, result.error, result.UUID);
} else {
completion(result.image, result.error, result.UUID);
}
#else
completion(result.image, result.error, result.UUID);
completion(result.image, result.error, result.UUID);
#endif
});
}
};

return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs
options:PINRemoteImageManagerDownloadOptionsSkipDecode
progressImage:nil
progressDownload:progressDownload
completion:imageCompletion];
}];
};

// add "IgnoreCache" option since we have a caching API so we already checked it, not worth checking again.
// PINRemoteImage is responsible for coalescing downloads, and even if it wasn't, the tiny probability of
// extra downloads isn't worth the effort of rechecking caches every single time. In order to provide
// feedback to the consumer about whether images are cached, we can't simply make the cache a no-op and
// check the cache as part of this download.
return [[self sharedPINRemoteImageManager] downloadImageWithURLs:URLs
options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache
progressImage:nil
progressDownload:progressDownload
completion:imageCompletion];
}

- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier
Expand Down Expand Up @@ -369,5 +352,29 @@ - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageMana
return nil;
}

#pragma mark - Private

/**
* If on main thread and queue is main, perform now.
* If queue is nil, assert and perform now.
* Otherwise, dispatch async to queue.
*/
+ (void)_performWithCallbackQueue:(dispatch_queue_t)queue work:(void (^)())work
{
if (work == nil) {
// No need to assert here, really. We aren't expecting any feedback from this method.
return;
}

if (ASDisplayNodeThreadIsMain() && queue == dispatch_get_main_queue()) {
work();
} else if (queue == nil) {
ASDisplayNodeFailAssert(@"Callback queue should not be nil.");
work();
} else {
dispatch_async(queue, work);
}
}

@end
#endif

0 comments on commit af99ff5

Please sign in to comment.