TwitterImagePipeline/TIPGlobalConfiguration.m (613 lines of code) (raw):

// // TIPGlobalConfiguration.m // TwitterImagePipeline // // Created on 10/1/15. // Copyright © 2020 Twitter. All rights reserved. // #include <pthread.h> #include <objc/runtime.h> #import <UIKit/UITraitCollection.h> #import "TIP_Project.h" #import "TIPError.h" #import "TIPGlobalConfiguration+Project.h" #import "TIPImageCache.h" #import "TIPImageDiskCache.h" #import "TIPImageFetchDownloadInternal.h" #import "TIPImageFetchOperation.h" #import "TIPImageMemoryCache.h" #import "TIPImagePipeline+Project.h" #import "TIPImageRenderedCache.h" #import "TIPImageStoreAndMoveOperations.h" NS_ASSUME_NONNULL_BEGIN SInt64 const TIPMaxBytesForAllRenderedCachesDefault = -1; SInt64 const TIPMaxBytesForAllMemoryCachesDefault = -1; SInt64 const TIPMaxBytesForAllDiskCachesDefault = -1; SInt16 const TIPMaxCountForAllMemoryCachesDefault = INT16_MAX >> 7; SInt16 const TIPMaxCountForAllRenderedCachesDefault = INT16_MAX >> 7; SInt16 const TIPMaxCountForAllDiskCachesDefault = INT16_MAX >> 4; NSInteger const TIPMaxConcurrentImagePipelineDownloadCountDefault = 4; NSUInteger const TIPMaxRatioSizeOfCacheEntryDefault = 6; // Cap the default max memory bytes at 160MB (to be split equally betweet Rendered and Memory caches) -- a reasonable limit for devices with lots of RAM since iOS still enforces memory warnings even if the device has much more RAM available #define DEFAULT_MAX_RENDERED_BYTES_CAP (160ull * 1024ull * 1024ull) // Default the max bytes for in memory caching to 1/12th the devices RAM (to be split equally betweet Rendered and Memory caches) #define DEFAULT_MAX_RENDERED_BYTES_DIVISOR (12ull) // Arbitrarily default the max bytes for on disk caching to 128MBs (roughly 64 large images or 1,600 small images or 32,000 73x73 avatars) #define DEFAULT_MAX_DISK_BYTES (128ull * 1024ull * 1024ull) NS_INLINE SInt64 _MaxBytesForAllRenderedCachesDefaultValue() { return (SInt64)MIN([[NSProcessInfo processInfo] physicalMemory] / DEFAULT_MAX_RENDERED_BYTES_DIVISOR, DEFAULT_MAX_RENDERED_BYTES_CAP) / 2; } NS_INLINE SInt64 _MaxBytesForAllMemoryCachesDefaultValue() { return (SInt64)48ull * 1024ull * 1024ull; } NS_INLINE SInt64 _MaxBytesForAllDiskCachesDefaultValue() { return (SInt64)DEFAULT_MAX_DISK_BYTES; } @implementation TIPGlobalConfiguration { NSOperationQueue *_sharedImagePipelineQueue; dispatch_queue_t _globalObserversQueue; dispatch_queue_t _queueForMemoryCaches; dispatch_queue_t _queueForDiskCaches; NSHashTable<id<TIPImagePipelineObserver>> *_globalObservers; } @synthesize imageFetchDownloadProvider = _imageFetchDownloadProvider; - (void)setInternalTotalBytesForAllDiskCaches:(SInt64)internalTotalBytesForAllDiskCaches { TIPAssert(internalTotalBytesForAllDiskCaches >= 0); _internalTotalBytesForAllDiskCaches = internalTotalBytesForAllDiskCaches; } - (void)setInternalTotalBytesForAllMemoryCaches:(SInt64)internalTotalBytesForAllMemoryCaches { TIPAssert(internalTotalBytesForAllMemoryCaches >= 0); _internalTotalBytesForAllMemoryCaches = internalTotalBytesForAllMemoryCaches; } - (void)setInternalTotalBytesForAllRenderedCaches:(SInt64)internalTotalBytesForAllRenderedCaches { TIPAssert(internalTotalBytesForAllRenderedCaches >= 0); _internalTotalBytesForAllRenderedCaches = internalTotalBytesForAllRenderedCaches; } - (void)setInternalMaxCountForAllDiskCaches:(SInt16)internalMaxCountForAllDiskCaches { TIPAssert(internalMaxCountForAllDiskCaches >= 0); _internalMaxCountForAllDiskCaches = internalMaxCountForAllDiskCaches; } - (void)setInternalMaxCountForAllMemoryCaches:(SInt16)internalMaxCountForAllMemoryCaches { TIPAssert(internalMaxCountForAllMemoryCaches >= 0); _internalMaxCountForAllMemoryCaches = internalMaxCountForAllMemoryCaches; } - (void)setInternalMaxCountForAllRenderedCaches:(SInt16)internalMaxCountForAllRenderedCaches { TIPAssert(internalMaxCountForAllRenderedCaches >= 0); _internalMaxCountForAllRenderedCaches = internalMaxCountForAllRenderedCaches; } - (nonnull instancetype)initInternal { if (self = [super init]) { _internalMaxBytesForAllDiskCaches = _MaxBytesForAllDiskCachesDefaultValue(); _internalMaxBytesForAllMemoryCaches = _MaxBytesForAllMemoryCachesDefaultValue(); _internalMaxBytesForAllRenderedCaches = _MaxBytesForAllRenderedCachesDefaultValue(); _internalMaxCountForAllDiskCaches = TIPMaxCountForAllDiskCachesDefault; _internalMaxCountForAllMemoryCaches = TIPMaxCountForAllMemoryCachesDefault; _internalMaxCountForAllRenderedCaches = TIPMaxCountForAllRenderedCachesDefault; _maxConcurrentImagePipelineDownloadCount = TIPMaxConcurrentImagePipelineDownloadCountDefault; _maxRatioSizeOfCacheEntry = TIPMaxRatioSizeOfCacheEntryDefault; _clearMemoryCachesOnApplicationBackgroundEnabled = NO; _serializeCGContextAccess = YES; _queueForDiskCaches = dispatch_queue_create("tip.global.disk.cache.queue", DISPATCH_QUEUE_SERIAL); _queueForMemoryCaches = dispatch_queue_create("tip.global.memory.cache.queue", DISPATCH_QUEUE_SERIAL); _globalObserversQueue = dispatch_queue_create("tip.global.obervers.accessor.queue", DISPATCH_QUEUE_CONCURRENT); _sharedImagePipelineQueue = [[NSOperationQueue alloc] init]; _sharedImagePipelineQueue.name = @"tip.global.image.pipeline.operation.queue"; _sharedImagePipelineQueue.qualityOfService = NSQualityOfServiceUtility; // Don't let TIP get overwhelmed with fetch requests #if __LP64__ _sharedImagePipelineQueue.maxConcurrentOperationCount = 6; #else _sharedImagePipelineQueue.maxConcurrentOperationCount = 4; #endif _globalObservers = [NSHashTable<id<TIPImagePipelineObserver>> weakObjectsHashTable]; self.imageFetchDownloadProvider = nil; (void)TIPIsExtension(); // cache if we're an extension } return self; } + (instancetype)sharedInstance { static TIPGlobalConfiguration *sConfig; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sConfig = [[TIPGlobalConfiguration alloc] initInternal]; }); return sConfig; } - (void)setMaxBytesForAllRenderedCaches:(SInt64)maxBytes { if ([NSThread isMainThread]) { self.internalMaxBytesForAllRenderedCaches = (maxBytes >= 0ll) ? maxBytes : _MaxBytesForAllRenderedCachesDefaultValue(); [self pruneAllCachesOfType:TIPImageCacheTypeRendered withPriorityCache:nil]; } else { tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ self.maxBytesForAllRenderedCaches = maxBytes; }); } } - (SInt64)maxBytesForAllRenderedCaches { if (![NSThread isMainThread]) { TIPLogWarning(@"Read %@ from %@ off the main thread!", NSStringFromSelector(_cmd), NSStringFromClass([self class])); } return self.internalMaxBytesForAllRenderedCaches; } - (void)setMaxCountForAllRenderedCaches:(SInt16)maxCount { if ([NSThread isMainThread]) { self.internalMaxCountForAllRenderedCaches = (maxCount >= 0) ? maxCount : TIPMaxCountForAllRenderedCachesDefault; [self pruneAllCachesOfType:TIPImageCacheTypeRendered withPriorityCache:nil]; } else { tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ self.maxCountForAllRenderedCaches = maxCount; }); } } - (SInt16)maxCountForAllRenderedCaches { if (![NSThread isMainThread]) { TIPLogWarning(@"Read %@ from %@ off the main thread!", NSStringFromSelector(_cmd), NSStringFromClass([self class])); } return self.internalMaxCountForAllRenderedCaches; } - (void)setMaxBytesForAllMemoryCaches:(SInt64)maxBytes { tip_dispatch_async_autoreleasing(_queueForMemoryCaches, ^{ self.internalMaxBytesForAllMemoryCaches = (maxBytes >= 0ll) ? maxBytes : _MaxBytesForAllMemoryCachesDefaultValue(); [self pruneAllCachesOfType:TIPImageCacheTypeMemory withPriorityCache:nil]; }); } - (SInt64)maxBytesForAllMemoryCaches { __block SInt64 maxBytes; dispatch_sync(_queueForMemoryCaches, ^{ maxBytes = self.internalMaxBytesForAllMemoryCaches; }); return maxBytes; } - (void)setMaxCountForAllMemoryCaches:(SInt16)maxCount { tip_dispatch_async_autoreleasing(_queueForMemoryCaches, ^{ self.internalMaxCountForAllMemoryCaches = (maxCount >= 0) ? maxCount : TIPMaxCountForAllMemoryCachesDefault; [self pruneAllCachesOfType:TIPImageCacheTypeMemory withPriorityCache:nil]; }); } - (SInt16)maxCountForAllMemoryCaches { __block SInt16 maxCount; dispatch_sync(_queueForMemoryCaches, ^{ maxCount = self.internalMaxCountForAllMemoryCaches; }); return maxCount; } - (void)setMaxBytesForAllDiskCaches:(SInt64)maxBytes { tip_dispatch_async_autoreleasing(_queueForDiskCaches, ^{ self.internalMaxBytesForAllDiskCaches = (maxBytes >= 0ll) ? maxBytes : _MaxBytesForAllDiskCachesDefaultValue(); [self pruneAllCachesOfType:TIPImageCacheTypeDisk withPriorityCache:nil]; }); } - (SInt64)maxBytesForAllDiskCaches { __block SInt64 maxBytes; tip_dispatch_sync_autoreleasing(_queueForDiskCaches, ^{ maxBytes = self.internalMaxBytesForAllDiskCaches; }); return maxBytes; } - (void)setMaxCountForAllDiskCaches:(SInt16)maxCount { tip_dispatch_async_autoreleasing(_queueForDiskCaches, ^{ self.internalMaxCountForAllDiskCaches = (maxCount >= 0) ? maxCount : TIPMaxCountForAllDiskCachesDefault; [self pruneAllCachesOfType:TIPImageCacheTypeDisk withPriorityCache:nil]; }); } - (SInt16)maxCountForAllDiskCaches { __block SInt16 maxCount; dispatch_sync(_queueForDiskCaches, ^{ maxCount = self.internalMaxCountForAllDiskCaches; }); return maxCount; } - (SInt64)totalBytesForAllRenderedCaches { if (![NSThread isMainThread]) { TIPLogWarning(@"Read %@ from %@ off the main thread!", NSStringFromSelector(_cmd), NSStringFromClass([self class])); } return self.internalTotalBytesForAllRenderedCaches; } - (SInt16)totalCountForAllRenderedCaches { if (![NSThread isMainThread]) { TIPLogWarning(@"Read %@ from %@ off the main thread!", NSStringFromSelector(_cmd), NSStringFromClass([self class])); } return self.internalTotalCountForAllRenderedCaches; } - (SInt64)totalBytesForAllMemoryCaches { __block SInt64 totalBytes; dispatch_sync(_queueForMemoryCaches, ^{ totalBytes = self.internalTotalBytesForAllMemoryCaches; }); return totalBytes; } - (SInt16)totalCountForAllMemoryCaches { __block SInt16 totalCount; dispatch_sync(_queueForMemoryCaches, ^{ totalCount = self.internalTotalCountForAllMemoryCaches; }); return totalCount; } - (SInt64)totalBytesForAllDiskCaches { __block SInt64 totalBytes; dispatch_sync(_queueForDiskCaches, ^{ totalBytes = self.internalTotalBytesForAllDiskCaches; }); return totalBytes; } - (SInt16)totalCountForAllDiskCaches { __block SInt16 totalCount; dispatch_sync(_queueForDiskCaches, ^{ totalCount = self.internalTotalCountForAllDiskCaches; }); return totalCount; } #pragma mark Instance Methods - (void)clearAllDiskCaches { [[TIPImagePipeline allRegisteredImagePipelines] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TIPImagePipeline * _Nonnull pipeline, BOOL * _Nonnull stop) { [pipeline clearDiskCache]; }]; } - (void)clearAllMemoryCaches { [[TIPImagePipeline allRegisteredImagePipelines] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TIPImagePipeline * _Nonnull pipeline, BOOL * _Nonnull stop) { [pipeline clearMemoryCaches]; }]; } - (void)clearAllRenderedMemoryCaches { [[TIPImagePipeline allRegisteredImagePipelines] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TIPImagePipeline * _Nonnull pipeline, BOOL * _Nonnull stop) { [pipeline.renderedCache clearAllImages:NULL]; }]; } - (void)clearAllRenderedMemoryCacheImagesWithIdentifier:(NSString *)identifier { [[TIPImagePipeline allRegisteredImagePipelines] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TIPImagePipeline * _Nonnull pipeline, BOOL * _Nonnull stop) { [pipeline clearRenderedMemoryCacheImageWithIdentifier:identifier]; }]; } - (void)dirtyAllRenderedMemoryCacheImagesWithIdentifier:(NSString *)identifier { [[TIPImagePipeline allRegisteredImagePipelines] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, TIPImagePipeline * _Nonnull pipeline, BOOL * _Nonnull stop) { [pipeline dirtyRenderedMemoryCacheImageWithIdentifier:identifier]; }]; } #pragma mark Observing - (void)addImagePipelineObserver:(id<TIPImagePipelineObserver>)observer { tip_dispatch_barrier_async_autoreleasing(_globalObserversQueue, ^{ [self->_globalObservers addObject:observer]; }); } - (void)removeImagePipelineObserver:(id<TIPImagePipelineObserver>)observer { tip_dispatch_barrier_async_autoreleasing(_globalObserversQueue, ^{ [self->_globalObservers removeObject:observer]; }); } - (NSArray<id<TIPImagePipelineObserver>> *)allImagePipelineObservers { __block NSArray<id<TIPImagePipelineObserver>> *observers; tip_dispatch_sync_autoreleasing(_globalObserversQueue, ^{ observers = self->_globalObservers.allObjects; }); return observers; } #pragma mark Project Dispatch Methods - (dispatch_queue_t)queueForCachesOfType:(TIPImageCacheType)type { switch (type) { case TIPImageCacheTypeMemory: return _queueForMemoryCaches; case TIPImageCacheTypeDisk: return _queueForDiskCaches; case TIPImageCacheTypeRendered: default: return dispatch_get_main_queue(); } } #pragma mark Project Instance Methods - (SInt16)internalMaxCountForAllCachesOfType:(TIPImageCacheType)type { switch (type) { case TIPImageCacheTypeRendered: return self.internalMaxCountForAllRenderedCaches; case TIPImageCacheTypeMemory: return self.internalMaxCountForAllMemoryCaches; case TIPImageCacheTypeDisk: return self.internalMaxCountForAllDiskCaches; } return 0; } - (SInt16)internalTotalCountForAllCachesOfType:(TIPImageCacheType)type { switch (type) { case TIPImageCacheTypeRendered: return self.internalTotalCountForAllRenderedCaches; case TIPImageCacheTypeMemory: return self.internalTotalCountForAllMemoryCaches; case TIPImageCacheTypeDisk: return self.internalTotalCountForAllDiskCaches; } return 0; } - (SInt64)internalMaxBytesForAllCachesOfType:(TIPImageCacheType)type { switch (type) { case TIPImageCacheTypeRendered: return self.internalMaxBytesForAllRenderedCaches; case TIPImageCacheTypeMemory: return self.internalMaxBytesForAllMemoryCaches; case TIPImageCacheTypeDisk: return self.internalMaxBytesForAllDiskCaches; } return 0; } - (SInt64)internalTotalBytesForAllCachesOfType:(TIPImageCacheType)type { switch (type) { case TIPImageCacheTypeRendered: return self.internalTotalBytesForAllRenderedCaches; case TIPImageCacheTypeMemory: return self.internalTotalBytesForAllMemoryCaches; case TIPImageCacheTypeDisk: return self.internalTotalBytesForAllDiskCaches; } return 0; } - (SInt64)internalMaxBytesForCacheEntryOfType:(TIPImageCacheType)type { const SInt64 maxBytes = [self internalMaxBytesForAllCachesOfType:type]; if (maxBytes < 0) { // negative == unlimited return INT64_MAX; } // if on the main thread, accept potentially stale max ratio size by using nonatomic access NSInteger ratio = [NSThread isMainThread] ? _maxRatioSizeOfCacheEntry : self.maxRatioSizeOfCacheEntry; if (ratio < 0) { // negative == use default ratio = TIPMaxRatioSizeOfCacheEntryDefault; } if (ratio <= 1) { // 0 or 1 == no maximium ratio, aka, no max bytes return INT64_MAX; } return maxBytes / (SInt64)ratio; } - (void)enqueueImagePipelineOperation:(NSOperation *)op { [_sharedImagePipelineQueue addOperation:op]; } - (void)postProblem:(NSString *)problemName userInfo:(NSDictionary<NSString *, id> *)userInfo { id<TIPProblemObserver> observer = self.problemObserver; if (observer && [observer respondsToSelector:@selector(tip_problemWasEncountered:userInfo:)]) { [observer tip_problemWasEncountered:problemName userInfo:userInfo]; } } - (void)accessedCGContext:(BOOL)seriallyAccessed duration:(NSTimeInterval)duration isMainThread:(BOOL)mainThread { id<TIPProblemObserver> observer = self.problemObserver; if (observer && [observer respondsToSelector:@selector(tip_CGContextAccessed:serially:fromMainThread:)]) { [observer tip_CGContextAccessed:duration serially:seriallyAccessed fromMainThread:mainThread]; } } #pragma mark Max Bytes - (SInt64)internalMaxBytesForDiskCacheEntry { return [self internalMaxBytesForCacheEntryOfType:TIPImageCacheTypeDisk]; } - (SInt64)internalMaxBytesForMemoryCacheEntry { return [self internalMaxBytesForCacheEntryOfType:TIPImageCacheTypeMemory]; } - (SInt64)internalMaxBytesForRenderedCacheEntry { return [self internalMaxBytesForCacheEntryOfType:TIPImageCacheTypeRendered]; } #pragma mark Project Class Methods - (void)pruneAllCachesOfType:(TIPImageCacheType)type withPriorityCache:(nullable id<TIPImageCache>)priorityCache { const SInt64 globalMaxBytes = [self internalMaxBytesForAllCachesOfType:type]; const SInt16 globalMaxCount = [self internalMaxCountForAllCachesOfType:type]; [self pruneAllCachesOfType:type withPriorityCache:priorityCache toGlobalMaxBytes:globalMaxBytes toGlobalMaxCount:globalMaxCount]; } - (void)pruneAllCachesOfType:(TIPImageCacheType)type withPriorityCache:(nullable id<TIPImageCache>)priorityCache toGlobalMaxBytes:(SInt64)globalMaxBytes toGlobalMaxCount:(SInt16)globalMaxCount { @autoreleasepool { switch (type) { case TIPImageCacheTypeRendered: case TIPImageCacheTypeMemory: case TIPImageCacheTypeDisk: break; default: TIPAssertNever(); return; } TIPAssert(globalMaxBytes >= 0); TIPAssert(globalMaxCount >= 0); // max bytes of 0 == disable the cache if (globalMaxBytes == 0) { // leave as 0 } // max count of 0 == unlimited if (globalMaxCount == 0) { globalMaxCount = INT16_MAX; } NSArray<TIPImagePipeline *> *allPipelines = nil; NSInteger knownTotalEntries = -1; NSInteger knownPriorityEntries = 0; if (priorityCache) { TIPLRUCache *manifest = (TIPImageCacheTypeDisk == type) ? [(TIPImageDiskCache *)priorityCache diskCache_syncAccessManifest] : priorityCache.manifest; knownPriorityEntries = (NSInteger)manifest.numberOfEntries; } // Remove entries from the non-priority caches to alleviate memory pressure while (([self internalTotalBytesForAllCachesOfType:type] > globalMaxBytes || [self internalTotalCountForAllCachesOfType:type] > globalMaxCount) && knownTotalEntries != knownPriorityEntries) { if (!allPipelines) { // lazy load allPipelines = [[TIPImagePipeline allRegisteredImagePipelines] allValues]; } // Only load knownTotalEntries once const BOOL getKnownTotalEntries = knownTotalEntries < 0; if (getKnownTotalEntries) { knownTotalEntries = 0; } // Ditch the oldest entry for all non-priority caches for (TIPImagePipeline *pipeline in allPipelines) { id<TIPImageCache> cache = [pipeline cacheOfType:type]; if (cache) { TIPLRUCache *manifest = (TIPImageCacheTypeDisk == type) ? [(TIPImageDiskCache *)cache diskCache_syncAccessManifest] : cache.manifest; if (getKnownTotalEntries) { knownTotalEntries += manifest.numberOfEntries; } if (cache != priorityCache) { if ([manifest removeTailEntry]) { knownTotalEntries--; } } } } } // If we still have too much data being consumed, start removing entries from the priority cache while (([self internalTotalBytesForAllCachesOfType:type] > globalMaxBytes || [self internalTotalCountForAllCachesOfType:type] > globalMaxCount) && knownPriorityEntries > 0) { [priorityCache.manifest removeTailEntry]; knownPriorityEntries--; } #if DEBUG if ([self internalTotalBytesForAllCachesOfType:type] > globalMaxBytes || [self internalTotalCountForAllCachesOfType:type] > globalMaxCount) { NSString *typeStr = nil; switch (type) { case TIPImageCacheTypeRendered: typeStr = @"Rendered Cache"; break; case TIPImageCacheTypeMemory: typeStr = @"Memory Cache"; break; case TIPImageCacheTypeDisk: typeStr = @"Disk Cache"; break; } TIPLogWarning(@"We cleared as many entries from %@s as we could and still are over the cap", typeStr); } #endif } } #pragma mark Runtime Methods - (void)setAssertsEnabled:(BOOL)assertsEnabled { gTwitterImagePipelineAssertEnabled = assertsEnabled; } - (BOOL)areAssertsEnabled { return gTwitterImagePipelineAssertEnabled; } - (void)setLogger:(nullable id<TIPLogger>)logger { gTIPLogger = logger; self.internalLogger = logger; } - (nullable id<TIPLogger>)logger { return self.internalLogger; } #pragma mark Download Methods - (id<TIPImageFetchDownload>)createImageFetchDownloadWithContext:(id<TIPImageFetchDownloadContext>)context { id<TIPImageFetchDownloadProvider> imageFetchDownloadProvider = self.imageFetchDownloadProvider; TIPAssert(imageFetchDownloadProvider != nil); id<TIPImageFetchDownload> download = [imageFetchDownloadProvider imageFetchDownloadWithContext:context]; if (context != download.context) { NSDictionary *userInfo; if (imageFetchDownloadProvider) { userInfo = @{ @"className" : NSStringFromClass([imageFetchDownloadProvider class]) }; } @throw [NSException exceptionWithName:TIPImageFetchDownloadConstructorExceptionName reason:@"TIPImageFetchDownload did not adhere to protocol requirements!" userInfo:userInfo]; } return download; } - (void)setImageFetchDownloadProvider:(nullable id<TIPImageFetchDownloadProvider>)imageFetchDownloadProvider { if (!imageFetchDownloadProvider) { if ([_imageFetchDownloadProvider class] == [TIPImageFetchDownloadProviderInternal class]) { imageFetchDownloadProvider = _imageFetchDownloadProvider; } else { imageFetchDownloadProvider = [[TIPImageFetchDownloadProviderInternal alloc] init]; } } TIPAssert([imageFetchDownloadProvider conformsToProtocol:@protocol(TIPImageFetchDownloadProvider)]); if (_imageFetchDownloadProvider == imageFetchDownloadProvider) { return; } const BOOL supportsStubbing = [imageFetchDownloadProvider respondsToSelector:@selector(setDownloadStubbingEnabled:)] && [imageFetchDownloadProvider conformsToProtocol:@protocol(TIPImageFetchDownloadProviderWithStubbingSupport)]; if (_imageFetchDownloadProviderSupportsStubbing) { [(id<TIPImageFetchDownloadProviderWithStubbingSupport>)_imageFetchDownloadProvider removeAllDownloadStubs]; [(id<TIPImageFetchDownloadProviderWithStubbingSupport>)_imageFetchDownloadProvider setDownloadStubbingEnabled:NO]; } if (supportsStubbing) { [(id<TIPImageFetchDownloadProviderWithStubbingSupport>)imageFetchDownloadProvider removeAllDownloadStubs]; [(id<TIPImageFetchDownloadProviderWithStubbingSupport>)imageFetchDownloadProvider setDownloadStubbingEnabled:YES]; } _imageFetchDownloadProviderSupportsStubbing = NO; _imageFetchDownloadProvider = imageFetchDownloadProvider; _imageFetchDownloadProviderSupportsStubbing = supportsStubbing; } @end @implementation TIPGlobalConfiguration (Inspect) - (void)getAllFetchOperations:(out NSArray<TIPImageFetchOperation *> * __nullable * __nullable)fetchOpsOut allStoreOperations:(out NSArray<TIPImageStoreOperation *> * __nullable * __nullable)storeOpsOut { NSMutableArray<TIPImageFetchOperation *> *fetchOps = [[NSMutableArray alloc] init]; NSMutableArray<TIPImageStoreOperation *> *storeOps = [[NSMutableArray alloc] init]; for (NSOperation *op in _sharedImagePipelineQueue.operations) { if ([op isKindOfClass:[TIPImageFetchOperation class]]) { [fetchOps addObject:(id)op]; } else if ([op isKindOfClass:[TIPImageStoreOperation class]]) { [storeOps addObject:(id)op]; } } if (fetchOpsOut) { *fetchOpsOut = [fetchOps copy]; } if (storeOpsOut) { *storeOpsOut = [storeOps copy]; } } - (void)inspect:(TIPGlobalConfigurationInspectionCallback)callback { NSMutableDictionary<NSString *, TIPImagePipeline *> *pipelines = [[TIPImagePipeline allRegisteredImagePipelines] mutableCopy]; NSMutableDictionary<NSString *, TIPImagePipelineInspectionResult *> *results = [NSMutableDictionary dictionaryWithCapacity:pipelines.count]; _Inspect(pipelines, results, callback); } static void _Inspect(NSMutableDictionary<NSString *, TIPImagePipeline *> *remainingPipelines, NSMutableDictionary<NSString *, TIPImagePipelineInspectionResult *> *gatheredResults, TIPGlobalConfigurationInspectionCallback callback) { NSString *identifier = remainingPipelines.allKeys.firstObject; if (!identifier) { callback(gatheredResults); return; } TIPImagePipeline *pipeline = remainingPipelines[identifier]; [remainingPipelines removeObjectForKey:identifier]; [pipeline inspect:^(TIPImagePipelineInspectionResult *result) { if (result) { gatheredResults[identifier] = result; } _Inspect(remainingPipelines, gatheredResults, callback); }]; } @end NS_ASSUME_NONNULL_END