TwitterImagePipeline/Project/TIPImageStoreAndMoveOperations.m (383 lines of code) (raw):
//
// TIPImageStoreAndMoveOperations.m
// TwitterImagePipeline
//
// Created on 1/13/16.
// Copyright © 2020 Twitter. All rights reserved.
//
#include <stdatomic.h>
#import "TIP_Project.h"
#import "TIPError.h"
#import "TIPGlobalConfiguration+Project.h"
#import "TIPImageDiskCache.h"
#import "TIPImageFetchRequest.h"
#import "TIPImageMemoryCache.h"
#import "TIPImagePipeline+Project.h"
#import "TIPImageRenderedCache.h"
#import "TIPImageStoreAndMoveOperations.h"
#import "UIImage+TIPAdditions.h"
// Static asserts to ensure the Fetch/Store options are 1:1 matching
TIPStaticAssert(TIPImageFetchNoOptions == TIPImageStoreNoOptions, NoOptionsMissmatch);
TIPStaticAssert(TIPImageFetchDoNotResetExpiryOnAccess == TIPImageStoreDoNotResetExpiryOnAccess, DoNotResetExpiryOnAccessMissmatch);
TIPStaticAssert(TIPImageFetchTreatAsPlaceholder == TIPImageStoreTreatAsPlaceholder, TreatAsPlaceholderMissmatch);
NS_ASSUME_NONNULL_BEGIN
@interface TIPImageStoreOperation ()
@property (tip_nonatomic_direct, readonly) id<TIPImageStoreRequest> request;
@property (tip_nonatomic_direct, readonly) TIPImagePipeline *pipeline;
@property (tip_nonatomic_direct, copy, readonly, nullable) TIPImagePipelineOperationCompletionBlock storeCompletionBlock;
@end
TIP_OBJC_DIRECT_MEMBERS
@interface TIPImageStoreOperation (Private)
- (nullable NSData *)_getImageData;
- (nullable NSString *)_getImageFilePath;
- (nullable TIPImageContainer *)_getImageContainer;
- (TIPCompleteImageEntryContext *)_getEntryContextWithImageURL:(NSURL *)imageURL
imageContainer:(nullable TIPImageContainer *)imageContainer
imageFilePath:(nullable NSString *)imageFilePath
imageData:(nullable NSData *)imageData;
@end
@implementation TIPDisabledExternalMutabilityOperation
- (void)_tip_addDependency:(NSOperation *)op
{
[super addDependency:op];
}
- (void)makeDependencyOfTargetOperation:(NSOperation *)op
{
[op addDependency:self];
}
- (void)cancel
{
[super doesNotRecognizeSelector:_cmd];
}
- (void)addDependency:(NSOperation *)op
{
[super doesNotRecognizeSelector:_cmd];
}
- (void)removeDependency:(NSOperation *)op
{
[super doesNotRecognizeSelector:_cmd];
}
- (void)setQueuePriority:(NSOperationQueuePriority)queuePriority
{
[super doesNotRecognizeSelector:_cmd];
}
- (void)setCompletionBlock:(nullable void (^)(void))completionBlock
{
[super doesNotRecognizeSelector:_cmd];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (void)setThreadPriority:(double)threadPriority
#pragma clang diagnostic pop
{
[super doesNotRecognizeSelector:_cmd];
}
- (void)setQualityOfService:(NSQualityOfService)qualityOfService
{
[super doesNotRecognizeSelector:_cmd];
}
@end
@implementation TIPImageStoreOperation
{
TIPImageStoreHydrationOperation *_hydrationOperation;
}
- (instancetype)initWithRequest:(id<TIPImageStoreRequest>)request
pipeline:(TIPImagePipeline *)pipeline
completion:(nullable TIPImagePipelineOperationCompletionBlock)completion
{
if (self = [super init]) {
_request = request;
_pipeline = pipeline;
_storeCompletionBlock = [completion copy];
}
return self;
}
- (void)setHydrationDependency:(TIPImageStoreHydrationOperation *)dependency
{
if (_hydrationOperation) {
return;
}
_hydrationOperation = dependency;
[super _tip_addDependency:dependency];
}
- (void)main
{
@autoreleasepool {
void (^completion)(TIPImageCacheEntry * __nullable, NSError * __nullable);
completion = ^(TIPImageCacheEntry * __nullable completedEntry,
NSError * __nullable completedError) {
TIPAssert((completedEntry != nil) ^ (completedError != nil));
if (completedEntry) {
[self.pipeline postCompletedEntry:completedEntry manual:YES];
}
TIPImagePipelineOperationCompletionBlock block = self.storeCompletionBlock;
if (block) {
const BOOL success = completedEntry != nil;
tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
block(self, success, completedError);
});
}
};
// Check hydration
if (_hydrationOperation) {
NSError *hydrationError = _hydrationOperation.error;
if (hydrationError) {
completion(nil, hydrationError);
return;
} else if (_hydrationOperation.hydratedRequest) {
_request = _hydrationOperation.hydratedRequest;
}
}
// Confirm Caches
if (!_pipeline.diskCache && !_pipeline.memoryCache) {
completion(nil, [NSError errorWithDomain:TIPImageStoreErrorDomain
code:TIPImageStoreErrorCodeNoCacheForStoring
userInfo:nil]);
return;
}
// Pull out image info
NSData *imageData = [self _getImageData];
NSString *imageFilePath = [self _getImageFilePath];
TIPImageContainer *imageContainer = [self _getImageContainer];
// Validate image info
TIPAssertMessage(imageContainer != nil || imageData != nil || imageFilePath != nil, @"%@ didn't have any image info", NSStringFromClass([_request class]));
if (!imageContainer && !imageData && !imageFilePath) {
completion(nil, [NSError errorWithDomain:TIPImageStoreErrorDomain
code:TIPImageStoreErrorCodeImageNotProvided
userInfo:nil]);
return;
}
// Pull out and validate URL
NSURL *imageURL = _request.imageURL;
TIPAssert(imageURL != nil);
if (!imageURL) {
completion(nil, [NSError errorWithDomain:TIPImageStoreErrorDomain
code:TIPImageStoreErrorCodeImageURLNotProvided
userInfo:nil]);
return;
}
// Pull out the identifier
NSString *identifier = TIPImageStoreRequestGetImageIdentifier(_request);
// Create context
TIPCompleteImageEntryContext *context = [self _getEntryContextWithImageURL:imageURL
imageContainer:imageContainer
imageFilePath:imageFilePath
imageData:imageData];
// Create Memory Entry
TIPImageCacheEntry *memoryEntry = nil;
if (_pipeline.memoryCache && imageData) {
memoryEntry = [[TIPImageCacheEntry alloc] init];
memoryEntry.completeImageData = imageData;
memoryEntry.completeImageContext = [context copy];
memoryEntry.identifier = identifier;
}
// Create Disk Entry
TIPImageCacheEntry *diskEntry = nil;
if (_pipeline.diskCache) {
diskEntry = [[TIPImageCacheEntry alloc] init];
if (imageFilePath && ([[NSFileManager defaultManager] fileExistsAtPath:imageFilePath] || (!imageData && !imageContainer))) {
diskEntry.completeImageFilePath = imageFilePath;
} else if (imageData) {
diskEntry.completeImageData = imageData;
} else {
TIPAssert(imageContainer);
diskEntry.completeImage = imageContainer;
TIPAssert(diskEntry.completeImage);
}
diskEntry.completeImageContext = [context copy];
diskEntry.identifier = identifier;
}
// Update caches
[_pipeline.renderedCache clearImageWithIdentifier:identifier];
if (diskEntry) {
[_pipeline.diskCache updateImageEntry:diskEntry
forciblyReplaceExisting:!context.treatAsPlaceholder];
} else {
// we always have a disk entry when there's a disk cache
TIPAssert(_pipeline.diskCache == nil);
}
if (memoryEntry) {
[_pipeline.memoryCache updateImageEntry:memoryEntry
forciblyReplaceExisting:!context.treatAsPlaceholder];
} else {
// no memory entry, clear it so it can load from disk instead
[_pipeline.memoryCache clearImageWithIdentifier:identifier];
}
completion(memoryEntry ?: diskEntry, nil);
}
}
@end
@implementation TIPImageStoreOperation (Private)
- (nullable NSData *)_getImageData
{
return [_request respondsToSelector:@selector(imageData)] ? _request.imageData : nil;
}
- (nullable NSString *)_getImageFilePath
{
return [_request respondsToSelector:@selector(imageFilePath)] ? _request.imageFilePath : nil;
}
- (nullable TIPImageContainer *)_getImageContainer
{
TIPImageContainer *imageContainer = nil;
if ([_request respondsToSelector:@selector(image)]) {
UIImage *image = _request.image;
if (image.CIImage) {
image = [image tip_CGImageBackedImageAndReturnError:NULL];
}
if (image) {
if (image.images.count > 0) {
NSUInteger loopCount = [_request respondsToSelector:@selector(animationLoopCount)] ? _request.animationLoopCount : 0;
NSArray<NSNumber *> *durations = [_request respondsToSelector:@selector(animationFrameDurations)] ? _request.animationFrameDurations : nil;
imageContainer = [[TIPImageContainer alloc] initWithAnimatedImage:image
loopCount:loopCount
frameDurations:durations];
} else {
imageContainer = [[TIPImageContainer alloc] initWithImage:image];
}
TIPAssert(imageContainer != nil);
}
}
return imageContainer;
}
- (TIPCompleteImageEntryContext *)_getEntryContextWithImageURL:(NSURL *)imageURL
imageContainer:(nullable TIPImageContainer *)imageContainer
imageFilePath:(nullable NSString *)imageFilePath
imageData:(nullable NSData *)imageData
{
TIPCompleteImageEntryContext *context = [[TIPCompleteImageEntryContext alloc] init];
const TIPImageStoreOptions options = [_request respondsToSelector:@selector(options)] ?
[_request options] :
TIPImageStoreNoOptions;
context.updateExpiryOnAccess = TIP_BITMASK_EXCLUDES_FLAGS(options, TIPImageStoreDoNotResetExpiryOnAccess);
context.treatAsPlaceholder = TIP_BITMASK_HAS_SUBSET_FLAGS(options, TIPImageStoreTreatAsPlaceholder);
context.TTL = [_request respondsToSelector:@selector(timeToLive)] ? [_request timeToLive] : -1.0;
if (context.TTL <= 0.0) {
context.TTL = TIPTimeToLiveDefault;
}
context.URL = imageURL;
if (imageContainer) {
context.dimensions = imageContainer.dimensions;
} else if ([_request respondsToSelector:@selector(imageDimensions)]) {
context.dimensions = _request.imageDimensions;
} else if (imageData) {
context.dimensions = TIPDetectImageDataDimensions(imageData);
} else if (imageFilePath) {
context.dimensions = TIPDetectImageFileDimensions(imageFilePath);
}
if ([_request respondsToSelector:@selector(imageType)]) {
context.imageType = [_request imageType];
}
if (imageContainer) {
context.animated = imageContainer.isAnimated;
} else {
if ([context.imageType isEqualToString:TIPImageTypeGIF]) {
context.animated = YES;
}
}
return context;
}
@end
@implementation TIPImageStoreHydrationOperation
{
id<TIPImageStoreRequest> _request;
TIPImagePipeline *_pipeline;
id<TIPImageStoreRequestHydrater> _hydrater;
volatile atomic_bool _isFinished;
volatile atomic_bool _isExecuting;
volatile atomic_bool _didStart;
}
- (instancetype)initWithRequest:(id<TIPImageStoreRequest>)request
pipeline:(TIPImagePipeline *)pipeline
hydrater:(id<TIPImageStoreRequestHydrater>)hydrater
{
TIPAssert(request);
TIPAssert(pipeline);
TIPAssert(hydrater);
if (!request || !pipeline || !hydrater) {
return nil;
}
if (self = [super init]) {
_request = request;
_pipeline = pipeline;
_hydrater = hydrater;
atomic_init(&_isFinished, false);
atomic_init(&_isExecuting, false);
atomic_init(&_didStart, false);
}
return self;
}
- (BOOL)isAsynchronous
{
return YES;
}
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isExecuting
{
return atomic_load(&_isExecuting);
}
- (BOOL)isFinished
{
return atomic_load(&_isFinished);
}
- (void)start
{
tip_defer(^{
atomic_store(&(self->_didStart), true);
});
[self willChangeValueForKey:@"isExecuting"];
atomic_store(&_isExecuting, true);
[self didChangeValueForKey:@"isExecuting"];
[_hydrater tip_hydrateImageStoreRequest:_request
imagePipeline:_pipeline
completion:^(id<TIPImageStoreRequest> newRequest, NSError *error) {
[self _completeHydrationWithNewRequest:newRequest error:error];
}];
}
- (void)_completeHydrationWithNewRequest:(nullable id<TIPImageStoreRequest>)request
error:(nullable NSError *)error TIP_OBJC_DIRECT
{
if (false == atomic_load(&_didStart)) {
// Completed synchronously, don't want to mess up "isAsynchronous" behavior
[[TIPGlobalConfiguration sharedInstance] enqueueImagePipelineOperation:[NSBlockOperation blockOperationWithBlock:^{
[self _completeHydrationWithNewRequest:request error:error];
}]];
return;
}
if (error) {
_error = error;
} else {
_hydratedRequest = request ?: _request;
}
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
atomic_store(&_isExecuting, false);
atomic_store(&_isFinished, true);
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
@implementation TIPImageMoveOperation
{
TIPImagePipelineOperationCompletionBlock _completion;
}
- (instancetype)initWithPipeline:(TIPImagePipeline *)pipeline
originalIdentifier:(NSString *)oldIdentifier
updatedIdentifier:(NSString *)newIdentifier
completion:(nullable TIPImagePipelineOperationCompletionBlock)completion
{
TIPAssert(pipeline != nil);
if (self = [super init]) {
_pipeline = pipeline;
_originalIdentifier = [oldIdentifier copy];
_updatedIdentifier = [newIdentifier copy];
_completion = [completion copy];
}
return self;
}
- (void)main
{
NSError *error = nil;
TIPImageDiskCache *cache = _pipeline.diskCache;
if (!cache) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EINVAL
userInfo:nil];
} else {
const BOOL success = [cache renameImageEntryWithIdentifier:_originalIdentifier
toIdentifier:_updatedIdentifier error:&error];
TIPAssert(!success ^ !error);
if (success) {
[_pipeline clearImageWithIdentifier:_originalIdentifier];
}
}
TIPImagePipelineOperationCompletionBlock completion = _completion;
if (completion) {
tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
completion(self, !error, error);
});
}
}
@end
NS_ASSUME_NONNULL_END