TwitterImagePipeline/TIPImageFetchOperation.m (2,083 lines of code) (raw):

// // TIPImageFetchOperation.m // TwitterImagePipeline // // Created on 3/6/15. // Copyright (c) 2015 Twitter. All rights reserved. // #include <objc/runtime.h> #include <stdatomic.h> #import "TIP_Project.h" #import "TIPError.h" #import "TIPGlobalConfiguration+Project.h" #import "TIPImageCacheEntry.h" #import "TIPImageDiskCache.h" #import "TIPImageDiskCacheTemporaryFile.h" #import "TIPImageDownloader.h" #import "TIPImageFetchDelegate.h" #import "TIPImageFetchDownload.h" #import "TIPImageFetchMetrics+Project.h" #import "TIPImageFetchOperation+Project.h" #import "TIPImageFetchProgressiveLoadingPolicies.h" #import "TIPImageFetchRequest.h" #import "TIPImageFetchTransformer.h" #import "TIPImageMemoryCache.h" #import "TIPImagePipeline+Project.h" #import "TIPImageRenderedCache.h" #import "TIPPartialImage.h" #import "TIPTiming.h" #import "UIImage+TIPAdditions.h" NSErrorDomain const TIPImageFetchErrorDomain = @"TIPImageFetchErrorDomain"; NSErrorDomain const TIPImageStoreErrorDomain = @"TIPImageStoreErrorDomain"; NSErrorDomain const TIPErrorDomain = @"TIPErrorDomain"; TIPErrorInfoKey TIPErrorInfoHTTPStatusCodeKey = @"httpStatusCode"; NS_ASSUME_NONNULL_BEGIN typedef void(^TIPBoolBlock)(BOOL boolVal); typedef void(^TIPImageFetchDelegateWorkBlock)(id<TIPImageFetchDelegate> __nullable delegate); static NSQualityOfService ConvertNSOperationQueuePriorityToQualityOfService(NSOperationQueuePriority pri); #if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64 #define TIPImageFetchOperationState_Unaligned_AtomicT volatile atomic_int_fast64_t #define TIPImageFetchOperationState_AtomicT TIPImageFetchOperationState_Unaligned_AtomicT __attribute__((aligned(8))) #else #define TIPImageFetchOperationState_Unaligned_AtomicT volatile atomic_int_fast32_t #define TIPImageFetchOperationState_AtomicT TIPImageFetchOperationState_Unaligned_AtomicT __attribute__((aligned(4))) #endif @interface TIPImageFetchDownloadRequest : NSObject <TIPImageDownloadRequest> // Populated on init @property (nonatomic, readonly, nullable) NSURL *imageDownloadURL; @property (nonatomic, copy, readonly, nullable) NSString *imageDownloadIdentifier; // Manually set (set once) @property (nonatomic, copy, nullable) NSDictionary<NSString *, NSString *> *imageDownloadHeaders; @property (nonatomic) TIPImageFetchOptions imageDownloadOptions; @property (nonatomic) NSTimeInterval imageDownloadTTL; @property (nonatomic, nullable, copy) TIPImageFetchHydrationBlock imageDownloadHydrationBlock; @property (nonatomic, nullable, copy) TIPImageFetchAuthorizationBlock imageDownloadAuthorizationBlock; @property (nonatomic, nullable, copy) NSDictionary<NSString *, id> *decoderConfigMap; @property (nonatomic) CGSize targetDimensions; @property (nonatomic) UIViewContentMode targetContentMode; // Manually set @property (atomic, nullable, copy) NSString *imageDownloadLastModified; @property (atomic, nullable) TIPPartialImage *imageDownloadPartialImageForResuming; @property (atomic, nullable) TIPImageDiskCacheTemporaryFile *imageDownloadTemporaryFileForResuming; @property (nonatomic) NSOperationQueuePriority imageDownloadPriority; // init - (instancetype)initWithRequest:(id<TIPImageFetchRequest>)fetchRequest; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end TIP_OBJC_FINAL TIP_OBJC_DIRECT_MEMBERS @interface TIPImageFetchDelegateDeallocHandler : NSObject - (instancetype)initWithFetchOperation:(TIPImageFetchOperation *)operation delegate:(id<TIPImageFetchDelegate>)delegate; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; - (void)invalidate; @end TIP_OBJC_FINAL TIP_OBJC_DIRECT_MEMBERS @interface TIPImageFetchOperationNetworkStepContext : NSObject @property (nonatomic, nullable) TIPImageFetchDownloadRequest *imageDownloadRequest; @property (nonatomic, nullable) id<TIPImageDownloadContext> imageDownloadContext; @end @interface TIPImageFetchResultInternal : NSObject <TIPImageFetchResult> + (nullable TIPImageFetchResultInternal *)resultWithImageContainer:(nullable TIPImageContainer *)imageContainer identifier:(nullable NSString *)identifier loadSource:(TIPImageLoadSource)source URL:(nullable NSURL *)URL originalDimensions:(CGSize)originalDimensions placeholder:(BOOL)placeholder transformed:(BOOL)transformed TIP_OBJC_DIRECT; @end @implementation TIPImageFetchOperationNetworkStepContext @end @interface TIPImageFetchOperation () <TIPImageDownloadDelegate> @property (atomic, nullable, weak) id<TIPImageFetchDelegate> delegate; #pragma twitter startignorestylecheck @property (tip_atomic_direct, nullable, strong) id<TIPImageFetchDelegate> strongDelegate; #pragma twitter endignorestylecheck @property (tip_atomic_direct, nullable, weak) TIPImageFetchDelegateDeallocHandler *delegateHandler; @property (nonatomic) TIPImageFetchOperationState state; @property (nonatomic) float progress; @property (tip_nonatomic_direct, nullable) NSError *operationError; @property (tip_nonatomic_direct, nullable) id<TIPImageFetchResult> previewResult; @property (tip_nonatomic_direct, nullable) id<TIPImageFetchResult> progressiveResult; @property (tip_nonatomic_direct, nullable) id<TIPImageFetchResult> finalResult; @property (tip_nonatomic_direct, nullable) TIPImageContainer *previewImageContainerRaw; @property (tip_nonatomic_direct, nullable) TIPImageContainer *finalImageContainerRaw; @property (nonatomic, nullable) NSError *error; @property (nonatomic, nullable, copy) NSString *networkLoadImageType; @property (nonatomic) CGSize networkImageOriginalDimensions; // Private - (void)_extractBasicRequestInfo TIP_OBJC_DIRECT; - (void)_initializeDelegate:(nullable id<TIPImageFetchDelegate>)delegate TIP_OBJC_DIRECT; - (void)_clearDelegateHandler TIP_OBJC_DIRECT; - (void)_hydrateNewContext:(TIPImageCacheEntryContext *)context imageURL:(NSURL *)imageURL placeholder:(BOOL)placeholder TIP_OBJC_DIRECT; @end TIP_OBJC_DIRECT_MEMBERS @interface TIPImageFetchOperation (Background) // Start/Abort - (void)_background_start; - (BOOL)_background_shouldAbort; // Generate State - (void)_background_extractObservers; - (void)_background_extractAdvancedRequestInfo; - (void)_background_extractTargetInfo; - (void)_background_extractStorageInfo; - (void)_background_validateProgressiveSupportWithPartialImage:(TIPPartialImage *)partialImage; - (void)_background_clearNetworkContextVariables; - (void)_background_setFinalStateAfterFlushingDelegate:(TIPImageFetchOperationState)state; // Load - (void)_background_dispatchLoadStarted:(TIPImageLoadSource)source; - (void)_background_loadFromNextSource; - (void)_background_loadFromMemory; - (void)_background_loadFromDisk; - (void)_background_loadFromOtherPipelineDisk; - (void)_background_loadFromAdditional; - (void)_background_loadFromNextAdditionalCache:(NSMutableArray<id<TIPImageAdditionalCache>> *)caches imageURL:(NSURL *)imageURL; - (void)_background_loadFromNetwork; // Update - (void)_background_updateFailureToLoadFinalImage:(NSError *)error updateMetrics:(BOOL)updateMetrics; - (void)_background_updateProgress:(float)progress; - (void)_background_updateFinalImage:(TIPImageContainer *)image imageData:(nullable NSData *)imageData renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL loadSource:(TIPImageLoadSource)source networkImageType:(nullable NSString *)networkImageType networkByteCount:(NSUInteger)networkByteCount placeholder:(BOOL)placeholder; - (void)_background_updatePreviewImageWithCacheEntry:(TIPImageCacheEntry *)cacheEntry loadSource:(TIPImageLoadSource)source; - (void)_background_updateProgressiveImage:(UIImage *)image transformed:(BOOL)transformed renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL progress:(float)progress sourcePartialImage:(TIPPartialImage *)sourcePartialImage loadSource:(TIPImageLoadSource)source; - (void)_background_updateFirstAnimatedImageFrame:(UIImage *)image renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL progress:(float)progress sourcePartialImage:(TIPPartialImage *)sourcePartialImage loadSource:(TIPImageLoadSource)source; - (void)_background_handleCompletedMemoryEntry:(TIPImageMemoryCacheEntry *)entry; - (void)_background_handlePartialMemoryEntry:(TIPImageMemoryCacheEntry *)entry; - (void)_background_handleCompletedDiskEntry:(TIPImageDiskCacheEntry *)entry; - (void)_background_handlePartialDiskEntry:(TIPImageDiskCacheEntry *)entry tryOtherPipelineDiskCachesIfNeeded:(BOOL)tryOtherPipelineDiskCachesIfNeeded; // Render Progress - (void)_background_processContinuedPartialEntry:(TIPPartialImage *)partialImage URL:(NSURL *)URL loadSource:(TIPImageLoadSource)source; - (nullable UIImage *)_background_getNextProgressiveImageWithAppendResult:(TIPImageDecoderAppendResult)appendResult partialImage:(TIPPartialImage *)partialImage renderCount:(NSUInteger)renderCount; - (nullable UIImage *)_background_getFirstFrameOfAnimatedImageIfNotYetProvided:(TIPPartialImage *)partialImage; - (UIImage *)_background_transformAndScaleImage:(UIImage *)image progress:(float)progress didTransform:(nonnull out BOOL *)transformedOut; - (TIPImageContainer *)_background_transformAndScaleImageContainer:(TIPImageContainer *)imageContainer progress:(float)progress didTransform:(nonnull out BOOL *)transformedOut; // Create Cache Entry - (nullable TIPImageCacheEntry *)_background_createCacheEntryUsingRawImage:(BOOL)useRawImage permitPreviewFallback:(BOOL)permitPreviewFallback didFallback:(nullable out BOOL *)didFallbackToPreviewOut; - (nullable TIPImageCacheEntry *)_background_createCacheEntryFromPartialImage:(TIPPartialImage *)partialImage lastModified:(NSString *)lastModified imageURL:(NSURL *)imageURL; // Cache propagation - (void)_background_propagateFinalImageData:(nullable NSData *)imageData loadSource:(TIPImageLoadSource)source; - (void)_background_propagateFinalRenderedImage:(TIPImageLoadSource)source; - (void)_background_propagatePartialImage:(TIPPartialImage *)partialImage lastModified:(NSString *)lastModified wasResumed:(BOOL)wasResumed; // source is always the network - (void)_background_propagatePreviewImage:(TIPImageLoadSource)source; // Notifications - (void)_background_postDidStart; - (void)_background_postDidFinish; - (void)_background_postDidStartDownload; - (void)_background_postDidFinishDownloadingImageOfType:(NSString *)imageType sizeInBytes:(NSUInteger)sizeInBytes; // Execute - (void)_background_executeDelegateWork:(TIPImageFetchDelegateWorkBlock)block; - (void)_executeBackgroundWork:(dispatch_block_t)block; @end TIP_OBJC_DIRECT_MEMBERS @interface TIPImageFetchOperation (DiskCache) - (void)_diskCache_loadFromOtherPipelines:(NSArray<TIPImagePipeline *> *)pipelines startMachTime:(uint64_t)startMachTime; - (BOOL)_diskCache_attemptLoadFromOtherPipelineDisk:(TIPImagePipeline *)nextPipeline startMachTime:(uint64_t)startMachTime; - (void)_diskCache_completeLoadFromOtherPipelineDisk:(nullable TIPImagePipeline *)pipeline imageContainer:(nullable TIPImageContainer *)imageContainer URL:(nullable NSURL *)URL latency:(NSTimeInterval)latency placeholder:(BOOL)placeholder; @end // If this fails, the atomic ivar will no longer be valid TIPStaticAssert(sizeof(TIPImageFetchOperationState_Unaligned_AtomicT) == sizeof(TIPImageFetchOperationState), enum_size_missmatch); @implementation TIPImageFetchOperation { // iVars dispatch_queue_t _backgroundQueue; TIPImageFetchMetrics *_metricsInternal; TIPImageFetchOperationState_AtomicT _state; uint64_t _enqueueTime; uint64_t _startTime; uint64_t _finishTime; // Fetch info CGSize _targetDimensions; UIViewContentMode _targetContentMode; TIPImageFetchLoadingSources _loadingSources; NSDictionary<NSString *, id<TIPImageFetchProgressiveLoadingPolicy>> *_progressiveLoadingPolicies; id<TIPImageFetchProgressiveLoadingPolicy> _progressiveLoadingPolicy; id<TIPImageFetchTransformer> _transformer; NSString *_transfomerIdentifier; NSArray<id<TIPImagePipelineObserver>> *_observers; NSDictionary<NSString *, id> *_decoderConfigMap; // Network TIPImageFetchOperationNetworkStepContext *_networkContext; NSUInteger _progressiveRenderCount; // Priority NSOperationQueuePriority _enqueuedPriority; // Flags struct { BOOL cancelled:1; BOOL isEarlyCompletion:1; BOOL invalidRequest:1; BOOL wasEnqueued:1; BOOL didStart:1; BOOL didReceiveFirstByte:1; BOOL shouldJumpToResumingDownload:1; BOOL wasResumedDownload:1; BOOL progressivePermissionValidated:1; BOOL permitsProgressiveLoading:1; BOOL delegateSupportsAttemptWillStartCallbacks:1; BOOL didExtractStorageInfo:1; BOOL didExtractTargetInfo:1; BOOL didReceiveFirstAnimatedFrame:1; BOOL transitioningToFinishedState:1; BOOL progressiveImageWasTransformed:1; BOOL previewImageWasTransformed:1; BOOL finalImageWasTransformed:1; BOOL shouldSkipRenderedCacheStore:1; } _flags; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initWithImagePipeline:(TIPImagePipeline *)pipeline request:(id<TIPImageFetchRequest>)request delegate:(id<TIPImageFetchDelegate>)delegate { if (self = [super init]) { _imagePipeline = pipeline; _request = request; _metricsInternal = [[TIPImageFetchMetrics alloc] initProject]; _targetContentMode = UIViewContentModeCenter; _backgroundQueue = dispatch_queue_create("image.fetch.queue", DISPATCH_QUEUE_SERIAL); atomic_init(&_state, TIPImageFetchOperationStateIdle); _networkContext = [[TIPImageFetchOperationNetworkStepContext alloc] init]; [self _initializeDelegate:delegate]; [self _extractBasicRequestInfo]; } return self; } - (void)_initializeDelegate:(nullable id<TIPImageFetchDelegate>)delegate { _delegate = delegate; _flags.delegateSupportsAttemptWillStartCallbacks = ([delegate respondsToSelector:@selector(tip_imageFetchOperation:willAttemptToLoadFromSource:)] != NO); if (!delegate) { // nil delegate, just let the operation happen } else if ([delegate isKindOfClass:[TIPSimpleImageFetchDelegate class]]) { _strongDelegate = delegate; } else { // associate an object to perform the cancel on dealloc of the delegate TIPImageFetchDelegateDeallocHandler *handler; handler = [[TIPImageFetchDelegateDeallocHandler alloc] initWithFetchOperation:self delegate:delegate]; if (handler) { _delegateHandler = handler; // Use the reference as the unique key since a delegate could have multiple operations objc_setAssociatedObject(delegate, (__bridge const void *)(handler), handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } } - (void)_clearDelegateHandler { TIPImageFetchDelegateDeallocHandler *handler = self.delegateHandler; if (handler) { [handler invalidate]; self.delegateHandler = nil; id<TIPImageFetchDelegate> delegate = self.delegate; if (delegate) { objc_setAssociatedObject(delegate, (__bridge const void *)(handler), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } } - (void)_extractBasicRequestInfo { _networkContext.imageDownloadRequest = [[TIPImageFetchDownloadRequest alloc] initWithRequest:_request]; _loadingSources = [_request respondsToSelector:@selector(loadingSources)] ? [_request loadingSources] : TIPImageFetchLoadingSourcesAll; _decoderConfigMap = _networkContext.imageDownloadRequest.decoderConfigMap; _transformer = [_request respondsToSelector:@selector(transformer)] ? _request.transformer : nil; if ([_transformer respondsToSelector:@selector(tip_transformerIdentifier)]) { _transfomerIdentifier = [[_transformer tip_transformerIdentifier] copy]; TIPAssert(_transfomerIdentifier.length > 0); } if (!self.imageURL || self.imageIdentifier.length == 0) { TIPLogError(@"Cannot fetch request, it is invalid. URL = '%@', Identifier = '%@'", self.imageURL, self.imageIdentifier); _flags.invalidRequest = 1; } } #pragma mark State - (nullable NSString *)transformerIdentifier { return _transfomerIdentifier; } - (nullable NSString *)imageIdentifier { return _networkContext.imageDownloadRequest.imageDownloadIdentifier; } - (nullable NSURL *)imageURL { return _networkContext.imageDownloadRequest.imageDownloadURL; } - (NSTimeInterval)timeSpentIdleInQueue { __block NSTimeInterval ti; dispatch_sync(_backgroundQueue, ^{ if (!self->_enqueueTime) { ti = 0; } else if (!self->_startTime) { ti = TIPComputeDuration(self->_enqueueTime, mach_absolute_time()); } else { ti = TIPComputeDuration(self->_enqueueTime, self->_startTime); } }); return ti; } - (NSTimeInterval)timeSpentExecuting { __block NSTimeInterval ti; dispatch_sync(_backgroundQueue, ^{ if (!self->_startTime) { ti = 0; } else if (!self->_finishTime) { ti = TIPComputeDuration(self->_startTime, mach_absolute_time()); } else { ti = TIPComputeDuration(self->_startTime, self->_finishTime); } }); return ti; } - (TIPImageFetchOperationState)state { return atomic_load(&_state); } - (void)setState:(const TIPImageFetchOperationState)state { // There are only 2 ways that the state is modified // 1) from the background thread during an async operation // 2) from the main thread on early completion // Since the mutation will never happen on multiple threads, // this method is not going to synchronize everything that is // executing (that is automatic by the nature of being called serially // from known threads); rather, just the setting of the _state will // be made atomic with atomic_store. // This will eliminate inconsistencies by ensuring exposed reads are // thread safe with atomic_load. TIPAssert(!!_flags.isEarlyCompletion == !![NSThread isMainThread]); const TIPImageFetchOperationState oldState = atomic_load(&_state); if (oldState == state) { return; } // Never go backwards if (state >= 0) { TIPAssert(state > oldState); if (state < oldState) { return; } } const BOOL finished = TIPImageFetchOperationStateIsFinished(state) != self.isFinished; const BOOL active = TIPImageFetchOperationStateIsActive(state) != self.isExecuting; const BOOL cancelled = (TIPImageFetchOperationStateCancelled == state) != self.isCancelled; if (finished) { [self willChangeValueForKey:@"isFinished"]; } if (active) { [self willChangeValueForKey:@"isExecuting"]; } if (cancelled) { [self willChangeValueForKey:@"isCancelled"]; } atomic_store(&_state, state); if (cancelled) { [self didChangeValueForKey:@"isCancelled"]; } if (active) { [self didChangeValueForKey:@"isExecuting"]; } if (finished) { [self didChangeValueForKey:@"isFinished"]; TIPLogDebug(@"Metrics: %@", self.metrics); // completion cleanup _transformer = nil; // leave _transformerIdentifier [self _clearDelegateHandler]; } } - (void)setQueuePriority:(NSOperationQueuePriority)queuePriority { // noop } - (NSOperationQueuePriority)queuePriority { return _flags.wasEnqueued ? _enqueuedPriority : self.priority; } - (void)setQualityOfService:(NSQualityOfService)qualityOfService { // noop } - (NSQualityOfService)qualityOfService { return ConvertNSOperationQueuePriorityToQualityOfService(_flags.wasEnqueued ? _enqueuedPriority : self.priority); } - (void)setPriority:(NSOperationQueuePriority)priority { if (_networkContext.imageDownloadRequest.imageDownloadPriority != priority) { const BOOL wasEnqueued = _flags.wasEnqueued; // cannot modify other NSOperation priorities if we've already been enqueued if (!wasEnqueued) { [self willChangeValueForKey:@"queuePriority"]; [self willChangeValueForKey:@"qualityOfService"]; } _networkContext.imageDownloadRequest.imageDownloadPriority = priority; if (!wasEnqueued) { [self didChangeValueForKey:@"qualityOfService"]; [self didChangeValueForKey:@"queuePriority"]; } [_imagePipeline.downloader updatePriorityOfContext:_networkContext.imageDownloadContext]; } } - (NSOperationQueuePriority)priority { return _networkContext.imageDownloadRequest.imageDownloadPriority; } #pragma mark Cancel - (void)discardDelegate { [self _clearDelegateHandler]; self.delegate = nil; self.strongDelegate = nil; } - (void)cancel { [self _executeBackgroundWork:^{ if (!self->_flags.cancelled) { self->_flags.cancelled = 1; [self->_imagePipeline.downloader removeDelegate:self forContext:self->_networkContext.imageDownloadContext]; } }]; } - (void)cancelAndDiscardDelegate { [self discardDelegate]; [self cancel]; } #pragma mark NSOperation - (BOOL)isFinished { return TIPImageFetchOperationStateIsFinished(atomic_load(&_state)); } - (BOOL)isExecuting { return TIPImageFetchOperationStateIsActive(atomic_load(&_state)); } - (BOOL)isCancelled { return TIPImageFetchOperationStateCancelled == atomic_load(&_state); } - (BOOL)isConcurrent { return YES; } - (BOOL)isAsynchronous { return YES; } - (void)start { [self _executeBackgroundWork:^{ if (!self->_flags.didStart) { self->_flags.didStart = 1; [self _background_start]; } }]; } - (void)completeOperationEarlyWithImageEntry:(TIPImageCacheEntry *)entry transformed:(BOOL)transformed sourceImageDimensions:(CGSize)sourceDims { TIPAssert([NSThread isMainThread]); TIPAssert(!_flags.didStart); TIPAssert(entry.completeImage != nil); TIPAssert(_metrics == nil); TIPAssert(_metricsInternal != nil); if (CGSizeEqualToSize(CGSizeZero, sourceDims)) { sourceDims = entry.completeImage.dimensions; } [self willEnqueue]; _flags.didStart = 1; _flags.isEarlyCompletion = 1; TIPLogDebug(@"%@ %@, id=%@", NSStringFromSelector(_cmd), entry.completeImage, entry.identifier); _startTime = mach_absolute_time(); self.state = TIPImageFetchOperationStateLoadingFromMemory; [self _executeBackgroundWork:^{ [self _background_extractObservers]; [self _background_postDidStart]; }]; id<TIPImageFetchDelegate> delegate = self.delegate; if ([delegate respondsToSelector:@selector(tip_imageFetchOperationDidStart:)]) { [delegate tip_imageFetchOperationDidStart:self]; } [_metricsInternal startWithSource:TIPImageLoadSourceMemoryCache]; _networkContext = nil; self.finalImageContainerRaw = entry.completeImage; id<TIPImageFetchResult> finalResult = [TIPImageFetchResultInternal resultWithImageContainer:entry.completeImage identifier:entry.identifier loadSource:TIPImageLoadSourceMemoryCache URL:entry.completeImageContext.URL originalDimensions:sourceDims placeholder:entry.completeImageContext.treatAsPlaceholder transformed:transformed]; self.finalResult = finalResult; [_imagePipeline.memoryCache touchImageWithIdentifier:entry.identifier]; [_imagePipeline.diskCache touchImageWithIdentifier:entry.identifier orSaveImageEntry:nil]; [_metricsInternal finalWasHit:0.0 synchronously:YES]; [_metricsInternal endSource]; _metrics = _metricsInternal; _metricsInternal = nil; _finishTime = mach_absolute_time(); TIPAssert(finalResult != nil); if (finalResult && [delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadFinalImage:)]) { [delegate tip_imageFetchOperation:self didLoadFinalImage:finalResult]; } [self _executeBackgroundWork:^{ [self _background_postDidFinish]; }]; self.state = TIPImageFetchOperationStateSucceeded; } - (void)handleEarlyLoadOfDirtyImageEntry:(TIPImageCacheEntry *)entry transformed:(BOOL)transformed sourceImageDimensions:(CGSize)sourceDims { TIPAssert([NSThread isMainThread]); TIPAssert(!_flags.didStart); TIPAssert(entry.completeImage != nil); TIPAssert(_metrics == nil); TIPAssert(_metricsInternal != nil); TIPLogDebug(@"%@ %@, id=%@", NSStringFromSelector(_cmd), entry.completeImage, entry.identifier); id<TIPImageFetchDelegate> delegate = self.delegate; if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadDirtyPreviewImage:)]) { id<TIPImageFetchResult> result = [TIPImageFetchResultInternal resultWithImageContainer:entry.completeImage identifier:entry.identifier loadSource:TIPImageLoadSourceMemoryCache URL:entry.completeImageContext.URL originalDimensions:sourceDims placeholder:entry.completeImageContext.treatAsPlaceholder transformed:transformed]; [delegate tip_imageFetchOperation:self didLoadDirtyPreviewImage:result]; } } - (void)willEnqueue { TIPAssert(!_flags.wasEnqueued); _flags.wasEnqueued = 1; _enqueuedPriority = self.priority; _enqueueTime = mach_absolute_time(); } - (BOOL)supportsLoadingFromRenderedCache { if (_transformer && !_transfomerIdentifier) { return NO; } return [self supportsLoadingFromSource:TIPImageLoadSourceMemoryCache]; } - (BOOL)supportsLoadingFromSource:(TIPImageLoadSource)source { if (TIPImageLoadSourceUnknown == source) { return YES; } return TIP_BITMASK_HAS_SUBSET_FLAGS(_loadingSources, (1 << source)); } #pragma mark Wait - (void)waitUntilFinished { [super waitUntilFinished]; } - (void)waitUntilFinishedWithoutBlockingRunLoop { // Default implementation is to block the thread until the execution completes. // This can deadlock if the caller is not careful and the completion queue or callback queue // are the same thread that waitUntilFinished are called from. // In this method, we'll pump the run loop until we're finished as a way to provide an alternative. NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; if (!runLoop) { return [self waitUntilFinished]; } while (!self.isFinished) { [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; } } #pragma mark Downloader Delegate - (nullable dispatch_queue_t)imageDownloadDelegateQueue { return _backgroundQueue; } - (id<TIPImageDownloadRequest>)imageDownloadRequest { return _networkContext.imageDownloadRequest; } - (TIPImageDiskCacheTemporaryFile *)regenerateImageDownloadTemporaryFileForImageDownload:(id<TIPImageDownloadContext>)context { TIPImageDiskCacheTemporaryFile *tempFile = [_imagePipeline.diskCache openTemporaryFileForImageIdentifier:self.imageIdentifier]; TIPAssert(tempFile != nil); _networkContext.imageDownloadRequest.imageDownloadTemporaryFileForResuming = tempFile; return (TIPImageDiskCacheTemporaryFile * _Nonnull)tempFile; // TIPAssert() performed 2 lines above } - (void)imageDownloadDidStart:(id<TIPImageDownloadContext>)context { [self _background_postDidStartDownload]; const TIPPartialImage *partialImage = _networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming; const float progress = partialImage ? partialImage.progress : 0.0f; [self _background_updateProgress:progress]; } - (void)imageDownload:(id<TIPImageDownloadContext>)context didResetFromPartialImage:(TIPPartialImage *)oldPartialImage { TIPAssert(!_flags.didReceiveFirstByte); [self _background_clearNetworkContextVariables]; if ([self supportsLoadingFromSource:TIPImageLoadSourceNetwork]) { [self _background_updateProgress:0.0f]; } else { // Not configured to do a normal network load, fail NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCouldNotLoadImage userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; [_imagePipeline.downloader removeDelegate:self forContext:context]; } } - (void)imageDownload:(id<TIPImageDownloadContext>)context didAppendBytes:(NSUInteger)byteCount toPartialImage:(TIPPartialImage *)partialImage result:(TIPImageDecoderAppendResult)result { if ([self _background_shouldAbort]) { return; } if (!_flags.didReceiveFirstByte) { _flags.didReceiveFirstByte = 1; if (partialImage.byteCount > byteCount) { _flags.wasResumedDownload = YES; [_metricsInternal convertNetworkMetricsToResumedNetworkMetrics]; } } _progressiveFrameCount = partialImage.frameCount; uint64_t startMachTime = mach_absolute_time(); if (partialImage.type) { self.networkLoadImageType = partialImage.type; } if (TIPSizeEqualToZero(_networkImageOriginalDimensions) && !TIPSizeEqualToZero(partialImage.dimensions)) { self.networkImageOriginalDimensions = partialImage.dimensions; } // Progress const float progress = partialImage.progress; if (partialImage.isAnimated) { UIImage *image = [self _background_getFirstFrameOfAnimatedImageIfNotYetProvided:partialImage]; const NSTimeInterval latency = TIPComputeDuration(startMachTime, mach_absolute_time()); if (image) { // First frame progress [self _background_updateFirstAnimatedImageFrame:image renderLatency:latency URL:self.imageURL progress:progress sourcePartialImage:partialImage loadSource:(_flags.wasResumedDownload) ? TIPImageLoadSourceNetworkResumed : TIPImageLoadSourceNetwork]; } } else if (partialImage.isProgressive) { UIImage *image = [self _background_getNextProgressiveImageWithAppendResult:result partialImage:partialImage renderCount:_progressiveRenderCount]; if (image) { // Progressive image progress BOOL transformed = NO; image = [self _background_transformAndScaleImage:image progress:progress didTransform:&transformed]; const NSTimeInterval latency = TIPComputeDuration(startMachTime, mach_absolute_time()); _progressiveRenderCount++; [self _background_updateProgressiveImage:image transformed:transformed renderLatency:latency URL:self.imageURL progress:progress sourcePartialImage:partialImage loadSource:(_flags.wasResumedDownload) ? TIPImageLoadSourceNetworkResumed : TIPImageLoadSourceNetwork]; } } // Always update the plain ol' progress [self _background_updateProgress:progress]; } - (void)imageDownload:(id<TIPImageDownloadContext>)context didCompleteWithPartialImage:(nullable TIPPartialImage *)partialImage lastModified:(nullable NSString *)lastModified byteSize:(NSUInteger)bytes imageType:(nullable NSString *)imageType image:(nullable TIPImageContainer *)image imageData:(nullable NSData *)imageData imageRenderLatency:(NSTimeInterval)latency statusCode:(NSInteger)statusCode error:(nullable NSError *)error { const BOOL wasResuming = (_networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming != nil); [self _background_clearNetworkContextVariables]; _networkContext.imageDownloadContext = nil; id<TIPImageFetchDownload> download = (id<TIPImageFetchDownload>)context; [_metricsInternal addNetworkMetrics:[download respondsToSelector:@selector(downloadMetrics)] ? download.downloadMetrics : nil forRequest:download.finalURLRequest imageType:imageType imageSizeInBytes:bytes imageDimensions:(image) ? image.dimensions : partialImage.dimensions]; if (partialImage && !image) { [self _background_propagatePartialImage:partialImage lastModified:lastModified wasResumed:wasResuming]; } if ([self _background_shouldAbort]) { return; } if (image) { if (partialImage.hasGPSInfo) { // we should NEVER encounter an image with GPS info, // that would be a MAJOR security risk [[TIPGlobalConfiguration sharedInstance] postProblem:TIPProblemImageDownloadedHasGPSInfo userInfo:@{ TIPProblemInfoKeyImageURL : self.imageURL }]; } const BOOL placeholder = TIP_BITMASK_HAS_SUBSET_FLAGS(_networkContext.imageDownloadRequest.imageDownloadOptions, TIPImageFetchTreatAsPlaceholder); [self _background_updateFinalImage:image imageData:imageData // TODO: is this too much? Could defer the caching of the data to memory until next disk cache hit renderLatency:latency URL:self.imageURL loadSource:(_flags.wasResumedDownload) ? TIPImageLoadSourceNetworkResumed : TIPImageLoadSourceNetwork networkImageType:imageType networkByteCount:bytes placeholder:placeholder]; } else { TIPAssert(error != nil); if (wasResuming && 416 /* Requested range not satisfiable */ == statusCode) { TIPAssert(!_flags.wasResumedDownload); if (!_flags.wasResumedDownload) { TIPLogWarning(@"Network resume yielded HTTP 416... retrying with full network load: %@", _networkContext.imageDownloadRequest.imageDownloadURL); [self _background_loadFromNetwork]; return; } } if (!error) { error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeUnknown userInfo:nil]; } [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; } } #pragma mark Helpers - (void)_hydrateNewContext:(TIPImageCacheEntryContext *)context imageURL:(NSURL *)imageURL placeholder:(BOOL)placeholder { if (!imageURL) { imageURL = self.imageURL; } const TIPImageFetchOptions options = _networkContext.imageDownloadRequest.imageDownloadOptions; context.updateExpiryOnAccess = TIP_BITMASK_EXCLUDES_FLAGS(options, TIPImageFetchDoNotResetExpiryOnAccess); context.treatAsPlaceholder = placeholder; context.TTL = _networkContext.imageDownloadRequest.imageDownloadTTL; context.URL = imageURL; context.lastAccess = [NSDate date]; if (context.TTL <= 0.0) { context.TTL = TIPTimeToLiveDefault; } } @end @implementation TIPImageFetchOperation (Background) #pragma mark Start / Abort - (void)_background_start { _startTime = mach_absolute_time(); if ([self _background_shouldAbort]) { return; } self.state = TIPImageFetchOperationStateStarting; [self _background_extractObservers]; [self _background_postDidStart]; [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate){ if ([delegate respondsToSelector:@selector(tip_imageFetchOperationDidStart:)]) { [delegate tip_imageFetchOperationDidStart:self]; } }]; [self _background_extractTargetInfo]; // now that we decode to the target sizing, extract early [self _background_extractAdvancedRequestInfo]; [self _background_loadFromNextSource]; } - (BOOL)_background_shouldAbort { if (self.isFinished || _flags.transitioningToFinishedState) { return YES; } if (_flags.cancelled) { NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCancelled userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; return YES; } if (_flags.invalidRequest) { NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeInvalidRequest userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:NO]; return YES; } return NO; } #pragma mark Generate State - (void)_background_extractObservers { _observers = [TIPGlobalConfiguration sharedInstance].allImagePipelineObservers; id<TIPImagePipelineObserver> pipelineObserver = self.imagePipeline.observer; if (pipelineObserver) { if (!_observers) { _observers = @[pipelineObserver]; } else { _observers = [_observers arrayByAddingObject:pipelineObserver]; } } } - (void)_background_extractStorageInfo { if (_flags.didExtractStorageInfo) { return; } NSTimeInterval TTL = [_request respondsToSelector:@selector(timeToLive)] ? [_request timeToLive] : -1.0; if (TTL <= 0.0) { TTL = TIPTimeToLiveDefault; } _networkContext.imageDownloadRequest.imageDownloadTTL = TTL; const TIPImageFetchOptions options = [_request respondsToSelector:@selector(options)] ? [_request options] : TIPImageFetchNoOptions; _networkContext.imageDownloadRequest.imageDownloadOptions = options; _flags.shouldSkipRenderedCacheStore = TIP_BITMASK_HAS_SUBSET_FLAGS(options, TIPImageFetchSkipStoringToRenderedCache); _flags.didExtractStorageInfo = 1; } - (void)_background_extractAdvancedRequestInfo { _networkContext.imageDownloadRequest.targetDimensions = _targetDimensions; _networkContext.imageDownloadRequest.targetContentMode = _targetContentMode; _networkContext.imageDownloadRequest.imageDownloadHydrationBlock = [_request respondsToSelector:@selector(imageRequestHydrationBlock)] ? _request.imageRequestHydrationBlock : nil; _networkContext.imageDownloadRequest.imageDownloadAuthorizationBlock = [_request respondsToSelector:@selector(imageRequestAuthorizationBlock)] ? _request.imageRequestAuthorizationBlock : nil; _progressiveLoadingPolicies = nil; id<TIPImageFetchDelegate> delegate = self.delegate; if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:shouldLoadProgressivelyWithIdentifier:URL:imageType:originalDimensions:)]) { // could support progressive, prep the policy _progressiveLoadingPolicies = [_request respondsToSelector:@selector(progressiveLoadingPolicies)] ? [[_request progressiveLoadingPolicies] copy] : nil; } } - (void)_background_extractTargetInfo { if (_flags.didExtractTargetInfo) { return; } _targetDimensions = [_request respondsToSelector:@selector(targetDimensions)] ? [_request targetDimensions] : CGSizeZero; _targetContentMode = [_request respondsToSelector:@selector(targetContentMode)] ? [_request targetContentMode] : UIViewContentModeCenter; _flags.didExtractTargetInfo = 1; } - (void)_background_validateProgressiveSupportWithPartialImage:(TIPPartialImage *)partialImage { if (!_flags.progressivePermissionValidated) { if (partialImage.state > TIPPartialImageStateLoadingHeaders) { id<TIPImageFetchDelegate> delegate = self.delegate; if (partialImage.progressive && [delegate respondsToSelector:@selector(tip_imageFetchOperation:shouldLoadProgressivelyWithIdentifier:URL:imageType:originalDimensions:)]) { TIPAssert(partialImage.type != nil); _progressiveLoadingPolicy = _progressiveLoadingPolicies[partialImage.type ?: @""]; if (!_progressiveLoadingPolicy) { _progressiveLoadingPolicy = TIPImageFetchProgressiveLoadingPolicyDefaultPolicies()[partialImage.type ?: @""]; } if (_progressiveLoadingPolicy) { const BOOL shouldLoad = [delegate tip_imageFetchOperation:self shouldLoadProgressivelyWithIdentifier:self.imageIdentifier URL:self.imageURL imageType:partialImage.type originalDimensions:partialImage.dimensions]; if (shouldLoad) { _flags.permitsProgressiveLoading = 1; } } } _flags.progressivePermissionValidated = 1; } } } - (void)_background_clearNetworkContextVariables { _networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming = nil; _networkContext.imageDownloadRequest.imageDownloadLastModified = nil; _networkContext.imageDownloadRequest.imageDownloadTemporaryFileForResuming = nil; _progressiveRenderCount = 0; } - (void)_background_setFinalStateAfterFlushingDelegate:(TIPImageFetchOperationState)state { TIPAssert(TIPImageFetchOperationStateIsFinished(state)); _flags.transitioningToFinishedState = 1; [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> __unused delegate) { [self _executeBackgroundWork:^{ self.state = state; self->_flags.transitioningToFinishedState = 0; }]; }]; } #pragma mark Load - (void)_background_dispatchLoadStarted:(TIPImageLoadSource)source { if (_flags.delegateSupportsAttemptWillStartCallbacks) { [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate) { [delegate tip_imageFetchOperation:self willAttemptToLoadFromSource:source]; }]; } } - (void)_background_loadFromNextSource { if ([self _background_shouldAbort]) { return; } TIPImageLoadSource nextSource = TIPImageLoadSourceUnknown; const TIPImageFetchOperationState currentState = atomic_load(&_state); // Get the next loading source if (_flags.shouldJumpToResumingDownload && currentState < TIPImageFetchOperationStateLoadingFromNetwork) { nextSource = TIPImageLoadSourceNetwork; } else { switch (currentState) { case TIPImageFetchOperationStateIdle: case TIPImageFetchOperationStateStarting: nextSource = TIPImageLoadSourceMemoryCache; break; case TIPImageFetchOperationStateLoadingFromMemory: nextSource = TIPImageLoadSourceDiskCache; break; case TIPImageFetchOperationStateLoadingFromDisk: nextSource = TIPImageLoadSourceAdditionalCache; break; case TIPImageFetchOperationStateLoadingFromAdditionalCache: nextSource = TIPImageLoadSourceNetwork; break; case TIPImageFetchOperationStateLoadingFromNetwork: nextSource = TIPImageLoadSourceUnknown; break; case TIPImageFetchOperationStateCancelled: case TIPImageFetchOperationStateFailed: case TIPImageFetchOperationStateSucceeded: // nothing to do return; } } // Update the metrics if (currentState != TIPImageFetchOperationStateIdle && currentState != TIPImageFetchOperationStateStarting) { [_metricsInternal endSource]; } if (TIPImageLoadSourceUnknown != nextSource) { [_metricsInternal startWithSource:nextSource]; } // Load whatever's next (or set state to failed) switch (nextSource) { case TIPImageLoadSourceUnknown: { NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCouldNotLoadImage userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:NO]; break; } case TIPImageLoadSourceMemoryCache: [self _background_loadFromMemory]; break; case TIPImageLoadSourceDiskCache: [self _background_loadFromDisk]; break; case TIPImageLoadSourceAdditionalCache: [self _background_loadFromAdditional]; break; case TIPImageLoadSourceNetwork: case TIPImageLoadSourceNetworkResumed: [self _background_loadFromNetwork]; break; } } - (void)_background_loadFromMemory { self.state = TIPImageFetchOperationStateLoadingFromMemory; if (![self supportsLoadingFromSource:TIPImageLoadSourceMemoryCache]) { [self _background_loadFromNextSource]; return; } [self _background_dispatchLoadStarted:TIPImageLoadSourceMemoryCache]; TIPImageMemoryCacheEntry *entry = [_imagePipeline.memoryCache imageEntryForIdentifier:self.imageIdentifier targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; [self _background_handleCompletedMemoryEntry:entry]; } - (void)_background_loadFromDisk { self.state = TIPImageFetchOperationStateLoadingFromDisk; if (![self supportsLoadingFromSource:TIPImageLoadSourceDiskCache]) { [self _background_loadFromNextSource]; return; } [self _background_dispatchLoadStarted:TIPImageLoadSourceDiskCache]; // Just load the meta-data (options == TIPImageDiskCacheFetchOptionsNone) TIPImageDiskCacheEntry *entry; entry = [_imagePipeline.diskCache imageEntryForIdentifier:self.imageIdentifier options:TIPImageDiskCacheFetchOptionsNone targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; [self _background_handleCompletedDiskEntry:entry]; } - (void)_background_loadFromOtherPipelineDisk { TIPAssert(self.state == TIPImageFetchOperationStateLoadingFromDisk); [self _background_extractStorageInfo]; // need TTL and options NSMutableDictionary<NSString *, TIPImagePipeline *> *pipelines = [[TIPImagePipeline allRegisteredImagePipelines] mutableCopy]; [pipelines removeObjectForKey:_imagePipeline.identifier]; NSArray<TIPImagePipeline *> *otherPipelines = [pipelines allValues]; tip_dispatch_async_autoreleasing([TIPGlobalConfiguration sharedInstance].queueForDiskCaches, ^{ [self _diskCache_loadFromOtherPipelines:otherPipelines startMachTime:mach_absolute_time()]; }); } - (void)_background_loadFromAdditional { self.state = TIPImageFetchOperationStateLoadingFromAdditionalCache; if (![self supportsLoadingFromSource:TIPImageLoadSourceAdditionalCache]) { [self _background_loadFromNextSource]; return; } [self _background_dispatchLoadStarted:TIPImageLoadSourceAdditionalCache]; NSMutableArray<id<TIPImageAdditionalCache>> *additionalCaches = [_imagePipeline.additionalCaches mutableCopy]; [self _background_loadFromNextAdditionalCache:additionalCaches imageURL:self.imageURL]; } - (void)_background_loadFromNextAdditionalCache:(NSMutableArray<id<TIPImageAdditionalCache>> *)caches imageURL:(NSURL *)imageURL { if (caches.count == 0) { [self _background_loadFromNextSource]; return; } id<TIPImageAdditionalCache> nextCache = caches.firstObject; [caches removeObjectAtIndex:0]; [nextCache tip_retrieveImageForURL:imageURL completion:^(UIImage *image) { [self _executeBackgroundWork:^{ if ([self _background_shouldAbort]) { return; } if (image) { const BOOL placeholder = TIP_BITMASK_HAS_SUBSET_FLAGS(self->_networkContext.imageDownloadRequest.imageDownloadOptions, TIPImageFetchTreatAsPlaceholder); [self _background_updateFinalImage:[[TIPImageContainer alloc] initWithImage:image] imageData:nil renderLatency:0 URL:imageURL loadSource:TIPImageLoadSourceAdditionalCache networkImageType:nil networkByteCount:0 placeholder:placeholder]; } else { [self _background_loadFromNextAdditionalCache:caches imageURL:imageURL]; } }]; }]; } - (void)_background_loadFromNetwork { self.state = TIPImageFetchOperationStateLoadingFromNetwork; if (!_imagePipeline.downloader) { NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCouldNotDownloadImage userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; return; } if (![self supportsLoadingFromSource:TIPImageLoadSourceNetwork]) { if (![self supportsLoadingFromSource:TIPImageLoadSourceNetworkResumed] || !_networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming) { // if full loads not OK and resuming not OK - fail NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCouldNotLoadImage userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; return; } // else if full loads not OK, but resuming is OK - continue } // Start loading [self _background_extractStorageInfo]; const TIPImageLoadSource loadSource = (_networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming != nil) ? TIPImageLoadSourceNetworkResumed : TIPImageLoadSourceNetwork; [self _background_dispatchLoadStarted:loadSource]; _networkContext.imageDownloadContext = [_imagePipeline.downloader fetchImageWithDownloadDelegate:self]; } #pragma mark Update - (void)_background_updateFailureToLoadFinalImage:(NSError *)error updateMetrics:(BOOL)updateMetrics { TIPAssert(error != nil); TIPAssert(_metrics == nil); TIPAssert(_metricsInternal != nil); TIPLogDebug(@"Failed to Load Image: %@", @{ @"id" : self.imageIdentifier ?: @"<null>", @"URL" : self.imageURL ?: @"<null>", @"error" : error ?: @"<null>" }); self.error = error; const BOOL didCancel = ([error.domain isEqualToString:TIPImageFetchErrorDomain] && error.code == TIPImageFetchErrorCodeCancelled); if (updateMetrics) { if (didCancel) { [_metricsInternal cancelSource]; } else { [_metricsInternal endSource]; } } _metrics = _metricsInternal; _metricsInternal = nil; _finishTime = mach_absolute_time(); [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate) { if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:didFailToLoadFinalImage:)]) { [delegate tip_imageFetchOperation:self didFailToLoadFinalImage:error]; } }]; [self _background_postDidFinish]; [self _background_setFinalStateAfterFlushingDelegate:(didCancel) ? TIPImageFetchOperationStateCancelled : TIPImageFetchOperationStateFailed]; } - (void)_background_updateFinalImage:(TIPImageContainer *)image imageData:(nullable NSData *)imageData renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL loadSource:(TIPImageLoadSource)source networkImageType:(nullable NSString *)networkImageType networkByteCount:(NSUInteger)networkByteCount placeholder:(BOOL)placeholder { TIPAssert(image != nil); TIPAssert(_metrics == nil); TIPAssert(_metricsInternal != nil); [self _background_extractTargetInfo]; self.finalImageContainerRaw = image; const uint64_t startMachTime = mach_absolute_time(); BOOL transformed = NO; TIPImageContainer *finalImageContainer = [self _background_transformAndScaleImageContainer:image progress:1.f didTransform:&transformed]; _flags.finalImageWasTransformed = transformed; imageRenderLatency += TIPComputeDuration(startMachTime, mach_absolute_time()); id<TIPImageFetchResult> finalResult = [TIPImageFetchResultInternal resultWithImageContainer:finalImageContainer identifier:self.imageIdentifier loadSource:source URL:URL originalDimensions:image.dimensions placeholder:placeholder transformed:transformed]; self.finalResult = finalResult; self.progress = 1.0f; [_metricsInternal finalWasHit:imageRenderLatency synchronously:NO]; [_metricsInternal endSource]; _metrics = _metricsInternal; _metricsInternal = nil; _finishTime = mach_absolute_time(); TIPLogDebug(@"Loaded Final Image: %@", @{ @"id" : self.imageIdentifier, @"URL" : self.imageURL, @"originalDimensions" : NSStringFromCGSize(self.finalImageContainerRaw.dimensions), @"finalDimensions" : NSStringFromCGSize(self.finalResult.imageContainer.dimensions), @"source" : @(source), @"store" : _imagePipeline.identifier, @"resumed" : @(_flags.wasResumedDownload), @"frames" : @(self.finalResult.imageContainer.frameCount), }); const BOOL sourceWasNetwork = TIPImageLoadSourceNetwork == source || TIPImageLoadSourceNetworkResumed == source; if (sourceWasNetwork && networkByteCount > 0) { [self _background_postDidFinishDownloadingImageOfType:networkImageType sizeInBytes:networkByteCount]; } TIPAssert(finalResult != nil); if (!finalResult) { self.error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeUnknown userInfo:nil]; [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate) { if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:didFailToLoadFinalImage:)]) { [delegate tip_imageFetchOperation:self didFailToLoadFinalImage:self.error]; } }]; [self _background_postDidFinish]; [self _background_setFinalStateAfterFlushingDelegate:TIPImageFetchOperationStateFailed]; return; } [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate){ if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadFinalImage:)]) { [delegate tip_imageFetchOperation:self didLoadFinalImage:finalResult]; } }]; [self _background_postDidFinish]; [self _background_propagateFinalImageData:imageData loadSource:source]; [self _background_setFinalStateAfterFlushingDelegate:TIPImageFetchOperationStateSucceeded]; } - (void)_background_postDidStart { for (id<TIPImagePipelineObserver> observer in _observers) { if ([observer respondsToSelector:@selector(tip_imageFetchOperationDidStart:)]) { [observer tip_imageFetchOperationDidStart:self]; } } } - (void)_background_postDidFinish { for (id<TIPImagePipelineObserver> observer in _observers) { if ([observer respondsToSelector:@selector(tip_imageFetchOperationDidFinish:)]) { [observer tip_imageFetchOperationDidFinish:self]; } } } - (void)_background_postDidStartDownload { for (id<TIPImagePipelineObserver> observer in _observers) { if ([observer respondsToSelector:@selector(tip_imageFetchOperation:didStartDownloadingImageAtURL:)]) { [observer tip_imageFetchOperation:self didStartDownloadingImageAtURL:self.imageURL]; } } } - (void)_background_postDidFinishDownloadingImageOfType:(NSString *)imageType sizeInBytes:(NSUInteger)sizeInBytes { for (id<TIPImagePipelineObserver> observer in _observers) { if ([observer respondsToSelector:@selector(tip_imageFetchOperation:didFinishDownloadingImageAtURL:imageType:sizeInBytes:dimensions:wasResumed:)]) { [observer tip_imageFetchOperation:self didFinishDownloadingImageAtURL:self.imageURL imageType:imageType sizeInBytes:sizeInBytes dimensions:self.finalImageContainerRaw.dimensions wasResumed:(self.finalResult.imageSource == TIPImageLoadSourceNetworkResumed)]; } } } - (void)_background_updatePreviewImageWithCacheEntry:(TIPImageCacheEntry *)cacheEntry loadSource:(TIPImageLoadSource)source { TIPBoolBlock block = ^(BOOL canContinue) { if ([self _background_shouldAbort]) { return; } else if (!canContinue) { NSError *error = [NSError errorWithDomain:TIPImageFetchErrorDomain code:TIPImageFetchErrorCodeCancelledAfterLoadingPreview userInfo:nil]; [self _background_updateFailureToLoadFinalImage:error updateMetrics:YES]; [self _background_propagatePreviewImage:source]; } else { if (TIPImageLoadSourceMemoryCache == source) { [self _background_handlePartialMemoryEntry:(id)cacheEntry]; } else if (TIPImageLoadSourceDiskCache == source) { [self _background_handlePartialDiskEntry:(id)cacheEntry tryOtherPipelineDiskCachesIfNeeded:NO]; } else { [self _background_loadFromNextSource]; } } }; [self _background_extractTargetInfo]; TIPImageContainer *image = cacheEntry.completeImage; TIPAssert(image != nil); if (!image) { // the analyzer reports image can be nil because .completeImage is nullable; // if it is (and thus even if we fail TIPAssert() in dogfood but not Production), // then return early, because the logic below will lead to a previewResult that // is also nil, which results in an else-part below where we do the following. block(YES); return; } self.previewImageContainerRaw = image; const uint64_t startMachTime = mach_absolute_time(); BOOL transformed = NO; TIPImageContainer *previewImageContainer; previewImageContainer = [self _background_transformAndScaleImageContainer:image progress:-1.f // negative == preview didTransform:&transformed]; _flags.previewImageWasTransformed = transformed; const NSTimeInterval latency = TIPComputeDuration(startMachTime, mach_absolute_time()); id<TIPImageFetchResult> previewResult = [TIPImageFetchResultInternal resultWithImageContainer:previewImageContainer identifier:self.imageIdentifier loadSource:source URL:cacheEntry.completeImageContext.URL originalDimensions:image.dimensions placeholder:cacheEntry.completeImageContext.treatAsPlaceholder transformed:transformed]; self.previewResult = previewResult; id<TIPImageFetchDelegate> delegate = self.delegate; [_metricsInternal previewWasHit:latency]; TIPLogDebug(@"Loaded Preview Image: %@", @{ @"id" : self.imageIdentifier, @"URL" : self.previewResult.imageURL, @"originalDimensions" : NSStringFromCGSize(self.previewImageContainerRaw.dimensions), @"finalDimensions" : NSStringFromCGSize(self.previewResult.imageContainer.dimensions), @"source" : @(source), @"store" : _imagePipeline.identifier, @"resumed" : @(_flags.wasResumedDownload), }); TIPAssert(previewResult != nil); if (previewResult && [delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadPreviewImage:completion:)]) { [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> blockDelegate) { if ([blockDelegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadPreviewImage:completion:)]) { [blockDelegate tip_imageFetchOperation:self didLoadPreviewImage:previewResult completion:^(TIPImageFetchPreviewLoadedBehavior behavior) { [self _executeBackgroundWork:^{ block(TIPImageFetchPreviewLoadedBehaviorContinueLoading == behavior); }]; }]; } else { [self _executeBackgroundWork:^{ block(YES); }]; } }]; } else { block(YES); } } - (void)_background_updateFirstAnimatedImageFrame:(UIImage *)image renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL progress:(float)progress sourcePartialImage:(TIPPartialImage *)sourcePartialImage loadSource:(TIPImageLoadSource)source { id<TIPImageFetchDelegate> delegate = self.delegate; if (![delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadFirstAnimatedImageFrame:progress:)]) { return; } TIPAssert(!isnan(progress) && !isinf(progress)); self.progress = progress; const uint64_t startMachTime = mach_absolute_time(); UIImage *firstAnimatedImage = [image tip_scaledImageWithTargetDimensions:_targetDimensions contentMode:_targetContentMode]; TIPImageContainer *firstAnimatedImageFrameContainer = (firstAnimatedImage) ? [[TIPImageContainer alloc] initWithImage:firstAnimatedImage] : nil; imageRenderLatency += TIPComputeDuration(startMachTime, mach_absolute_time()); id<TIPImageFetchResult> progressiveResult = [TIPImageFetchResultInternal resultWithImageContainer:firstAnimatedImageFrameContainer identifier:self.imageIdentifier loadSource:source URL:URL originalDimensions:image.tip_dimensions placeholder:NO transformed:NO]; self.progressiveResult = progressiveResult; [_metricsInternal progressiveFrameWasHit:imageRenderLatency]; TIPLogDebug(@"Loaded First Animated Image Frame: %@", @{ @"id" : self.imageIdentifier, @"URL" : self.imageURL, @"originalDimensions" : NSStringFromCGSize(sourcePartialImage.dimensions), @"finalDimensions" : NSStringFromCGSize([firstAnimatedImageFrameContainer dimensions]), @"source" : @(_flags.wasResumedDownload ? TIPImageLoadSourceNetworkResumed : TIPImageLoadSourceNetwork), @"store" : _imagePipeline.identifier, @"resumed" : @(_flags.wasResumedDownload), }); TIPAssert(progressiveResult != nil); if (progressiveResult) { [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> blockDelegate) { if ([blockDelegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadFirstAnimatedImageFrame:progress:)]) { [blockDelegate tip_imageFetchOperation:self didLoadFirstAnimatedImageFrame:progressiveResult progress:progress]; } }]; } } - (void)_background_updateProgressiveImage:(UIImage *)image transformed:(BOOL)transformed renderLatency:(NSTimeInterval)imageRenderLatency URL:(NSURL *)URL progress:(float)progress sourcePartialImage:(TIPPartialImage *)sourcePartialImage loadSource:(TIPImageLoadSource)source { id<TIPImageFetchDelegate> delegate = self.delegate; if (![delegate respondsToSelector:@selector(tip_imageFetchOperation:didUpdateProgressiveImage:progress:)]) { return; } TIPAssert(!isnan(progress) && !isinf(progress)); self.progress = progress; TIPAssert(image != nil); _flags.progressiveImageWasTransformed = transformed; TIPImageContainer *progressContainer = (image) ? [[TIPImageContainer alloc] initWithImage:image] : nil; id<TIPImageFetchResult> progressiveResult = [TIPImageFetchResultInternal resultWithImageContainer:progressContainer identifier:self.imageIdentifier loadSource:source URL:URL originalDimensions:image.tip_dimensions placeholder:NO transformed:NO]; self.progressiveResult = progressiveResult; [_metricsInternal progressiveFrameWasHit:imageRenderLatency]; TIPLogDebug(@"Loaded Progressive Image: %@", @{ @"progress" : @(progress), @"id" : self.imageIdentifier, @"URL" : URL, @"originalDimensions" : NSStringFromCGSize(sourcePartialImage.dimensions), @"finalDimensions" : NSStringFromCGSize([self.progressiveResult.imageContainer dimensions]), @"source" : @(source), @"store" : _imagePipeline.identifier, @"resumed" : @(_flags.wasResumedDownload), }); TIPAssert(progressiveResult != nil); if (progressiveResult) { [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> blockDelegate) { if ([blockDelegate respondsToSelector:@selector(tip_imageFetchOperation:didUpdateProgressiveImage:progress:)]) { [blockDelegate tip_imageFetchOperation:self didUpdateProgressiveImage:progressiveResult progress:progress]; } }]; } } - (void)_background_updateProgress:(float)progress { TIPAssert(!isnan(progress) && !isinf(progress)); self.progress = progress; [self _background_executeDelegateWork:^(id<TIPImageFetchDelegate> delegate) { if ([delegate respondsToSelector:@selector(tip_imageFetchOperation:didUpdateProgress:)]) { [delegate tip_imageFetchOperation:self didUpdateProgress:progress]; } }]; } - (void)_background_handleCompletedMemoryEntry:(TIPImageMemoryCacheEntry *)entry { if ([self _background_shouldAbort]) { return; } TIPImageContainer *image = entry.completeImage; if (image) { NSURL *completeImageURL = entry.completeImageContext.URL; const BOOL isFinalImage = [completeImageURL isEqual:self.imageURL]; if (isFinalImage) { [self _background_updateFinalImage:image imageData:entry.completeImageData /*ok if nil*/ renderLatency:0 URL:completeImageURL loadSource:TIPImageLoadSourceMemoryCache networkImageType:nil networkByteCount:0 placeholder:entry.completeImageContext.treatAsPlaceholder]; return; } if (!self.previewResult) { [self _background_updatePreviewImageWithCacheEntry:entry loadSource:TIPImageLoadSourceMemoryCache]; return; } } // continue [self _background_handlePartialMemoryEntry:entry]; } - (void)_background_handlePartialMemoryEntry:(TIPImageMemoryCacheEntry *)entry { if ([self _background_shouldAbort]) { return; } TIPPartialImage * const partialImage = entry.partialImage; if (partialImage && [self supportsLoadingFromSource:TIPImageLoadSourceNetworkResumed]) { const BOOL isFinalImage = [self.imageURL isEqual:entry.partialImageContext.URL]; if (isFinalImage) { TIPPartialImageEntryContext * const partialImageContext = entry.partialImageContext; NSString * const entryIdentifier = entry.identifier; _networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming = partialImage; _networkContext.imageDownloadRequest.imageDownloadLastModified = partialImageContext.lastModified; TIPImageDiskCache * const diskCache = _imagePipeline.diskCache; if (diskCache) { TIPImageDiskCacheEntry *diskEntry; diskEntry = [diskCache imageEntryForIdentifier:entryIdentifier options:TIPImageDiskCacheFetchOptionTemporaryFile targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; TIPImageDiskCacheTemporaryFile *diskTempFile = diskEntry.tempFile; if (!diskTempFile) { diskTempFile = [_imagePipeline.diskCache openTemporaryFileForImageIdentifier:entry.identifier]; [diskTempFile appendData:partialImage.data]; } _networkContext.imageDownloadRequest.imageDownloadTemporaryFileForResuming = diskTempFile; } [self _background_processContinuedPartialEntry:partialImage URL:partialImageContext.URL loadSource:TIPImageLoadSourceMemoryCache]; _flags.shouldJumpToResumingDownload = 1; } } // continue [self _background_loadFromNextSource]; } - (void)_background_handleCompletedDiskEntry:(TIPImageDiskCacheEntry *)entry { if ([self _background_shouldAbort]) { return; } if (entry.completeImageContext) { NSURL *completeImageURL = entry.completeImageContext.URL; const CGSize dimensions = entry.completeImageContext.dimensions; const CGSize currentDimensions = self.previewResult.imageContainer.dimensions; const BOOL isFinal = [completeImageURL isEqual:self.imageURL]; if (isFinal || (dimensions.width * dimensions.height > currentDimensions.width * currentDimensions.height)) { // Metadata checks out, load the actual complete image entry = [_imagePipeline.diskCache imageEntryForIdentifier:entry.identifier options:TIPImageDiskCacheFetchOptionCompleteImage targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; if ([completeImageURL isEqual:entry.completeImageContext.URL]) { TIPImageContainer *image = entry.completeImage; if (image) { if (isFinal) { [self _background_updateFinalImage:image imageData:entry.completeImageData // ok if nil renderLatency:0 URL:completeImageURL loadSource:TIPImageLoadSourceDiskCache networkImageType:nil networkByteCount:0 placeholder:entry.completeImageContext.treatAsPlaceholder]; return; } if (!self.previewResult) { [self _background_updatePreviewImageWithCacheEntry:entry loadSource:TIPImageLoadSourceDiskCache]; return; } } } } } [self _background_handlePartialDiskEntry:entry tryOtherPipelineDiskCachesIfNeeded:YES]; } - (void)_background_handlePartialDiskEntry:(TIPImageDiskCacheEntry *)entry tryOtherPipelineDiskCachesIfNeeded:(BOOL)tryOtherPipelineDiskCachesIfNeeded { if ([self _background_shouldAbort]) { return; } if (entry.partialImageContext && [self supportsLoadingFromSource:TIPImageLoadSourceNetworkResumed]) { const CGSize dimensions = entry.completeImageContext.dimensions; const BOOL isFinal = [self.imageURL isEqual:entry.partialImageContext.URL]; BOOL isReasonableDataRemainingAndLarger = NO; [self _background_extractTargetInfo]; const BOOL couldBeReasonableDataRemainingAndLarger = !isFinal && TIPSizeGreaterThanZero(_targetDimensions) && ((dimensions.width * dimensions.height) > (_targetDimensions.width * _targetDimensions.height)); if (couldBeReasonableDataRemainingAndLarger) { const double ratio = (dimensions.width * dimensions.height) / (_targetDimensions.width * _targetDimensions.height); const NSUInteger remainingBytes = (entry.partialImageContext.expectedContentLength > entry.partialFileSize) ? entry.partialImageContext.expectedContentLength - entry.partialFileSize : NSUIntegerMax; NSUInteger hypotheticalBytes = (entry.partialImageContext.expectedContentLength) ?: 0; hypotheticalBytes = (NSUInteger)((double)hypotheticalBytes / ratio); isReasonableDataRemainingAndLarger = remainingBytes < hypotheticalBytes; } if (isFinal || isReasonableDataRemainingAndLarger) { // meta-data checks out, load the actual partial image entry = [_imagePipeline.diskCache imageEntryForIdentifier:entry.identifier options:(TIPImageDiskCacheFetchOptionPartialImage | TIPImageDiskCacheFetchOptionTemporaryFile) targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; if ([self.imageURL isEqual:entry.partialImageContext.URL] && entry.partialImage && entry.tempFile) { _networkContext.imageDownloadRequest.imageDownloadLastModified = entry.partialImageContext.lastModified; _networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming = entry.partialImage; _networkContext.imageDownloadRequest.imageDownloadTemporaryFileForResuming = entry.tempFile; [self _background_processContinuedPartialEntry:entry.partialImage URL:entry.partialImageContext.URL loadSource:TIPImageLoadSourceDiskCache]; _flags.shouldJumpToResumingDownload = 1; } } } if (tryOtherPipelineDiskCachesIfNeeded) { [self _background_loadFromOtherPipelineDisk]; } else { [self _background_loadFromNextSource]; } } #pragma mark Render Progress - (TIPImageContainer *)_background_transformAndScaleImageContainer:(TIPImageContainer *)imageContainer progress:(float)progress didTransform:(out BOOL *)transformedOut { TIPImageContainer *outputImage; if (imageContainer.isAnimated) { outputImage = [imageContainer scaleToTargetDimensions:_targetDimensions contentMode:_targetContentMode] ?: imageContainer; *transformedOut = NO; } else { UIImage *scaledImage = [self _background_transformAndScaleImage:imageContainer.image progress:progress didTransform:transformedOut]; outputImage = [[TIPImageContainer alloc] initWithImage:scaledImage]; } return outputImage; } - (UIImage *)_background_transformAndScaleImage:(UIImage *)image progress:(float)progress didTransform:(out BOOL *)transformedOut { *transformedOut = NO; [self _background_extractTargetInfo]; if (_transformer) { UIImage *transformedImage = [_transformer tip_transformImage:image withProgress:progress hintTargetDimensions:_targetDimensions hintTargetContentMode:_targetContentMode forImageFetchOperation:self]; if (transformedImage) { image = transformedImage; *transformedOut = YES; } } image = [image tip_scaledImageWithTargetDimensions:_targetDimensions contentMode:_targetContentMode]; TIPAssert(image != nil); return image; } - (void)_background_processContinuedPartialEntry:(TIPPartialImage *)partialImage URL:(NSURL *)URL loadSource:(TIPImageLoadSource)source { [self _background_validateProgressiveSupportWithPartialImage:partialImage]; // If we have a partial image with enough progress to display, let's decode it and use it as a progress image if (_flags.permitsProgressiveLoading && partialImage.frameCount > 0 && [self.delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadPreviewImage:completion:)]) { const uint64_t startMachTime = mach_absolute_time(); const TIPImageDecoderAppendResult givenResult = TIPImageDecoderAppendResultDidLoadFrame; UIImage *progressImage = [self _background_getNextProgressiveImageWithAppendResult:givenResult partialImage:partialImage renderCount:0]; if (progressImage) { const float progress = partialImage.progress; BOOL transformed = NO; progressImage = [self _background_transformAndScaleImage:progressImage progress:progress didTransform:&transformed]; const NSTimeInterval latency = TIPComputeDuration(startMachTime, mach_absolute_time()); [self _background_updateProgressiveImage:progressImage transformed:transformed renderLatency:latency URL:URL progress:progress sourcePartialImage:partialImage loadSource:source]; } } } - (nullable UIImage *)_background_getNextProgressiveImageWithAppendResult:(TIPImageDecoderAppendResult)appendResult partialImage:(TIPPartialImage *)partialImage renderCount:(NSUInteger)renderCount { [self _background_validateProgressiveSupportWithPartialImage:partialImage]; BOOL shouldRender = NO; TIPImageDecoderRenderMode mode = TIPImageDecoderRenderModeCompleteImage; if (partialImage.state > TIPPartialImageStateLoadingHeaders) { shouldRender = YES; if (_flags.permitsProgressiveLoading) { TIPImageFetchProgress fetchProgress = TIPImageFetchProgressNone; if (TIPImageDecoderAppendResultDidLoadFrame == appendResult) { fetchProgress = TIPImageFetchProgressFullFrame; } else if (partialImage.state > TIPPartialImageStateLoadingHeaders) { fetchProgress = TIPImageFetchProgressPartialFrame; } TIPImageFetchProgressUpdateBehavior behavior = TIPImageFetchProgressUpdateBehaviorNone; if (_progressiveLoadingPolicy) { behavior = [_progressiveLoadingPolicy tip_imageFetchOperation:self behaviorForProgress:fetchProgress frameCount:partialImage.frameCount progress:partialImage.progress type:partialImage.type dimensions:partialImage.dimensions renderCount:renderCount]; } switch (behavior) { case TIPImageFetchProgressUpdateBehaviorNone: shouldRender = NO; break; case TIPImageFetchProgressUpdateBehaviorUpdateWithAnyProgress: mode = TIPImageDecoderRenderModeAnyProgress; break; case TIPImageFetchProgressUpdateBehaviorUpdateWithFullFrameProgress: mode = TIPImageDecoderRenderModeFullFrameProgress; break; } } } TIPImageContainer *image = (shouldRender) ? [partialImage renderImageWithMode:mode targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoded:YES] : nil; return image.image; } - (nullable UIImage *)_background_getFirstFrameOfAnimatedImageIfNotYetProvided:(TIPPartialImage *)partialImage { if (partialImage.isAnimated && partialImage.frameCount >= 1 && !_flags.didReceiveFirstAnimatedFrame) { if ([self.delegate respondsToSelector:@selector(tip_imageFetchOperation:didLoadFirstAnimatedImageFrame:progress:)]) { TIPImageContainer *imageContainer = [partialImage renderImageWithMode:TIPImageDecoderRenderModeFullFrameProgress targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoded:NO]; if (imageContainer && !imageContainer.isAnimated) { // Provide the first frame if requested _flags.didReceiveFirstAnimatedFrame = 1; return imageContainer.image; } } } return nil; } #pragma mark Create Cache Entry - (nullable TIPImageCacheEntry *)_background_createCacheEntryUsingRawImage:(BOOL)useRawImage permitPreviewFallback:(BOOL)permitPreviewFallback didFallback:(nullable out BOOL *)didFallbackToPreviewOut { TIPImageCacheEntry *entry = nil; TIPImageContainer *image = (useRawImage) ? self.finalImageContainerRaw : self.finalResult.imageContainer; NSURL *imageURL = self.finalResult.imageURL; BOOL isPlaceholder = self.finalResult.imageIsTreatedAsPlaceholder; if (!image && permitPreviewFallback) { image = (useRawImage) ? self.previewImageContainerRaw : self.previewResult.imageContainer; imageURL = self.previewResult.imageURL; isPlaceholder = self.previewResult.imageIsTreatedAsPlaceholder; if (didFallbackToPreviewOut) { *didFallbackToPreviewOut = YES; } } if (image) { entry = [[TIPImageCacheEntry alloc] init]; TIPImageCacheEntryContext *context = nil; TIPCompleteImageEntryContext *completeContext = [[TIPCompleteImageEntryContext alloc] init]; completeContext.dimensions = image.dimensions; completeContext.animated = image.isAnimated; entry.completeImageContext = completeContext; entry.completeImage = image; context = completeContext; [self _hydrateNewContext:context imageURL:imageURL placeholder:isPlaceholder]; entry.identifier = self.imageIdentifier; } return entry; } - (nullable TIPImageCacheEntry *)_background_createCacheEntryFromPartialImage:(TIPPartialImage *)partialImage lastModified:(NSString *)lastModified imageURL:(NSURL *)imageURL { if (!partialImage) { return nil; } if (!lastModified) { return nil; } if (partialImage.state <= TIPPartialImageStateLoadingHeaders) { return nil; } if (TIP_BITMASK_HAS_SUBSET_FLAGS(_networkContext.imageDownloadRequest.imageDownloadOptions, TIPImageFetchTreatAsPlaceholder)) { return nil; } TIPImageCacheEntry *entry = [[TIPImageCacheEntry alloc] init]; TIPImageCacheEntryContext *context = nil; TIPPartialImageEntryContext *partialContext = [[TIPPartialImageEntryContext alloc] init]; partialContext.dimensions = partialImage.dimensions; partialContext.expectedContentLength = partialImage.expectedContentLength; partialContext.lastModified = lastModified; partialContext.animated = partialImage.isAnimated; entry.partialImageContext = partialContext; entry.partialImage = partialImage; context = partialContext; [self _hydrateNewContext:context imageURL:imageURL placeholder:NO]; entry.identifier = self.imageIdentifier; return entry; } #pragma mark Cache propagation - (void)_background_propagatePartialImage:(TIPPartialImage *)partialImage lastModified:(NSString *)lastModified wasResumed:(BOOL)wasResumed { [self _background_extractStorageInfo]; if (!_flags.didReceiveFirstByte || self.finalImageContainerRaw) { return; } TIPImageCacheEntry *entry = [self _background_createCacheEntryFromPartialImage:partialImage lastModified:lastModified imageURL:self.progressiveResult.imageURL]; TIPAssert(!entry || (entry.partialImage && entry.partialImageContext)); if (entry) { [_imagePipeline.memoryCache updateImageEntry:entry forciblyReplaceExisting:NO]; } } - (void)_background_propagatePreviewImage:(TIPImageLoadSource)source { if (TIPImageLoadSourceMemoryCache != source && TIPImageLoadSourceDiskCache != source) { // only memory/disk sources supported return; } [self _background_extractStorageInfo]; if (!self.previewImageContainerRaw || self.finalImageContainerRaw) { return; } TIPImageCacheEntry *entry = nil; // First, the memory cache (if coming from disk) if (TIPImageLoadSourceDiskCache == source) { entry = [self _background_createCacheEntryUsingRawImage:YES permitPreviewFallback:YES didFallback:NULL /*don't care*/]; TIPAssert(!entry || (entry.completeImage && entry.completeImageContext)); if (entry) { [_imagePipeline.memoryCache updateImageEntry:entry forciblyReplaceExisting:NO]; } } // Second, the rendered cache if (!_flags.shouldSkipRenderedCacheStore) { BOOL didFallbackToPreview = NO; entry = [self _background_createCacheEntryUsingRawImage:NO permitPreviewFallback:YES didFallback:&didFallbackToPreview]; TIPAssert(!entry || (entry.completeImage && entry.completeImageContext)); if (entry) { const CGSize rawSize = (didFallbackToPreview) ? self.previewImageContainerRaw.dimensions : self.finalImageContainerRaw.dimensions; const BOOL wasTransformed = (didFallbackToPreview) ? _flags.previewImageWasTransformed : _flags.finalImageWasTransformed; if (!wasTransformed || _transfomerIdentifier) { [_imagePipeline.renderedCache storeImageEntry:entry transformerIdentifier:(wasTransformed) ? _transfomerIdentifier : nil sourceImageDimensions:rawSize]; } } } } - (void)_background_propagateFinalImageData:(nullable NSData *)imageData loadSource:(TIPImageLoadSource)source { [self _background_extractStorageInfo]; if (!self.finalImageContainerRaw) { return; } TIPImageCacheEntry *entry = [self _background_createCacheEntryUsingRawImage:YES permitPreviewFallback:NO didFallback:NULL]; TIPAssert(!entry || (entry.completeImage && entry.completeImageContext)); if (entry) { switch (source) { case TIPImageLoadSourceMemoryCache: { [_imagePipeline.diskCache updateImageEntry:entry forciblyReplaceExisting:NO]; break; } case TIPImageLoadSourceDiskCache: case TIPImageLoadSourceNetwork: case TIPImageLoadSourceNetworkResumed: case TIPImageLoadSourceAdditionalCache: { if (imageData && !entry.completeImageData) { entry.completeImageData = imageData; } [_imagePipeline.memoryCache updateImageEntry:entry forciblyReplaceExisting:NO]; if (TIPImageLoadSourceNetwork == source || TIPImageLoadSourceNetworkResumed == source) { [_imagePipeline postCompletedEntry:entry manual:NO]; // the network will have already transitioned the disk entry to the disk cache // so there'se no need to update the image entry of the disk cache } break; } case TIPImageLoadSourceUnknown: return; } } // Always try to update the rendered cache [self _background_propagateFinalRenderedImage:source]; } - (void)_background_propagateFinalRenderedImage:(TIPImageLoadSource)source { if (_flags.finalImageWasTransformed && !_transfomerIdentifier) { return; } if (_flags.shouldSkipRenderedCacheStore) { return; } TIPImageCacheEntry *entry = [self _background_createCacheEntryUsingRawImage:NO permitPreviewFallback:NO didFallback:NULL]; TIPAssert(!entry || (entry.completeImage && entry.completeImageContext)); if (entry) { const CGSize rawSize = self.finalImageContainerRaw.dimensions; NSString *transformerIdentifier = (_flags.finalImageWasTransformed) ? _transfomerIdentifier : nil; [_imagePipeline.renderedCache storeImageEntry:entry transformerIdentifier:transformerIdentifier sourceImageDimensions:rawSize]; } } #pragma mark Execute - (void)_background_executeDelegateWork:(TIPImageFetchDelegateWorkBlock)block { id<TIPImageFetchDelegate> delegate = self.delegate; tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ block(delegate); }); } - (void)_executeBackgroundWork:(dispatch_block_t)block { tip_dispatch_async_autoreleasing(_backgroundQueue, block); } @end @implementation TIPImageFetchOperation (DiskCache) - (void)_diskCache_loadFromOtherPipelines:(NSArray<TIPImagePipeline *> *)pipelines startMachTime:(uint64_t)startMachTime { for (TIPImagePipeline *nextPipeline in pipelines) { // look in the pipeline's disk cache if ([self _diskCache_attemptLoadFromOtherPipelineDisk:nextPipeline startMachTime:startMachTime]) { // success! return; } } // Ran out of "next" pipelines, load from next source [self _diskCache_completeLoadFromOtherPipelineDisk:nil imageContainer:nil URL:nil latency:TIPComputeDuration(startMachTime, mach_absolute_time()) placeholder:NO]; } - (BOOL)_diskCache_attemptLoadFromOtherPipelineDisk:(TIPImagePipeline *)nextPipeline startMachTime:(uint64_t)startMachTime { TIPImageDiskCache *nextDiskCache = nextPipeline.diskCache; if (nextDiskCache) { // pull out the on disk path to the desired entry if available TIPCompleteImageEntryContext *context = nil; NSString *filePath = [nextDiskCache diskCache_imageEntryFilePathForIdentifier:self.imageIdentifier hitShouldMoveEntryToHead:NO context:&context]; // only accept an exact match (URLs are equal) if (filePath && [context.URL isEqual:self.imageURL]) { // pull out our pipeline's disk cache TIPImageDiskCache *thisDiskCache = _imagePipeline.diskCache; // override fetch values const TIPImageFetchOptions options = _networkContext.imageDownloadRequest.imageDownloadOptions; context.updateExpiryOnAccess = TIP_BITMASK_EXCLUDES_FLAGS(options, TIPImageFetchDoNotResetExpiryOnAccess); context.TTL = _networkContext.imageDownloadRequest.imageDownloadTTL; context.lastAccess = [NSDate date]; if (context.TTL <= 0.0) { context.TTL = TIPTimeToLiveDefault; } // leave context.treatAsPlaceholder as-is // create an entry TIPImageCacheEntry *entry = [[TIPImageCacheEntry alloc] init]; entry.identifier = self.imageIdentifier; entry.completeImageContext = context; entry.completeImageFilePath = filePath; // store the entry (via file path) to disk cache [thisDiskCache diskCache_updateImageEntry:entry forciblyReplaceExisting:!context.treatAsPlaceholder]; // complete the loop by retrieving entry (with UIImage) from disk cache entry = [thisDiskCache diskCache_imageEntryForIdentifier:entry.identifier options:TIPImageDiskCacheFetchOptionCompleteImage targetDimensions:_targetDimensions targetContentMode:_targetContentMode decoderConfigMap:_decoderConfigMap]; // did we get an image? TIPImageContainer *image = entry.completeImage; if (image) { // complete [self _diskCache_completeLoadFromOtherPipelineDisk:nextPipeline imageContainer:image URL:context.URL latency:TIPComputeDuration(startMachTime, mach_absolute_time()) placeholder:context.treatAsPlaceholder]; // success! return YES; } } } // didn't succeed return NO; } - (void)_diskCache_completeLoadFromOtherPipelineDisk:(nullable TIPImagePipeline *)pipeline imageContainer:(nullable TIPImageContainer *)imageContainer URL:(nullable NSURL *)URL latency:(NSTimeInterval)latency placeholder:(BOOL)placeholder { if (latency > 0.150) { TIPLogWarning(@"Other Pipeline Duration (%@): %.3fs", (imageContainer != nil) ? @"HIT" : @"MISS", latency); } else if (imageContainer) { TIPLogDebug(@"Other Pipeline Duration (HIT): %.3fs", latency); } [self _executeBackgroundWork:^{ if (imageContainer && URL) { [self _background_updateFinalImage:imageContainer imageData:nil renderLatency:0 URL:URL loadSource:TIPImageLoadSourceDiskCache networkImageType:nil networkByteCount:0 placeholder:placeholder]; } else { [self _background_loadFromNextSource]; } }]; } @end @implementation TIPImageFetchOperation (Testing) - (id<TIPImageDownloadContext>)associatedDownloadContext { return _networkContext.imageDownloadContext; } @end @implementation TIPImageFetchDelegateDeallocHandler { __weak TIPImageFetchOperation *_operation; Class _delegateClass; } - (instancetype)initWithFetchOperation:(TIPImageFetchOperation *)operation delegate:(id<TIPImageFetchDelegate>)delegate { if (self = [super init]) { _operation = operation; _delegateClass = [delegate class]; } return self; } - (void)invalidate { _operation = nil; } - (void)dealloc { TIPImageFetchOperation *op = _operation; if (op && !op.isFinished) { TIPLogInformation(@"%@<%@> deallocated, cancelling %@", NSStringFromClass(_delegateClass), NSStringFromProtocol(@protocol(TIPImageFetchDelegate)), op); [op cancel]; } } @end @implementation TIPImageFetchDownloadRequest - (instancetype)initWithRequest:(id<TIPImageFetchRequest>)fetchRequest { if (self = [super init]) { _imageDownloadPriority = NSOperationQueuePriorityNormal; _imageDownloadURL = [fetchRequest imageURL]; _imageDownloadIdentifier = [TIPImageFetchRequestGetImageIdentifier(fetchRequest) copy]; if ([fetchRequest respondsToSelector:@selector(decoderConfigMap)]) { _decoderConfigMap = [[fetchRequest decoderConfigMap] copy]; } } return self; } @end @implementation TIPImageFetchResultInternal @synthesize imageContainer = _imageContainer; @synthesize imageSource = _imageSource; @synthesize imageURL = _imageURL; @synthesize imageOriginalDimensions = _imageOriginalDimensions; @synthesize imageIsTreatedAsPlaceholder = _imageIsTreatedAsPlaceholder; @synthesize imageWasTransformed = _imageWasTransformed; @synthesize imageIdentifier = _imageIdentifier; + (nullable TIPImageFetchResultInternal *)resultWithImageContainer:(nullable TIPImageContainer *)imageContainer identifier:(nullable NSString *)identifier loadSource:(TIPImageLoadSource)source URL:(nullable NSURL *)URL originalDimensions:(CGSize)originalDimensions placeholder:(BOOL)placeholder transformed:(BOOL)transformed { if (!imageContainer || !URL || !identifier) { return nil; } return [[TIPImageFetchResultInternal alloc] initWithImageContainer:imageContainer identifier:identifier source:source URL:URL originalDimensions:originalDimensions placeholder:placeholder transformed:transformed]; } - (instancetype)initWithImageContainer:(TIPImageContainer *)imageContainer identifier:(NSString *)identifier source:(TIPImageLoadSource)source URL:(NSURL *)URL originalDimensions:(CGSize)originalDimensions placeholder:(BOOL)placeholder transformed:(BOOL)transformed TIP_OBJC_DIRECT { if (self = [super init]) { _imageContainer = imageContainer; _imageSource = source; _imageURL = URL; _imageOriginalDimensions = originalDimensions; _imageIsTreatedAsPlaceholder = placeholder; _imageWasTransformed = transformed; _imageIdentifier = [identifier copy]; } return self; } @end static NSQualityOfService ConvertNSOperationQueuePriorityToQualityOfService(NSInteger pri) { /* VLo Lo Nml Hi VHi -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 11 13 15 17 18 19 20 21 22 23 24 25 27 29 31 33 Bg Uti UIni UInt */ NSInteger qos = 1; if (pri <= -4) { qos = 17 - ((pri + 4) * 2); } else if (pri <= 4) { qos = 25 - (4 - pri); } else { qos = 25 + ((pri - 4) * 2); } if (qos < 1) { qos = 1; } return (NSQualityOfService)qos; } NS_ASSUME_NONNULL_END