mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-08 21:49:41 +00:00
If previously-displayed contents is gone (e.g. clearContents), and is not finished displaying by the time the node is onscreen, recreate the placeholder immediately.
465 lines
15 KiB
Plaintext
Executable File
465 lines
15 KiB
Plaintext
Executable File
/* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "ASNetworkImageNode.h"
|
|
|
|
#import "ASBasicImageDownloader.h"
|
|
#import "ASDisplayNode+Subclasses.h"
|
|
#import "ASDisplayNode+FrameworkPrivate.h"
|
|
#import "ASEqualityHelpers.h"
|
|
#import "ASThread.h"
|
|
#import "ASInternalHelpers.h"
|
|
|
|
#if PIN_REMOTE_IMAGE
|
|
#import "ASPINRemoteImageDownloader.h"
|
|
#endif
|
|
|
|
static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
|
|
|
@interface ASNetworkImageNode ()
|
|
{
|
|
ASDN::RecursiveMutex _lock;
|
|
__weak id<ASImageCacheProtocol, ASImageCacheProtocolDeprecated> _cache;
|
|
__weak id<ASImageDownloaderProtocol, ASImageDownloaderProtocolDeprecated> _downloader;
|
|
|
|
// Only access any of these with _lock.
|
|
__weak id<ASNetworkImageNodeDelegate> _delegate;
|
|
|
|
NSURL *_URL;
|
|
UIImage *_defaultImage;
|
|
|
|
NSUUID *_cacheUUID;
|
|
id _downloadIdentifier;
|
|
|
|
BOOL _imageLoaded;
|
|
|
|
BOOL _delegateSupportsDidStartFetchingData;
|
|
BOOL _delegateSupportsDidFailWithError;
|
|
BOOL _delegateSupportsImageNodeDidFinishDecoding;
|
|
|
|
//set on init only
|
|
BOOL _downloaderSupportsNewProtocol;
|
|
BOOL _downloaderImplementsSetProgress;
|
|
BOOL _downloaderImplementsSetPriority;
|
|
|
|
BOOL _cacheSupportsNewProtocol;
|
|
BOOL _cacheSupportsClearing;
|
|
BOOL _cacheSupportsSynchronousFetch;
|
|
}
|
|
@end
|
|
|
|
@implementation ASNetworkImageNode
|
|
|
|
- (instancetype)initWithCache:(id<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader
|
|
{
|
|
if (!(self = [super init]))
|
|
return nil;
|
|
|
|
_cache = (id<ASImageCacheProtocol, ASImageCacheProtocolDeprecated>)cache;
|
|
_downloader = (id<ASImageDownloaderProtocol, ASImageDownloaderProtocolDeprecated>)downloader;
|
|
|
|
ASDisplayNodeAssert([downloader respondsToSelector:@selector(downloadImageWithURL:callbackQueue:downloadProgress:completion:)] || [downloader respondsToSelector:@selector(downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:)], @"downloader must respond to either downloadImageWithURL:callbackQueue:downloadProgress:completion: or downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:.");
|
|
|
|
_downloaderSupportsNewProtocol = [downloader respondsToSelector:@selector(downloadImageWithURL:callbackQueue:downloadProgress:completion:)];
|
|
|
|
ASDisplayNodeAssert(cache == nil || [cache respondsToSelector:@selector(cachedImageWithURL:callbackQueue:completion:)] || [cache respondsToSelector:@selector(fetchCachedImageWithURL:callbackQueue:completion:)], @"cacher must respond to either cachedImageWithURL:callbackQueue:completion: or fetchCachedImageWithURL:callbackQueue:completion:");
|
|
|
|
_downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)];
|
|
_downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)];
|
|
|
|
_cacheSupportsNewProtocol = [cache respondsToSelector:@selector(cachedImageWithURL:callbackQueue:completion:)];
|
|
_cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)];
|
|
_cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)];
|
|
|
|
_shouldCacheImage = YES;
|
|
self.shouldBypassEnsureDisplay = YES;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
#if PIN_REMOTE_IMAGE
|
|
return [self initWithCache:[ASPINRemoteImageDownloader sharedDownloader] downloader:[ASPINRemoteImageDownloader sharedDownloader]];
|
|
#else
|
|
return [self initWithCache:nil downloader:[ASBasicImageDownloader sharedImageDownloader]];
|
|
#endif
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self _cancelImageDownload];
|
|
}
|
|
|
|
#pragma mark - Public methods -- must lock
|
|
|
|
- (void)setURL:(NSURL *)URL
|
|
{
|
|
[self setURL:URL resetToDefault:YES];
|
|
}
|
|
|
|
- (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
|
|
if (ASObjectIsEqual(URL, _URL)) {
|
|
return;
|
|
}
|
|
|
|
[self _cancelImageDownload];
|
|
_imageLoaded = NO;
|
|
|
|
_URL = URL;
|
|
|
|
if (reset || _URL == nil)
|
|
self.image = _defaultImage;
|
|
|
|
if (self.interfaceState & ASInterfaceStateFetchData) {
|
|
[self fetchData];
|
|
}
|
|
}
|
|
|
|
- (NSURL *)URL
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
return _URL;
|
|
}
|
|
|
|
- (void)setDefaultImage:(UIImage *)defaultImage
|
|
{
|
|
_lock.lock();
|
|
|
|
if (ASObjectIsEqual(defaultImage, _defaultImage)) {
|
|
_lock.unlock();
|
|
return;
|
|
}
|
|
_defaultImage = defaultImage;
|
|
|
|
if (!_imageLoaded) {
|
|
_lock.unlock();
|
|
// Locking: it is important to release _lock before entering setImage:, as it needs to release the lock before -invalidateCalculatedLayout.
|
|
// If we continue to hold the lock here, it will still be locked until the next unlock() call, causing a possible deadlock with
|
|
// -[ASNetworkImageNode displayWillStart] (which is called on a different thread / main, at an unpredictable time due to ASMainRunloopQueue).
|
|
self.image = defaultImage;
|
|
} else {
|
|
_lock.unlock();
|
|
}
|
|
}
|
|
|
|
- (UIImage *)defaultImage
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
return _defaultImage;
|
|
}
|
|
|
|
- (void)setDelegate:(id<ASNetworkImageNodeDelegate>)delegate
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
_delegate = delegate;
|
|
|
|
_delegateSupportsDidStartFetchingData = [delegate respondsToSelector:@selector(imageNodeDidStartFetchingData:)];
|
|
_delegateSupportsDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)];
|
|
_delegateSupportsImageNodeDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)];
|
|
}
|
|
|
|
- (id<ASNetworkImageNodeDelegate>)delegate
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
return _delegate;
|
|
}
|
|
|
|
- (BOOL)placeholderShouldPersist
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
return (self.image == nil && _URL != nil);
|
|
}
|
|
|
|
/* displayWillStart in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary
|
|
in ASMultiplexImageNode as well. */
|
|
- (void)displayWillStart
|
|
{
|
|
[super displayWillStart];
|
|
|
|
if (_cacheSupportsSynchronousFetch) {
|
|
ASDN::MutexLocker l(_lock);
|
|
if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) {
|
|
UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL];
|
|
if (result) {
|
|
self.image = result;
|
|
_imageLoaded = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
[self fetchData];
|
|
|
|
if (self.image == nil && _downloaderImplementsSetPriority) {
|
|
ASDN::MutexLocker l(_lock);
|
|
if (_downloadIdentifier != nil) {
|
|
[_downloader setPriority:ASImageDownloaderPriorityImminent withDownloadIdentifier:_downloadIdentifier];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* visibilityDidChange in ASMultiplexImageNode has a very similar implementation. Changes here are likely necessary
|
|
in ASMultiplexImageNode as well. */
|
|
- (void)visibilityDidChange:(BOOL)isVisible
|
|
{
|
|
[super visibilityDidChange:isVisible];
|
|
|
|
if (_downloaderImplementsSetPriority) {
|
|
ASDN::MutexLocker l(_lock);
|
|
if (_downloadIdentifier != nil) {
|
|
if (isVisible) {
|
|
[_downloader setPriority:ASImageDownloaderPriorityVisible withDownloadIdentifier:_downloadIdentifier];
|
|
} else {
|
|
[_downloader setPriority:ASImageDownloaderPriorityPreload withDownloadIdentifier:_downloadIdentifier];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_downloaderImplementsSetProgress) {
|
|
ASDN::MutexLocker l(_lock);
|
|
|
|
if (_downloadIdentifier != nil) {
|
|
__weak __typeof__(self) weakSelf = self;
|
|
ASImageDownloaderProgressImage progress = nil;
|
|
if (isVisible) {
|
|
progress = ^(UIImage * _Nonnull progressImage, id _Nullable downloadIdentifier) {
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
|
|
ASDN::MutexLocker l(_lock);
|
|
//Getting a result back for a different download identifier, download must not have been successfully canceled
|
|
if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
|
|
return;
|
|
}
|
|
|
|
strongSelf.image = progressImage;
|
|
};
|
|
}
|
|
[_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)clearFetchedData
|
|
{
|
|
[super clearFetchedData];
|
|
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
|
|
[self _cancelImageDownload];
|
|
[self _clearImage];
|
|
if (_cacheSupportsClearing) {
|
|
[_cache clearFetchedImageFromCacheWithURL:_URL];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)fetchData
|
|
{
|
|
[super fetchData];
|
|
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
[self _lazilyLoadImageIfNecessary];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Private methods -- only call with lock.
|
|
|
|
- (void)_clearImage
|
|
{
|
|
// Destruction of bigger images on the main thread can be expensive
|
|
// and can take some time, so we dispatch onto a bg queue to
|
|
// actually dealloc.
|
|
__block UIImage *image = self.image;
|
|
CGSize imageSize = image.size;
|
|
BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
|
|
imageSize.height > kMinReleaseImageOnBackgroundSize.height;
|
|
if (shouldReleaseImageOnBackgroundThread) {
|
|
ASPerformBlockOnBackgroundThread(^{
|
|
image = nil;
|
|
});
|
|
}
|
|
self.image = _defaultImage;
|
|
_imageLoaded = NO;
|
|
}
|
|
|
|
- (void)_cancelImageDownload
|
|
{
|
|
if (!_downloadIdentifier) {
|
|
return;
|
|
}
|
|
|
|
if (_downloadIdentifier) {
|
|
[_downloader cancelImageDownloadForIdentifier:_downloadIdentifier];
|
|
}
|
|
_downloadIdentifier = nil;
|
|
|
|
_cacheUUID = nil;
|
|
}
|
|
|
|
- (void)_downloadImageWithCompletion:(void (^)(UIImage *image, NSError*, id downloadIdentifier))finished
|
|
{
|
|
ASPerformBlockOnBackgroundThread(^{
|
|
ASDN::MutexLocker l(_lock);
|
|
if (_downloaderSupportsNewProtocol) {
|
|
_downloadIdentifier = [_downloader downloadImageWithURL:_URL
|
|
callbackQueue:dispatch_get_main_queue()
|
|
downloadProgress:NULL
|
|
completion:^(UIImage * _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier) {
|
|
if (finished != NULL) {
|
|
finished(image, error, downloadIdentifier);
|
|
}
|
|
}];
|
|
} else {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
_downloadIdentifier = [_downloader downloadImageWithURL:_URL
|
|
callbackQueue:dispatch_get_main_queue()
|
|
downloadProgressBlock:NULL
|
|
completion:^(CGImageRef responseImage, NSError *error) {
|
|
if (finished != NULL) {
|
|
finished([UIImage imageWithCGImage:responseImage], error, nil);
|
|
}
|
|
}];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)_lazilyLoadImageIfNecessary
|
|
{
|
|
// FIXME: We should revisit locking in this method (e.g. to access the instance variables at the top, and holding lock while calling delegate)
|
|
if (!_imageLoaded && _URL != nil && _downloadIdentifier == nil) {
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
if (_delegateSupportsDidStartFetchingData) {
|
|
[_delegate imageNodeDidStartFetchingData:self];
|
|
}
|
|
}
|
|
|
|
if (_URL.isFileURL) {
|
|
{
|
|
ASDN::MutexLocker l(_lock);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (self.shouldCacheImage) {
|
|
self.image = [UIImage imageNamed:_URL.path.lastPathComponent];
|
|
} else {
|
|
// First try to load the path directly, for efficiency assuming a developer who
|
|
// doesn't want caching is trying to be as minimal as possible.
|
|
self.image = [UIImage imageWithContentsOfFile:_URL.path];
|
|
if (!self.image) {
|
|
// If we couldn't find it, execute an -imageNamed:-like search so we can find resources even if the
|
|
// extension is not provided in the path. This allows the same path to work regardless of shouldCacheImage.
|
|
NSString *filename = [[NSBundle mainBundle] pathForResource:_URL.path.lastPathComponent ofType:nil];
|
|
if (filename) {
|
|
self.image = [UIImage imageWithContentsOfFile:filename];
|
|
}
|
|
}
|
|
}
|
|
|
|
_imageLoaded = YES;
|
|
[_delegate imageNode:self didLoadImage:self.image];
|
|
});
|
|
}
|
|
} else {
|
|
__weak __typeof__(self) weakSelf = self;
|
|
void (^finished)(UIImage *, NSError *, id downloadIdentifier) = ^(UIImage *responseImage, NSError *error, id downloadIdentifier) {
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
ASDN::MutexLocker l(strongSelf->_lock);
|
|
|
|
//Getting a result back for a different download identifier, download must not have been successfully canceled
|
|
if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
|
|
return;
|
|
}
|
|
|
|
if (responseImage != NULL) {
|
|
strongSelf->_imageLoaded = YES;
|
|
strongSelf.image = responseImage;
|
|
}
|
|
|
|
strongSelf->_downloadIdentifier = nil;
|
|
|
|
strongSelf->_cacheUUID = nil;
|
|
}
|
|
|
|
{
|
|
ASDN::MutexLocker l(strongSelf->_lock);
|
|
if (responseImage != NULL) {
|
|
[strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image];
|
|
}
|
|
else if (error && _delegateSupportsDidFailWithError) {
|
|
[strongSelf->_delegate imageNode:strongSelf didFailWithError:error];
|
|
}
|
|
}
|
|
};
|
|
|
|
if (_cache != nil) {
|
|
NSUUID *cacheUUID = [NSUUID UUID];
|
|
_cacheUUID = cacheUUID;
|
|
|
|
void (^cacheCompletion)(UIImage *) = ^(UIImage *image) {
|
|
// If the cache UUID changed, that means this request was cancelled.
|
|
if (![_cacheUUID isEqual:cacheUUID]) {
|
|
return;
|
|
}
|
|
|
|
if (image == NULL && _downloader != nil) {
|
|
[self _downloadImageWithCompletion:finished];
|
|
} else {
|
|
finished(image, NULL, nil);
|
|
}
|
|
};
|
|
|
|
if (_cacheSupportsNewProtocol) {
|
|
[_cache cachedImageWithURL:_URL
|
|
callbackQueue:dispatch_get_main_queue()
|
|
completion:cacheCompletion];
|
|
} else {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_cache fetchCachedImageWithURL:_URL
|
|
callbackQueue:dispatch_get_main_queue()
|
|
completion:^(CGImageRef image) {
|
|
cacheCompletion([UIImage imageWithCGImage:image]);
|
|
}];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
} else {
|
|
[self _downloadImageWithCompletion:finished];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - ASDisplayNode+Subclasses
|
|
|
|
- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange
|
|
{
|
|
if (self.asyncdisplaykit_asyncTransactionContainerState == ASAsyncTransactionContainerStateNoTransactions) {
|
|
ASDN::MutexLocker l(_lock);
|
|
if (self.layer.contents != nil && _delegateSupportsImageNodeDidFinishDecoding) {
|
|
[self.delegate imageNodeDidFinishDecoding:self];
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|