From d021431ac6385ed576a6578a51f5a78e3c13d68d Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Thu, 17 Dec 2020 17:11:30 -0800 Subject: [PATCH 1/7] Remove components not used by Opener. --- TJImageCache/TJImageCache.h | 7 ---- TJImageCache/TJImageCache.m | 64 ------------------------------------- 2 files changed, 71 deletions(-) diff --git a/TJImageCache/TJImageCache.h b/TJImageCache/TJImageCache.h index d9d5c3a..237ab1e 100755 --- a/TJImageCache/TJImageCache.h +++ b/TJImageCache/TJImageCache.h @@ -34,10 +34,8 @@ extern NSString *TJImageCacheHash(NSString *string); @interface TJImageCache : NSObject -+ (void)configureWithDefaultRootPath; + (void)configureWithRootPath:(NSString *const)rootPath; -+ (NSString *)hash:(NSString *)string __attribute__((deprecated("Use TJImageCacheHash instead", "TJImageCacheHash"))); + (NSString *)pathForURLString:(NSString *const)urlString; + (nullable IMAGE_CLASS *)imageAtURL:(NSString *const)url depth:(const TJImageCacheDepth)depth delegate:(nullable const id)delegate backgroundDecode:(const BOOL)backgroundDecode; @@ -48,17 +46,12 @@ extern NSString *TJImageCacheHash(NSString *string); + (void)cancelImageLoadForURL:(NSString *const)url delegate:(const id)delegate policy:(const TJImageCacheCancellationPolicy)policy; -+ (TJImageCacheDepth)depthForImageAtURL:(NSString *const)url; - -+ (void)removeImageAtURL:(NSString *const)url; -+ (void)dumpDiskCache; + (void)dumpMemoryCache; + (void)getDiskCacheSize:(void (^const)(long long diskCacheSize))completion; + (void)auditCacheWithBlock:(BOOL (^const)(NSString *hashedURL, NSURL *fileURL, long long fileSize))block // return YES to preserve the image, return NO to delete it propertyKeys:(nullable NSArray *)propertyKeys completionBlock:(nullable dispatch_block_t)completionBlock; -+ (void)auditCacheRemovingFilesLastAccessedBeforeDate:(NSDate *const)date; + (void)computeDiskCacheSizeIfNeeded; /// Will be @c nil until @c +computeDiskCacheSizeIfNeeded, @c +getDiskCacheSize:, or one of the cache auditing methods is called once, then it will update automatically as the cache changes. diff --git a/TJImageCache/TJImageCache.m b/TJImageCache/TJImageCache.m index fc232ea..97df7f6 100755 --- a/TJImageCache/TJImageCache.m +++ b/TJImageCache/TJImageCache.m @@ -54,11 +54,6 @@ @implementation TJImageCache #pragma mark - Configuration -+ (void)configureWithDefaultRootPath -{ - [self configureWithRootPath:[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"TJImageCache"]]; -} - + (void)configureWithRootPath:(NSString *const)rootPath { NSParameterAssert(rootPath); @@ -71,11 +66,6 @@ + (void)configureWithRootPath:(NSString *const)rootPath #pragma mark - Hashing -+ (NSString *)hash:(NSString *)string -{ - return TJImageCacheHash(string); -} - // Using 11 characters from the following table guarantees that we'll generate maximally unique keys that are also tagged pointer strings. // Tagged pointers have memory and CPU performance benefits, so this is better than just using a plain ol' hex hash. // I've omitted the "." and " " characters from this table to create "pleasant" filenames. @@ -346,29 +336,6 @@ + (void)cancelImageLoadForURL:(NSString *const)urlString delegate:(const id *const mapTable) { - isImageInMapTable = [mapTable objectForKey:urlString] != nil; - }, NO); - - if (isImageInMapTable) { - return TJImageCacheDepthMemory; - } - - NSString *const hash = TJImageCacheHash(urlString); - if ([[NSFileManager defaultManager] fileExistsAtPath:_pathForHash(hash)]) { - return TJImageCacheDepthDisk; - } - - return TJImageCacheDepthNetwork; -} - + (void)getDiskCacheSize:(void (^const)(long long diskCacheSize))completion { dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ @@ -388,31 +355,11 @@ + (void)getDiskCacheSize:(void (^const)(long long diskCacheSize))completion #pragma mark - Cache Manipulation -+ (void)removeImageAtURL:(NSString *const)urlString -{ - [_cache() removeObjectForKey:urlString]; - NSString *const path = _pathForHash(TJImageCacheHash(urlString)); - NSNumber *fileSizeNumber; - [[NSURL fileURLWithPath:path] getResourceValue:&fileSizeNumber forKey:NSURLTotalFileSizeKey error:nil]; - if ([[NSFileManager defaultManager] removeItemAtPath:path error:nil]) { - _modifyDeltaSize(-fileSizeNumber.longLongValue); - } -} - + (void)dumpMemoryCache { [_cache() removeAllObjects]; } -+ (void)dumpDiskCache -{ - [self auditCacheWithBlock:^BOOL(NSString *hashedURL, NSURL *fileURL, long long fileSize) { - return NO; - } - propertyKeys:nil - completionBlock:nil]; -} - #pragma mark - Cache Auditing + (void)auditCacheWithBlock:(BOOL (^const)(NSString *hashedURL, NSURL *fileURL, long long fileSize))block @@ -463,17 +410,6 @@ + (void)auditCacheWithBlock:(BOOL (^const)(NSString *hashedURL, NSURL *fileURL, }); } -+ (void)auditCacheRemovingFilesLastAccessedBeforeDate:(NSDate *const)date -{ - [self auditCacheWithBlock:^BOOL(NSString *hashedURL, NSURL *fileURL, long long fileSize) { - NSDate *lastAccess; - [fileURL getResourceValue:&lastAccess forKey:NSURLContentAccessDateKey error:nil]; - return ([lastAccess compare:date] != NSOrderedAscending); - } - propertyKeys:@[NSURLContentAccessDateKey] - completionBlock:nil]; -} - #pragma mark - Private static NSString *_rootPath(void) From 1be5d00899166d4d435893d4c77d0adbdd8db1dd Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Mon, 18 Jan 2021 11:02:42 -0800 Subject: [PATCH 2/7] More aggressively cancel outdated images for Opener. --- TJImageCache/TJProgressiveImageView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TJImageCache/TJProgressiveImageView.m b/TJImageCache/TJProgressiveImageView.m index 92476f1..a63ef5f 100644 --- a/TJImageCache/TJProgressiveImageView.m +++ b/TJImageCache/TJProgressiveImageView.m @@ -94,7 +94,7 @@ - (void)didGetImage:(UIImage *)image atURL:(NSString *)url } } else if (cancelLowPriImages) { // Cancel any lower priority images - [TJImageCache cancelImageLoadForURL:obj delegate:self policy:TJImageCacheCancellationPolicyImageProcessing]; + [TJImageCache cancelImageLoadForURL:obj delegate:self policy:TJImageCacheCancellationPolicyBeforeBody]; } else { *stop = YES; } From 0dd14ba4ef6b9388006c39cc47b863298f696ec5 Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Fri, 5 May 2023 21:21:35 -0700 Subject: [PATCH 3/7] Unload offscreen images when entering the background, recommended by https://developer.apple.com/videos/play/wwdc2018/416/. --- TJImageCache/TJProgressiveImageView.m | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/TJImageCache/TJProgressiveImageView.m b/TJImageCache/TJProgressiveImageView.m index a63ef5f..da53434 100644 --- a/TJImageCache/TJProgressiveImageView.m +++ b/TJImageCache/TJProgressiveImageView.m @@ -15,17 +15,34 @@ @interface TJProgressiveImageView () { @end -__attribute__((objc_direct_members)) @implementation TJProgressiveImageView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _currentImageURLStringIndex = NSNotFound; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } return self; } +- (void)applicationWillEnterForeground:(NSNotification *)notification +{ + if (!self.image) { + NSOrderedSet *imageURLStrings = self.imageURLStrings; + self.imageURLStrings = nil; + self.imageURLStrings = imageURLStrings; + } +} + +- (void)applicationDidEnterBackground:(NSNotification *)notification +{ + if (!self.window) { + self.image = nil; + } +} + - (void)setImageURLStrings:(NSOrderedSet *)imageURLStrings { [self setImageURLStrings:imageURLStrings secondaryImageDepth:TJImageCacheDepthDisk]; From 04f841c779de18d968ecf0da2e123a8bd3f93091 Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Tue, 2 Sep 2025 19:51:11 -0700 Subject: [PATCH 4/7] Transcode images to HEIC to save space. Only if not in low power mode and in nominal thermal state. --- TJImageCache/TJImageCache.m | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/TJImageCache/TJImageCache.m b/TJImageCache/TJImageCache.m index 97df7f6..18cc830 100755 --- a/TJImageCache/TJImageCache.m +++ b/TJImageCache/TJImageCache.m @@ -519,11 +519,23 @@ static void _tryUpdateMemoryCacheAndCallDelegates(NSString *const path, NSString IMAGE_CLASS *image = nil; if (canProcess) { if (path) { + IMAGE_CLASS *const diskImage = [IMAGE_CLASS imageWithContentsOfFile:path]; if (backgroundDecode) { - image = _predrawnImageFromPath(path); + image = [diskImage imageByPreparingForDisplay] ?: diskImage; + } else { + image = diskImage; } - if (!image) { - image = [IMAGE_CLASS imageWithContentsOfFile:path]; + if (@available(iOS 17.0, *)) { + if (size > 0 && image && ![[NSProcessInfo processInfo] isLowPowerModeEnabled] && [[NSProcessInfo processInfo] thermalState] == NSProcessInfoThermalStateNominal) { + __block IMAGE_CLASS *strongImage = image; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + NSData *const heicData = UIImageHEICRepresentation(diskImage); + if (heicData.length < size) { + [heicData writeToFile:path atomically:YES]; + } + strongImage = nil; // Hold reference until we're done + }); + } } } if (image) { From 23626748eadead0f8e8b236c5d531f97389b9a42 Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Wed, 3 Sep 2025 17:22:12 -0700 Subject: [PATCH 5/7] Remove unused function. --- TJImageCache/TJImageCache.m | 79 ------------------------------------- 1 file changed, 79 deletions(-) diff --git a/TJImageCache/TJImageCache.m b/TJImageCache/TJImageCache.m index 18cc830..2404533 100755 --- a/TJImageCache/TJImageCache.m +++ b/TJImageCache/TJImageCache.m @@ -601,85 +601,6 @@ static void _tryUpdateMemoryCacheAndCallDelegatesWithBundledImage(UIImage *const }); } -// Modified version of https://github.com/Flipboard/FLAnimatedImage/blob/master/FLAnimatedImageDemo/FLAnimatedImage/FLAnimatedImage.m#L641 -static IMAGE_CLASS *_predrawnImageFromPath(NSString *const path) -{ -#if defined(__IPHONE_15_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0 -#if !defined(__IPHONE_15_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 - if (@available(iOS 15.0, *)) -#endif - { - return [[UIImage imageWithContentsOfFile:path] imageByPreparingForDisplay]; - } -#endif - -#if !defined(__IPHONE_15_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 - // Always use a device RGB color space for simplicity and predictability what will be going on. - static CGColorSpaceRef colorSpaceDeviceRGBRef; - static CFDictionaryRef options; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB(); - options = (__bridge_retained CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache: (__bridge id)kCFBooleanFalse}; - }); - - const CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path isDirectory:NO], nil); - const CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, 0, options); - - if (imageSource) { - CFRelease(imageSource); - } - - if (!image) { - return nil; - } - - // "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs) - const size_t width = CGImageGetWidth(image); - const size_t height = CGImageGetHeight(image); - - // RGB+A - const size_t bytesPerRow = width << 2; - - CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image); - // If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one. - // "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs) - switch (alphaInfo) { - case kCGImageAlphaNone: - case kCGImageAlphaOnly: - case kCGImageAlphaFirst: - alphaInfo = kCGImageAlphaNoneSkipFirst; - break; - case kCGImageAlphaLast: - alphaInfo = kCGImageAlphaNoneSkipLast; - break; - default: - break; - } - - // Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time). - // Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why. - - const CGContextRef bitmapContextRef = CGBitmapContextCreate(NULL, width, height, CHAR_BIT, bytesPerRow, colorSpaceDeviceRGBRef, kCGBitmapByteOrderDefault | alphaInfo); - // Early return on failure! - if (!bitmapContextRef) { - NSCAssert(NO, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, (size_t)CHAR_BIT, bytesPerRow, image); - CGImageRelease(image); - return nil; - } - - // Draw image in bitmap context and create image by preserving receiver's properties. - CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, width, height), image); - const CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef); - IMAGE_CLASS *const predrawnImage = [IMAGE_CLASS imageWithCGImage:predrawnImageRef]; - CGImageRelease(image); - CGImageRelease(predrawnImageRef); - CGContextRelease(bitmapContextRef); - - return predrawnImage; -#endif -} - + (void)computeDiskCacheSizeIfNeeded { if (_tj_imageCacheBaseSize == nil) { From 0203bed7ed3d332045d7927e605094a13ca88673 Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Wed, 3 Sep 2025 17:22:40 -0700 Subject: [PATCH 6/7] Preserve animated images and modern compressed image formats. --- TJImageCache/TJImageCache.m | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/TJImageCache/TJImageCache.m b/TJImageCache/TJImageCache.m index 2404533..39ce4a2 100755 --- a/TJImageCache/TJImageCache.m +++ b/TJImageCache/TJImageCache.m @@ -3,6 +3,7 @@ #import "TJImageCache.h" #import +#import static NSString *_tj_imageCacheRootPath; @@ -529,10 +530,36 @@ static void _tryUpdateMemoryCacheAndCallDelegates(NSString *const path, NSString if (size > 0 && image && ![[NSProcessInfo processInfo] isLowPowerModeEnabled] && [[NSProcessInfo processInfo] thermalState] == NSProcessInfoThermalStateNominal) { __block IMAGE_CLASS *strongImage = image; dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + NSURL *fileURL = [NSURL fileURLWithPath:path]; + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL); + + if (!imageSource) { + return; + } + + const size_t count = CGImageSourceGetCount(imageSource); + + if (count > 1) { + CFRelease(imageSource); + return; // Preserve animated images + } + + NSString *imageType = (__bridge NSString *)CGImageSourceGetType(imageSource); + UTType *utType = [UTType typeWithIdentifier:imageType]; + CFRelease(imageSource); + + if ([utType conformsToType:UTTypeWebP] || + [utType conformsToType:UTTypeHEIC] || + [utType conformsToType:UTTypeHEIF]) { + // Don't attempt to re-encode already small images + return; + } + NSData *const heicData = UIImageHEICRepresentation(diskImage); if (heicData.length < size) { [heicData writeToFile:path atomically:YES]; } + strongImage = nil; // Hold reference until we're done }); } From 39de8f7ad1d09a3e6b52ebfa24fc13def4b9e26a Mon Sep 17 00:00:00 2001 From: Tim Johnsen Date: Wed, 3 Sep 2025 17:25:03 -0700 Subject: [PATCH 7/7] Reencode images serially. --- TJImageCache/TJImageCache.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/TJImageCache/TJImageCache.m b/TJImageCache/TJImageCache.m index 39ce4a2..6fa7cba 100755 --- a/TJImageCache/TJImageCache.m +++ b/TJImageCache/TJImageCache.m @@ -528,8 +528,14 @@ static void _tryUpdateMemoryCacheAndCallDelegates(NSString *const path, NSString } if (@available(iOS 17.0, *)) { if (size > 0 && image && ![[NSProcessInfo processInfo] isLowPowerModeEnabled] && [[NSProcessInfo processInfo] thermalState] == NSProcessInfoThermalStateNominal) { + static dispatch_queue_t reencodeQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + reencodeQueue = dispatch_queue_create_with_target("com.tijo.TJImageCache.reencodeQueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)); + }); + __block IMAGE_CLASS *strongImage = image; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + dispatch_async(reencodeQueue, ^{ NSURL *fileURL = [NSURL fileURLWithPath:path]; CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL);