TwitterImagePipeline/Project/TIPImageDownloader.m (817 lines of code) (raw):
//
// TIPImageDownloader.m
// TwitterImagePipeline
//
// Created on 3/3/15.
// Copyright (c) 2015 Twitter, Inc. All rights reserved.
//
#import "NSDictionary+TIPAdditions.h"
#import "TIP_Project.h"
#import "TIPError.h"
#import "TIPGlobalConfiguration+Project.h"
#import "TIPImageDownloader.h"
#import "TIPImageDownloadInternalContext.h"
#import "TIPImageFetchDownload.h"
#import "TIPTiming.h"
NS_ASSUME_NONNULL_BEGIN
#ifndef TIP_LOG_DOWNLOAD_PROGRESS
#define TIP_LOG_DOWNLOAD_PROGRESS 0
#endif
NSString * const TIPImageDownloaderCancelSource = @"Image Fetch Cancelled";
static const char *kTIPImageDownloaderQueueName = "com.twitter.tip.downloader.queue";
#define TIPAssertDownloaderQueue() \
do { \
if (!gTwitterImagePipelineAssertEnabled) { \
break; \
} \
const char *__currentLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); \
if (!__currentLabel || 0 != strcmp(__currentLabel, kTIPImageDownloaderQueueName)) { \
NSString *__assert_fn__ = @(__PRETTY_FUNCTION__); \
__assert_fn__ = __assert_fn__ ? __assert_fn__ : @"<Unknown Function>"; \
NSString *__assert_file__ = @(__FILE__); \
__assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
[[NSAssertionHandler currentHandler] handleFailureInFunction:__assert_fn__ \
file:__assert_file__ \
lineNumber:__LINE__ \
description:@"%s did not match expected GCD queue name: %s", __currentLabel ?: "<null>", kTIPImageDownloaderQueueName]; \
} \
} while (0)
static long long _ExpectedResponseBodySize(NSHTTPURLResponse * __nullable URLResponse);
static long long _ExpectedResponseBodySize(NSHTTPURLResponse * __nullable URLResponse)
{
long long contentLength = 0;
if (URLResponse) {
contentLength = URLResponse.expectedContentLength;
if (contentLength <= 0) {
contentLength = [[URLResponse.allHeaderFields tip_objectForCaseInsensitiveKey:@"Content-Length"] longLongValue];
}
}
return contentLength;
}
static BOOL _ImageDownloadIsComplete(NSHTTPURLResponse * __nullable response,
NSError * __nullable error);
static BOOL _ImageDownloadIsComplete(NSHTTPURLResponse * __nullable response,
NSError * __nullable error)
{
if (response.statusCode == 200 /* OK */ || response.statusCode == 206 /* Partial Content */) {
if (!error) {
return YES;
}
}
return NO;
}
static NSString *_ImageDownloadLastModifiedString(NSHTTPURLResponse * __nullable response,
NSError * __nullable error);
static NSString *_ImageDownloadLastModifiedString(NSHTTPURLResponse * __nullable response,
NSError * __nullable error)
{
if (response.statusCode == 200 /* OK */ || response.statusCode == 206 /* Partial Content */) {
if (error) {
// can only support partial downloads if Accept-Ranges is "bytes" and Last-Modified is present
NSString *lastModified = [response.allHeaderFields tip_objectForCaseInsensitiveKey:@"Last-Modified"];
if (lastModified) {
NSString *acceptRanges = [response.allHeaderFields tip_objectForCaseInsensitiveKey:@"Accept-Ranges"];
if ([acceptRanges compare:@"bytes" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
return lastModified;
}
}
}
}
return nil;
}
static void _ImageDownloadSetProgressStateFailureAndCancel(TIPImageDownloadInternalContext *context,
TIPImageFetchErrorCode code,
id<TIPImageFetchDownload> __nullable download);
static void _ImageDownloadSetProgressStateFailureAndCancel(TIPImageDownloadInternalContext *context,
TIPImageFetchErrorCode code,
id<TIPImageFetchDownload> __nullable download)
{
TIPAssertDownloaderQueue();
TIPAssert(context);
NSString *cancelDescription = nil;
switch (code) {
case TIPImageFetchErrorCodeDownloadEncounteredToStartMoreThanOnce:
cancelDescription = @"download started more than once";
break;
case TIPImageFetchErrorCodeDownloadAttemptedToHydrateRequestMoreThanOnce:
cancelDescription = @"download hydrated more than once";
break;
case TIPImageFetchErrorCodeDownloadReceivedResponseMoreThanOnce:
cancelDescription = @"download received response more than once";
break;
case TIPImageFetchErrorCodeDownloadNeverStarted:
cancelDescription = @"download wasn't started before download callbacks happened";
break;
case TIPImageFetchErrorCodeDownloadNeverAttemptedToHydrateRequest:
cancelDescription = @"download wasn't hydrated before downloading";
break;
case TIPImageFetchErrorCodeDownloadNeverReceivedResponse:
cancelDescription = @"download didn't receive a response before receiving data or completing";
break;
default:
cancelDescription = @"encountered error downloading";
break;
}
context->_progressStateError = [NSError errorWithDomain:TIPImageFetchErrorDomain
code:code
userInfo:nil];
[download cancelWithDescription:[NSString stringWithFormat:@"TIP: %@ %@", download, cancelDescription]];
}
static BOOL _CanCoalesceDelegate(NSObject<TIPImageDownloadDelegate> *delegate,
TIPImageDownloadInternalContext *context);
static BOOL _CanCoalesceDelegate(NSObject<TIPImageDownloadDelegate> *delegate,
TIPImageDownloadInternalContext *context)
{
TIPAssertDownloaderQueue();
id<TIPImageDownloadRequest> request = delegate.imageDownloadRequest;
id<TIPImageDownloadDelegate> otherDelegate = context.firstDelegate;
id<TIPImageDownloadRequest> otherRequest = otherDelegate.imageDownloadRequest;
if (![otherRequest.imageDownloadURL isEqual:request.imageDownloadURL]) {
return NO;
}
if (![otherRequest.imageDownloadIdentifier isEqual:request.imageDownloadIdentifier]) {
return NO;
}
if (otherRequest.imageDownloadHydrationBlock != request.imageDownloadHydrationBlock) {
return NO;
}
if (otherRequest.imageDownloadAuthorizationBlock != request.imageDownloadAuthorizationBlock) {
return NO;
}
if (delegate.imagePipeline != otherDelegate.imagePipeline) {
return NO;
}
return YES;
}
@interface TIPImageDownloader () <TIPImageFetchDownloadClient>
@end
TIP_OBJC_DIRECT_MEMBERS
@interface TIPImageDownloader (Background)
- (void)_background_dequeuePendingDownloads;
- (id<TIPImageFetchDownload>)_background_getOrCreateDownload:(NSObject<TIPImageDownloadDelegate> *)delegate;
- (void)_background_clearDownload:(id<TIPImageFetchDownload>)download;
- (void)_background_updatePriorityOfDownload:(id<TIPImageFetchDownload>)download;
- (void)_background_removeDelegate:(NSObject<TIPImageDownloadDelegate> *)delegate
fromDownload:(id<TIPImageFetchDownload>)download;
@end
@implementation TIPImageDownloader
{
dispatch_queue_t _downloaderQueue;
NSMutableDictionary<NSURL *, NSMutableArray<id<TIPImageFetchDownload>> *> *_constructedDownloads;
NSMutableArray<id<TIPImageFetchDownload>> *_pendingDownloads;
NSUInteger _runningDownloadsCount;
}
+ (instancetype)sharedInstance
{
static TIPImageDownloader *sDownloader;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sDownloader = [[TIPImageDownloader alloc] initInternal];
});
return sDownloader;
}
- (instancetype)initInternal
{
self = [super init];
if (self) {
_downloaderQueue = dispatch_queue_create(kTIPImageDownloaderQueueName, DISPATCH_QUEUE_SERIAL);
_constructedDownloads = [NSMutableDictionary dictionary];
_pendingDownloads = [NSMutableArray array];
}
return self;
}
- (id<TIPImageDownloadContext>)fetchImageWithDownloadDelegate:(id<TIPImageDownloadDelegate>)delegate
{
__block id<TIPImageFetchDownload> download = nil;
dispatch_sync(_downloaderQueue, ^{
download = [self _background_getOrCreateDownload:delegate];
});
return (id<TIPImageDownloadContext>)download;
}
- (void)removeDelegate:(id<TIPImageDownloadDelegate>)delegate
forContext:(id<TIPImageDownloadContext>)context
{
if (!context) {
return;
}
tip_dispatch_async_autoreleasing(_downloaderQueue, ^{
[self _background_removeDelegate:delegate
fromDownload:(id<TIPImageFetchDownload>)context];
});
}
- (void)updatePriorityOfContext:(id<TIPImageDownloadContext>)context
{
if (!context) {
return;
}
tip_dispatch_async_autoreleasing(_downloaderQueue, ^{
[self _background_updatePriorityOfDownload:(id<TIPImageFetchDownload>)context];
});
}
#pragma mark Delegate
- (void)imageFetchDownloadDidStart:(id<TIPImageFetchDownload>)download
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
return;
}
if (context->_flags.didStart) {
_ImageDownloadSetProgressStateFailureAndCancel(context, TIPImageFetchErrorCodeDownloadEncounteredToStartMoreThanOnce, download);
return;
}
context->_flags.didStart = YES;
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - starting", context.originalRequest.URL, download);
#endif
[context executePerDelegateSuspendingQueue:_downloaderQueue
block:^(id<TIPImageDownloadDelegate> delegate) {
[delegate imageDownloadDidStart:(id)download];
}];
}
}
- (void)imageFetchDownload:(id<TIPImageFetchDownload>)download
didReceiveURLResponse:(NSHTTPURLResponse *)response
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
return;
}
if (context->_flags.didReceiveResponse) {
_ImageDownloadSetProgressStateFailureAndCancel(context, TIPImageFetchErrorCodeDownloadReceivedResponseMoreThanOnce, download);
return;
}
context->_flags.didReceiveResponse = YES;
context->_response = response;
context->_contentLength = (NSUInteger)MAX(0LL, _ExpectedResponseBodySize(response));
if (!context->_flags.didRequestAuthorization) {
_ImageDownloadSetProgressStateFailureAndCancel(context, TIPImageFetchErrorCodeDownloadNeverAttemptedToAuthorizeRequest, download);
return;
}
TIPAssert(context.hydratedRequest);
if (200 /* OK */ == context->_response.statusCode) {
TIPPartialImage *partialImage = context->_partialImage;
// reset the resuming info
context->_partialImage = nil;
context->_temporaryFile = nil;
context->_lastModified = nil;
[context executePerDelegateSuspendingQueue:_downloaderQueue
block:^(id<TIPImageDownloadDelegate> delegate) {
[delegate imageDownload:(id)download didResetFromPartialImage:partialImage];
}];
} else if (206 /* Partial Content */ == context->_response.statusCode) {
TIPLogDebug(@"Did resume download of image at URL: %@", context.originalRequest.URL);
if ((context->_contentLength + context->_partialImage.byteCount) != context->_partialImage.expectedContentLength) {
TIPLogWarning(@"Continued partial image expected Content-Lenght (%tu) does not match recalculated expected Content-Length (%tu)", context->_partialImage.expectedContentLength, context->_contentLength + context->_partialImage.byteCount);
}
context->_contentLength = context->_partialImage.expectedContentLength;
} else {
// Failure status code, image is not going to load
context->_flags.responseStatusCodeIsFailure = YES;
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - got response (Content-Length: %tu)", context.originalRequest.URL, download, context.contentLength);
#endif
}
}
- (void)imageFetchDownload:(id<TIPImageFetchDownload>)download
didReceiveData:(NSData *)data
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
return;
}
TIPImageDecoderAppendResult result = TIPImageDecoderAppendResultDidProgress;
const NSUInteger byteCount = data.length;
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - downloaded %tu bytes", context.originalRequest.URL, download, byteCount);
#endif
if (!context->_flags.didReceiveResponse) {
_ImageDownloadSetProgressStateFailureAndCancel(context,
TIPImageFetchErrorCodeDownloadNeverReceivedResponse,
download);
return;
}
if (context->_flags.responseStatusCodeIsFailure) {
// data is for a failure, don't capture
return;
}
// Prep
if (!context->_flags.didReceiveData) {
context->_flags.didReceiveData = YES;
if (!context->_temporaryFile) {
context->_temporaryFile = [context.firstDelegate regenerateImageDownloadTemporaryFileForImageDownload:(id)download];
}
}
if (!context->_partialImage) {
context->_partialImage = [[TIPPartialImage alloc] initWithExpectedContentLength:context->_contentLength];
if (context->_decoderConfigMap) {
[context->_partialImage updateDecoderConfigMap:context->_decoderConfigMap];
}
}
// Update partial image
result = [context->_partialImage appendData:data final:NO];
// Update temporary file
[context->_temporaryFile appendData:data];
if (context.delegateCount > 0) {
TIPPartialImage *partialImage = context->_partialImage;
[context executePerDelegateSuspendingQueue:_downloaderQueue
block:^(id<TIPImageDownloadDelegate> delegate) {
[delegate imageDownload:(id)download
didAppendBytes:byteCount
toPartialImage:partialImage
result:result];
}];
} else {
// Running as a "detached" download, time to clean it up
[self _background_clearDownload:download];
[download cancelWithDescription:TIPImageDownloaderCancelSource];
}
}
}
- (void)imageFetchDownload:(id<TIPImageFetchDownload>)download
hydrateRequest:(NSURLRequest *)request
completion:(TIPImageFetchDownloadRequestHydrationCompleteBlock)complete
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
complete(nil);
return;
}
if (context->_flags.didRequestHydration) {
_ImageDownloadSetProgressStateFailureAndCancel(context, TIPImageFetchErrorCodeDownloadAttemptedToHydrateRequestMoreThanOnce, download);
return;
}
context->_flags.didRequestHydration = YES;
if (!context->_flags.didStart) {
_ImageDownloadSetProgressStateFailureAndCancel(context,
TIPImageFetchErrorCodeDownloadNeverStarted,
download);
return;
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - hydrating", context.originalRequest.URL, download);
#endif
void(^internalResumeBlock)(NSUInteger, NSString *, NSURLRequest *) = ^(NSUInteger alreadyDownloadedBytes, NSString *lastModified, NSURLRequest *requestToSend) {
if (alreadyDownloadedBytes > 0 && lastModified.length > 0) {
NSMutableURLRequest *mURLRequst = [requestToSend mutableCopy];
if (mURLRequst) {
[mURLRequst setValue:[NSString stringWithFormat:@"bytes=%tu-", alreadyDownloadedBytes] forHTTPHeaderField:@"Range"];
[mURLRequst setValue:lastModified forHTTPHeaderField:@"If-Range"];
requestToSend = mURLRequst;
}
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - did hydrate", context.originalRequest.URL, download);
#endif
context.hydratedRequest = [requestToSend copy];
TIPAssert(context.hydratedRequest != nil);
complete(nil);
};
// Pull out contextual values since accessing the context object from another thread is unsafe
NSUInteger partialImageByteCount = context->_partialImage.byteCount;
NSString *lastModified = context->_lastModified;
TIPImageFetchHydrationCompletionBlock hydrateBlock = ^(NSURLRequest * __nullable hydratedRequest, NSError * __nullable error) {
if (error) {
complete(error);
return;
}
if (!hydratedRequest) {
hydratedRequest = request;
}
TIPAssert([request.URL isEqual:hydratedRequest.URL]);
if (![request.URL isEqual:hydratedRequest.URL]) {
complete([NSError errorWithDomain:TIPImageFetchErrorDomain
code:TIPImageFetchErrorCodeIllegalModificationByHydrationBlock
userInfo:@{ @"originalURL" : request.URL,
@"modifiedURL" : hydratedRequest.URL }]);
return;
}
TIPAssert([request.HTTPMethod isEqualToString:hydratedRequest.HTTPMethod]);
if (![request.HTTPMethod isEqualToString:hydratedRequest.HTTPMethod]) {
complete([NSError errorWithDomain:TIPImageFetchErrorDomain
code:TIPImageFetchErrorCodeIllegalModificationByHydrationBlock
userInfo:@{ @"originalHTTPMethod" : request.HTTPMethod ?: [NSNull null],
@"modifiedHTTPMethod" : hydratedRequest.HTTPMethod ?: [NSNull null] }]);
return;
}
internalResumeBlock(partialImageByteCount, lastModified, hydratedRequest);
};
id<TIPImageDownloadDelegate> delegate = context.firstDelegate;
dispatch_queue_t delegateQueue = delegate.imageDownloadDelegateQueue;
TIPImageFetchHydrationBlock hydrationBlock = delegate.imageDownloadRequest.imageDownloadHydrationBlock;
if (hydrationBlock) {
if (delegateQueue) {
tip_dispatch_async_autoreleasing(delegateQueue, ^{
hydrationBlock(request, context, hydrateBlock);
});
} else {
hydrationBlock(request, context, hydrateBlock);
}
} else {
hydrateBlock(nil, nil);
}
}
}
- (void)imageFetchDownload:(id<TIPImageFetchDownload>)download
authorizeRequest:(NSURLRequest *)request
completion:(TIPImageFetchDownloadRequestAuthorizationCompleteBlock)complete
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
complete(nil);
return;
}
if (context->_flags.didRequestAuthorization) {
_ImageDownloadSetProgressStateFailureAndCancel(context, TIPImageFetchErrorCodeDownloadAttemptedToAuthorizeRequestMoreThanOnce, download);
return;
}
context->_flags.didRequestAuthorization = YES;
if (!context->_flags.didRequestHydration) {
_ImageDownloadSetProgressStateFailureAndCancel(context,
TIPImageFetchErrorCodeDownloadNeverAttemptedToHydrateRequest,
download);
return;
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - authorizing", context.originalRequest.URL, download);
#endif
TIPImageFetchAuthorizationCompletionBlock authCompleteBlock = ^(NSString * __nullable authValue, NSError * __nullable error) {
if (error) {
complete(error);
return;
}
if (authValue) {
context.authorization = authValue;
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - did authorize", context.originalRequest.URL, download);
#endif
complete(nil);
};
id<TIPImageDownloadDelegate> delegate = context.firstDelegate;
dispatch_queue_t delegateQueue = delegate.imageDownloadDelegateQueue;
TIPImageFetchAuthorizationBlock authorizationBlock = delegate.imageDownloadRequest.imageDownloadAuthorizationBlock;
if (authorizationBlock) {
if (delegateQueue) {
tip_dispatch_async_autoreleasing(delegateQueue, ^{
authorizationBlock(request, context, authCompleteBlock);
});
} else {
authorizationBlock(request, context, authCompleteBlock);
}
} else {
authCompleteBlock(nil, nil);
}
}
}
- (void)imageFetchDownloadWillRetry:(id<TIPImageFetchDownload>)download
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
return;
}
if (context->_flags.didComplete || context->_progressStateError != nil) {
TIPLogError(@"Cannot retry a download after it has already completed! %@", download);
return;
}
if (context->_flags.didReceiveData) {
TIPLogError(@"Cannot retry a download if it has already appended data, need to start a new TIP fetch!");
_ImageDownloadSetProgressStateFailureAndCancel(context,
TIPImageFetchErrorCodeDownloadWantedToRetryAfterAlreadyLoadingData,
download);
return;
}
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - retrying", context.originalRequest.URL, download);
#endif
[context reset];
}
}
- (void)imageFetchDownload:(id<TIPImageFetchDownload>)download
didCompleteWithError:(nullable NSError *)error
{
TIPAssertDownloaderQueue();
@autoreleasepool {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
TIPAssert(context);
if (!context) {
return;
}
if (context->_flags.didComplete) {
TIPAssertMessage(NO, @"%@ completed more than once", download);
return;
}
context->_flags.didComplete = YES;
[self _background_clearDownload:download];
context->_download = nil;
[download discardContext];
#if TIP_LOG_DOWNLOAD_PROGRESS
TIPLogDebug(@"(%@)[%p] - finished %@", context.originalRequest.URL, download, error ?: @(context.response.statusCode));
#endif
if (!error && !context->_progressStateError && !context->_flags.didReceiveData && !context->_flags.responseStatusCodeIsFailure) {
_ImageDownloadSetProgressStateFailureAndCancel(context,
TIPImageFetchErrorCodeDownloadNeverReceivedResponse,
nil /* don't cancel */);
}
if (error) {
if (context->_response.statusCode == 200 || context->_response.statusCode == 206) {
if (context->_partialImage.expectedContentLength == context->_partialImage.byteCount && context->_partialImage.byteCount > 0) {
/**
Networking is hard :(
It is not unheard of for a service responding with the payload of an image to mistakenly
disconnect, timeout or indicate failure for the response after it has successfully delivered
the final payload byte.
To protect against needless image resumes when we already have all the data,
catch cases when the download provides an error but the image had loaded all data.
*/
// 1) report the problem
NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
info[@"error"] = error;
info[@"response"] = context->_response;
if (download.finalURLRequest != nil) {
info[@"request"] = download.finalURLRequest;
}
if ([download respondsToSelector:@selector(downloadMetrics)] && download.downloadMetrics) {
info[@"metrics"] = download.downloadMetrics;
}
[[TIPGlobalConfiguration sharedInstance] postProblem:TIPProblemImageDownloadedWithUnnecessaryError
userInfo:info];
// 2) clear the unnecessary error
error = nil;
}
}
}
if (!error) {
error = context->_progressStateError;
}
const BOOL isComplete = _ImageDownloadIsComplete(context->_response, error);
[context->_partialImage appendData:nil final:isComplete];
const BOOL didReadHeaders = (context->_partialImage.state > TIPPartialImageStateLoadingHeaders);
const BOOL complete = isComplete && didReadHeaders;
context->_lastModified = _ImageDownloadLastModifiedString(context->_response, error);
NSUInteger totalBytes = context->_partialImage.byteCount;
NSString *imageType = context->_partialImage.type;
NSData* finalData = (isComplete) ? context->_partialImage.data : nil;
id<TIPImageDownloadDelegate> firstDelegate = context.firstDelegate;
const NSUInteger delegateCount = context.delegateCount;
if (!didReadHeaders || (!complete && !context->_lastModified && !context->_partialImage.progressive)) {
// Abandon the partial image
context->_partialImage = nil;
context->_lastModified = nil;
}
NSTimeInterval imageRenderLatency = 0.0;
TIPImageContainer *image = nil;
if (complete && firstDelegate != nil) {
const uint64_t startMachTime = mach_absolute_time();
image = [context->_partialImage renderImageWithMode:TIPImageDecoderRenderModeFullFrameProgress
targetDimensions:(delegateCount == 1) ? firstDelegate.imageDownloadRequest.targetDimensions : CGSizeZero
targetContentMode:(delegateCount == 1) ? firstDelegate.imageDownloadRequest.targetContentMode : UIViewContentModeCenter
decoded:NO];
imageRenderLatency = TIPComputeDuration(startMachTime, mach_absolute_time());
}
if (context->_temporaryFile) {
if (context->_partialImage) {
TIPImageCacheEntryContext *imageContext = nil;
if (complete) {
imageContext = [[TIPCompleteImageEntryContext alloc] init];
} else {
imageContext = [[TIPPartialImageEntryContext alloc] init];
TIPPartialImageEntryContext *partialContext = (id)imageContext;
partialContext.lastModified = context->_lastModified;
partialContext.expectedContentLength = context->_partialImage.expectedContentLength;
}
imageContext.animated = context->_partialImage.animated;
id<TIPImageDownloadRequest> firstDelegateRequest = firstDelegate.imageDownloadRequest;
if (firstDelegateRequest != nil) {
TIPImageFetchOptions options = firstDelegateRequest.imageDownloadOptions;
imageContext.TTL = firstDelegateRequest.imageDownloadTTL;
imageContext.updateExpiryOnAccess = TIP_BITMASK_EXCLUDES_FLAGS(options, TIPImageFetchDoNotResetExpiryOnAccess);
imageContext.treatAsPlaceholder = TIP_BITMASK_HAS_SUBSET_FLAGS(options, TIPImageFetchTreatAsPlaceholder);
imageContext.URL = firstDelegateRequest.imageDownloadURL;
} else {
// Defaults for dealing with "detached" download
imageContext.TTL = TIPTimeToLiveDefault;
imageContext.updateExpiryOnAccess = NO;
imageContext.treatAsPlaceholder = NO;
imageContext.URL = context.originalRequest.URL;
}
if (imageContext.TTL <= 0.0) {
imageContext.TTL = TIPTimeToLiveDefault;
}
imageContext.dimensions = context->_partialImage.dimensions;
if (TIPSizeEqualToZero(imageContext.dimensions) && image) {
imageContext.dimensions = image.dimensions;
}
if (gTwitterImagePipelineAssertEnabled) {
// Complex assertion, break it down
if (!firstDelegate) {
// OK
} else {
NSString *contextTempIdentifier = context->_temporaryFile.imageIdentifier;
NSString *delegateDownloadIdentifier = firstDelegateRequest.imageDownloadIdentifier;
if (!contextTempIdentifier || !delegateDownloadIdentifier) {
// Not OK! Need to have identifiers!
TIPAssertNever();
} else if (![contextTempIdentifier isEqualToString:delegateDownloadIdentifier]) {
// Not OK! Identifiers need to match!
TIPAssertNever();
} else {
// OK
}
}
}
[context->_temporaryFile finalizeWithContext:imageContext];
}
context->_temporaryFile = nil;
if (complete) {
context->_partialImage = nil;
}
}
if (!firstDelegate) {
// Nothing left to do if we don't have a delegate
return;
}
if (!error && !image) {
if (context->_flags.responseStatusCodeIsFailure || 200 != ((context->_response.statusCode / 100) * 100)) {
error = [NSError errorWithDomain:TIPImageFetchErrorDomain
code:TIPImageFetchErrorCodeHTTPTransactionError
userInfo:@{ TIPErrorInfoHTTPStatusCodeKey : @(context->_response.statusCode) }];
} else if (isComplete) {
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
id value = nil;
value = context->_temporaryFile.imageIdentifier;
if (value) {
userInfo[TIPProblemInfoKeyImageIdentifier] = value;
}
value = context.originalRequest.URL;
if (value) {
userInfo[TIPProblemInfoKeyImageURL] = value;
}
value = download.finalURLRequest;
if (value) {
userInfo[@"finalRequest"] = value;
}
value = context->_response;
if (value) {
userInfo[@"response"] = value;
}
value = [download respondsToSelector:@selector(downloadMetrics)] ? [download downloadMetrics] : nil;
if (value) {
userInfo[@"metrics"] = value;
}
error = [NSError errorWithDomain:TIPImageFetchErrorDomain
code:TIPImageFetchErrorCodeCouldNotDecodeImage
userInfo:userInfo];
[[TIPGlobalConfiguration sharedInstance] postProblem:TIPProblemImageDownloadedCouldNotBeDecoded
userInfo:userInfo];
} else { // !response.isComplete
TIPAssertNever();
error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EBADEXEC
userInfo:nil];
}
}
TIPAssert((nil == error) ^ (nil == image));
// Pull out contextual values since accessing the context object from another thread is unsafe
TIPPartialImage *partialImage = context->_partialImage;
NSString *lastModified = context->_lastModified;
const NSInteger statusCode = context->_response.statusCode;
[context executePerDelegateSuspendingQueue:NULL block:^(id<TIPImageDownloadDelegate> delegateInner) {
// only the first delegate is granted the honor of caching the partial image
const BOOL isFirstDelegate = (delegateInner == firstDelegate);
[delegateInner imageDownload:(id)download
didCompleteWithPartialImage:((isFirstDelegate) ? partialImage : nil)
lastModified:((isFirstDelegate) ? lastModified : nil)
byteSize:((isFirstDelegate) ? totalBytes : 0)
imageType:((isFirstDelegate) ? imageType : nil)
image:image
imageData:((isFirstDelegate) ? finalData : nil)
imageRenderLatency:imageRenderLatency
statusCode:statusCode
error:error];
}];
}
}
@end
#pragma mark Background
@implementation TIPImageDownloader (Background)
- (void)_background_dequeuePendingDownloads
{
TIPAssertDownloaderQueue();
// Cast signed max value to unsigned making negative values (infinite) be HUGE (and effectively infinite)
const NSUInteger count = (NSUInteger)[TIPGlobalConfiguration sharedInstance].maxConcurrentImagePipelineDownloadCount;
while (_runningDownloadsCount < count && _pendingDownloads.count > 0) {
id<TIPImageFetchDownload> download = _pendingDownloads.firstObject;
[_pendingDownloads removeObjectAtIndex:0];
_runningDownloadsCount++;
[download start];
}
}
- (void)_background_updatePriorityOfDownload:(id<TIPImageFetchDownload>)download
{
TIPAssertDownloaderQueue();
if ([download respondsToSelector:@selector(setPriority:)]) {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
NSOperationQueuePriority priority = [context downloadPriority];
download.priority = priority;
}
}
- (void)_background_removeDelegate:(NSObject<TIPImageDownloadDelegate> *)delegate
fromDownload:(id<TIPImageFetchDownload>)download
{
TIPAssertDownloaderQueue();
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
// Multiple delegates?
if (context.delegateCount > 1) {
// Just remove the delegate
[context removeDelegate:delegate];
return;
}
// Is it a known delegate?
if (![context containsDelegate:delegate]) {
// Unknown delegate, just no-op
return;
}
[self _background_clearDownload:download];
[download cancelWithDescription:TIPImageDownloaderCancelSource];
TIPLogInformation(@"Download[%p] has no more delegates and is below the acceptable download speed, cancelling", download);
}
- (void)_background_clearDownload:(id<TIPImageFetchDownload>)download
{
TIPAssertDownloaderQueue();
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)download.context;
NSURL *URL = context.originalRequest.URL;
NSMutableArray<id<TIPImageFetchDownload>> *downloads = _constructedDownloads[URL];
if (downloads) {
BOOL downloadFound = NO;
NSUInteger count = downloads.count;
NSUInteger index = [downloads indexOfObjectIdenticalTo:download];
if (index < count) {
[downloads removeObjectAtIndex:index];
count--;
downloadFound = YES;
}
TIPAssert(downloads.count == count);
if (!count) {
[_constructedDownloads removeObjectForKey:URL];
}
if (downloadFound) {
index = [_pendingDownloads indexOfObjectIdenticalTo:download];
if (index == NSNotFound) {
TIPAssert(_runningDownloadsCount > 0);
_runningDownloadsCount--;
[self _background_dequeuePendingDownloads];
} else {
[_pendingDownloads removeObjectAtIndex:index];
}
}
}
}
- (id<TIPImageFetchDownload>)_background_getOrCreateDownload:(NSObject<TIPImageDownloadDelegate> *)delegate
{
TIPAssertDownloaderQueue();
id<TIPImageFetchDownload> download = nil;
NSObject<TIPImageDownloadRequest> *request = delegate.imageDownloadRequest;
NSURL *URL = request.imageDownloadURL;
NSMutableArray<id<TIPImageFetchDownload>> *constructedDownloads = _constructedDownloads[URL];
// Coalesce if possible
for (id<TIPImageFetchDownload> existingDownload in constructedDownloads) {
TIPImageDownloadInternalContext *context = (TIPImageDownloadInternalContext *)existingDownload.context;
if (_CanCoalesceDelegate(delegate, context)) {
TIPLogDebug(@"Coalescing two requests for the same image: ('%@' ==> '%@')", request.imageDownloadIdentifier, request.imageDownloadURL);
download = existingDownload;
[context addDelegate:delegate];
if (context->_partialImage) {
// Prepopulate with progress (if available/possible)
TIPImageDecoderAppendResult result = TIPImageDecoderAppendResultDidProgress;
if (context->_partialImage.frameCount > 0) {
result = TIPImageDecoderAppendResultDidLoadFrame;
} else if (context->_partialImage.state > TIPPartialImageStateLoadingHeaders) {
result = TIPImageDecoderAppendResultDidLoadHeaders;
}
// Pull out contextual values since accessing the context object from another thread is unsafe
TIPPartialImage *partialImage = context->_partialImage;
const BOOL didStart = context->_flags.didStart;
const BOOL didReceiveFirstByte = context->_flags.didReceiveData;
NSInteger statusCode = context->_response.statusCode;
[TIPImageDownloadInternalContext executeDelegate:delegate
suspendingQueue:_downloaderQueue
block:^(id<TIPImageDownloadDelegate> blockDelegate) {
if (200 /* OK */ == statusCode) {
// already started a fresh download, reset
[blockDelegate imageDownload:(id)download
didResetFromPartialImage:partialImage];
}
if (didStart) {
// already started receiving data, catch the delegate up to speed
[blockDelegate imageDownloadDidStart:(id)download];
if (didReceiveFirstByte) {
// already started receiving data, catch the delegate up to speed
[blockDelegate imageDownload:(id)download
didAppendBytes:partialImage.byteCount
toPartialImage:partialImage
result:result];
}
}
}];
}
}
}
// Create a new operation if necessary
if (!download) {
TIPImageDownloadInternalContext *context = [[TIPImageDownloadInternalContext alloc] init];
context->_lastModified = request.imageDownloadLastModified;
context->_partialImage = request.imageDownloadPartialImageForResuming;
context->_temporaryFile = request.imageDownloadTemporaryFileForResuming;
context->_decoderConfigMap = request.decoderConfigMap;
[context addDelegate:delegate];
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL];
URLRequest.allHTTPHeaderFields = request.imageDownloadHeaders;
context.originalRequest = URLRequest;
context.downloadQueue = _downloaderQueue;
context.client = self;
download = [[TIPGlobalConfiguration sharedInstance] createImageFetchDownloadWithContext:context];
context->_download = download;
if (!constructedDownloads) {
constructedDownloads = [[NSMutableArray alloc] init];
_constructedDownloads[URL] = constructedDownloads;
}
[constructedDownloads addObject:download];
[_pendingDownloads addObject:download];
[self _background_dequeuePendingDownloads];
}
[self _background_updatePriorityOfDownload:download];
return download;
}
@end
NS_ASSUME_NONNULL_END