TwitterImagePipeline/Project/TIPImageRenderedCache.m (529 lines of code) (raw):
//
// TIPImageRenderedCache.m
// TwitterImagePipeline
//
// Created on 4/6/15.
// Copyright (c) 2015 Twitter. All rights reserved.
//
#import <UIKit/UIApplication.h>
#import "TIP_Project.h"
#import "TIPGlobalConfiguration+Project.h"
#import "TIPImageCacheEntry.h"
#import "TIPImagePipeline+Project.h"
#import "TIPImagePipelineInspectionResult+Project.h"
#import "TIPImageRenderedCache.h"
#import "TIPLRUCache.h"
#import "UIImage+TIPAdditions.h"
NS_ASSUME_NONNULL_BEGIN
static const NSUInteger kMaxEntriesPerRenderedCollection = 3;
NS_INLINE BOOL _StringsAreEqual(NSString * __nullable string1, NSString * __nullable string2)
{
if (string1 == string2) {
return YES;
}
if (!string1 || !string2) {
return NO;
}
return [string1 isEqualToString:string2];
}
TIP_OBJC_FINAL TIP_OBJC_DIRECT_MEMBERS
@interface TIPRenderedCacheItem : NSObject
@property (nonatomic, readonly, copy, nullable) NSString *transformerIdentifier;
@property (nonatomic, readonly) CGSize sourceImageDimensions;
@property (nonatomic, readonly, getter=isDirty) BOOL dirty;
@property (nonatomic, readonly) TIPImageCacheEntry *entry;
- (instancetype)initWithEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceDims;
- (void)markDirty;
// Methods for weakify (used when going into background to release images)
- (void)weakify;
- (BOOL)strongify;
@end
TIP_OBJC_FINAL TIP_OBJC_DIRECT_MEMBERS
@interface TIPImageRenderedEntriesCollection : NSObject
@property (nonatomic, readonly, copy) NSString *identifier;
- (instancetype)initWithIdentifier:(NSString *)identifier;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (NSUInteger)collectionCost;
- (void)addImageEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceDims;
- (nullable TIPImageCacheEntry *)imageEntryMatchingDimensions:(CGSize)size
contentMode:(UIViewContentMode)mode
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(out CGSize * __nullable)sourceDimsOut
dirty:(out BOOL * __nullable)dirtyOut;
- (NSArray<TIPImageCacheEntry *> *)allEntries;
- (void)dirtyAllEntries;
// weakify pattern
- (void)weakifyEntries;
- (BOOL)strongifyEntries;
@end
@interface TIPImageRenderedEntriesCollection () <TIPLRUEntry>
@end
@interface TIPImageRenderedCache () <TIPLRUCacheDelegate>
@property (tip_atomic_direct) SInt64 atomicTotalCost;
@end
#define STRONGIFY_TEMPORARILY_IF_NEEDED() \
const BOOL tip_macro_concat(inWeakMode__, __LINE__) = self->_weakCollections != nil; \
if ( tip_macro_concat(inWeakMode__, __LINE__) ) { \
[self _strongifyEntries]; \
} \
tip_defer(^{ \
if ( tip_macro_concat(inWeakMode__, __LINE__) ) { \
[self weakifyEntries]; \
} \
});
@implementation TIPImageRenderedCache
{
TIPLRUCache *_manifest;
NSMutableArray<TIPImageRenderedEntriesCollection *> *_weakCollections;
}
@synthesize manifest = _manifest;
- (NSUInteger)totalCost
{
return (NSUInteger)self.atomicTotalCost;
}
- (TIPImageCacheType)cacheType
{
return TIPImageCacheTypeRendered;
}
- (instancetype)init
{
if (self = [super init]) {
_manifest = [[TIPLRUCache alloc] initWithEntries:nil delegate:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_tip_didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
// Remove the cache's total bytes from our global count of total bytes
const SInt64 totalSize = (SInt64)self.atomicTotalCost;
const SInt16 totalCount = (SInt16)_manifest.numberOfEntries;
TIPGlobalConfiguration *config = [TIPGlobalConfiguration sharedInstance];
tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
config.internalTotalBytesForAllRenderedCaches -= totalSize;
config.internalTotalCountForAllRenderedCaches -= totalCount;
});
}
- (void)clearAllImages:(nullable void (^)(void))completion
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd
withObject:completion
waitUntilDone:NO];
return;
}
// Clear the manifest in the background to avoid main thread stalls
@autoreleasepool {
STRONGIFY_TEMPORARILY_IF_NEEDED();
TIPLRUCache *oldManifest = _manifest;
const SInt16 totalCount = (SInt16)oldManifest.numberOfEntries;
_manifest = [[TIPLRUCache alloc] initWithEntries:nil delegate:self];
tip_dispatch_async_autoreleasing(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[oldManifest clearAllEntries];
});
[self _updateByteCountsAdded:0 removed:(UInt64)self.atomicTotalCost];
[TIPGlobalConfiguration sharedInstance].internalTotalCountForAllRenderedCaches -= totalCount;
TIPLogInformation(@"Cleared all images in %@", self);
}
if (completion) {
completion();
}
}
- (nullable TIPImageCacheEntry *)imageEntryWithIdentifier:(NSString *)identifier
transformerIdentifier:(nullable NSString *)transformerIdentifier
targetDimensions:(CGSize)size
targetContentMode:(UIViewContentMode)mode
sourceImageDimensions:(out CGSize * __nullable)sourceDimsOut
dirty:(out BOOL * __nullable)dirtyOut
{
TIPAssert([NSThread isMainThread]);
TIPAssert(identifier != nil);
if (identifier != nil && [NSThread isMainThread]) {
@autoreleasepool {
[self _strongifyEntries];
TIPImageRenderedEntriesCollection *collection = [_manifest entryWithIdentifier:identifier];
return [collection imageEntryMatchingDimensions:size
contentMode:mode
transformerIdentifier:transformerIdentifier
sourceImageDimensions:sourceDimsOut
dirty:dirtyOut];
}
}
if (sourceDimsOut) {
*sourceDimsOut = CGSizeZero;
}
if (dirtyOut) {
*dirtyOut = NO;
}
return nil;
}
- (void)storeImageEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceDims
{
TIPAssert(entry != nil);
if (!entry.completeImage || !entry.completeImageContext) {
return;
}
if (entry.completeImageContext.treatAsPlaceholder) {
// no placeholders in the rendered cache
return;
}
@autoreleasepool {
entry = [entry copy];
entry.partialImage = nil;
entry.partialImageContext = nil;
}
if (![NSThread isMainThread]) {
tip_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{
[self storeImageEntry:entry
transformerIdentifier:transformerIdentifier
sourceImageDimensions:sourceDims];
});
return;
}
@autoreleasepool {
STRONGIFY_TEMPORARILY_IF_NEEDED();
TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance];
NSString *identifier = entry.identifier;
TIPImageRenderedEntriesCollection *collection = (TIPImageRenderedEntriesCollection *)[_manifest entryWithIdentifier:identifier];
const BOOL hasCollection = (collection != nil);
const NSUInteger oldCost = hasCollection ? collection.collectionCost : 0;
// Cap our entry size
if ((SInt64)entry.completeImage.sizeInMemory > [globalConfig internalMaxBytesForCacheEntryOfType:self.cacheType]) {
// too big, don't cache.
return;
}
if (!collection) {
collection = [[TIPImageRenderedEntriesCollection alloc] initWithIdentifier:identifier];
}
[collection addImageEntry:entry
transformerIdentifier:transformerIdentifier
sourceImageDimensions:sourceDims];
const NSUInteger newCost = collection.collectionCost;
if (!newCost) {
if (hasCollection) {
[_manifest removeEntry:collection];
}
} else {
[_manifest addEntry:collection]; // add entry or move to front
if (!hasCollection) {
globalConfig.internalTotalCountForAllRenderedCaches += 1;
}
}
[self _updateByteCountsAdded:newCost removed:oldCost];
[globalConfig pruneAllCachesOfType:self.cacheType withPriorityCache:self];
}
}
- (void)clearImageWithIdentifier:(NSString *)identifier
{
TIPAssert(identifier != nil);
if (!identifier) {
return;
}
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd
withObject:identifier
waitUntilDone:NO];
return;
}
@autoreleasepool {
STRONGIFY_TEMPORARILY_IF_NEEDED();
TIPImageRenderedEntriesCollection *collection = [_manifest entryWithIdentifier:identifier];
[_manifest removeEntry:collection];
}
}
- (void)dirtyImageWithIdentifier:(NSString *)identifier
{
TIPAssert(identifier != nil);
if (!identifier) {
return;
}
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd
withObject:identifier
waitUntilDone:NO];
return;
}
@autoreleasepool {
STRONGIFY_TEMPORARILY_IF_NEEDED();
TIPImageRenderedEntriesCollection *collection = [_manifest entryWithIdentifier:identifier];
[collection dirtyAllEntries];
}
}
- (void)weakifyEntries
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:NO];
return;
}
@autoreleasepool {
if (!_weakCollections) {
_weakCollections = [[NSMutableArray alloc] initWithCapacity:_manifest.numberOfEntries];
}
TIPImageRenderedEntriesCollection *collection;
while ((collection = _manifest.headEntry) != nil) {
[_weakCollections addObject:collection];
[_manifest removeEntry:collection]; // remove before weakifying to properly decrement cost
[collection weakifyEntries];
}
}
}
#pragma mark Delegate
- (void)tip_cache:(TIPLRUCache *)manifest didEvictEntry:(TIPImageRenderedEntriesCollection *)entry
{
[TIPGlobalConfiguration sharedInstance].internalTotalCountForAllRenderedCaches -= 1;
[self _updateByteCountsAdded:0 removed:entry.collectionCost];
TIPLogDebug(@"%@ Evicted '%@'", NSStringFromClass([self class]), entry.identifier);
}
#pragma mark Inspect
- (void)inspect:(TIPInspectableCacheCallback)callback
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd
withObject:callback
waitUntilDone:NO];
return;
}
@autoreleasepool {
NSMutableArray *inspectedEntries = [[NSMutableArray alloc] init];
// WARNING: if in weakify mode, this will yield zero results
for (TIPImageRenderedEntriesCollection *collection in _manifest) {
NSArray *allEntries = [collection allEntries];
for (TIPImageCacheEntry *cacheEntry in allEntries) {
TIPImagePipelineInspectionResultEntry *entry;
entry = [TIPImagePipelineInspectionResultEntry entryWithCacheEntry:cacheEntry
class:[TIPImagePipelineInspectionResultRenderedEntry class]];
TIPAssert(entry != nil);
entry.bytesUsed = [entry.image tip_estimatedSizeInBytes];
#ifndef __clang_analyzer__ // reports entry can be nil; we prefer to crash if it is
[inspectedEntries addObject:entry];
#endif
}
}
callback(inspectedEntries, nil);
}
}
#pragma mark Private
- (void)_tip_didReceiveMemoryWarning:(NSNotification *)note
{
[self clearAllImages:NULL];
}
- (void)_updateByteCountsAdded:(UInt64)bytesAdded removed:(UInt64)bytesRemoved
{
TIP_UPDATE_BYTES(self.atomicTotalCost, bytesAdded, bytesRemoved, @"Rendered Cache Size");
TIP_UPDATE_BYTES([TIPGlobalConfiguration sharedInstance].internalTotalBytesForAllRenderedCaches, bytesAdded, bytesRemoved, @"All Rendered Caches Size");
}
- (void)_strongifyEntries
{
if (!_weakCollections) {
return;
}
NSArray<TIPImageRenderedEntriesCollection *> *collections = _weakCollections;
_weakCollections = nil;
TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance];
for (TIPImageRenderedEntriesCollection *collection in collections) {
if ([collection strongifyEntries]) {
[_manifest addEntry:collection];
globalConfig.internalTotalCountForAllRenderedCaches += 1;
[self _updateByteCountsAdded:collection.collectionCost removed:0];
}
}
[globalConfig pruneAllCachesOfType:self.cacheType withPriorityCache:self];
}
@end
@implementation TIPImageRenderedEntriesCollection
{
NSMutableArray<TIPRenderedCacheItem *> *_items;
}
@synthesize nextLRUEntry = _nextLRUEntry;
@synthesize previousLRUEntry = _previousLRUEntry;
- (instancetype)initWithIdentifier:(NSString *)identifier
{
if (self = [super init]) {
_identifier = [identifier copy];
_items = [NSMutableArray arrayWithCapacity:kMaxEntriesPerRenderedCollection + 1];
// ^ we +1 the capacity because we will overfill the array first before trimming it back down to the cap
}
return self;
}
- (void)addImageEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceDims
{
const CGSize dimensions = entry.completeImage.dimensions;
if (dimensions.width < (CGFloat)1.0 || dimensions.height < (CGFloat)1.0) {
return;
}
for (NSInteger i = 0; i < (NSInteger)_items.count; i++) {
TIPRenderedCacheItem *item = _items[(NSUInteger)i];
const CGSize otherDimensions = item.entry.completeImage.dimensions;
if (CGSizeEqualToSize(otherDimensions, dimensions)) {
if (_StringsAreEqual(item.transformerIdentifier, transformerIdentifier)) {
if (!item.isDirty && item.sourceImageDimensions.height >= sourceDims.height && item.sourceImageDimensions.width >= sourceDims.width) {
return; // keep existing entry
} else {
// improved source image dims
// removal old item since it's lower fidelity
[_items removeObjectAtIndex:(NSUInteger)i];
i--;
}
}
}
}
if (gTwitterImagePipelineAssertEnabled && 0 == entry.completeImage.sizeInMemory) {
NSDictionary *info = @{
@"dimensions" : NSStringFromCGSize(entry.completeImageContext.dimensions),
@"URL" : entry.completeImageContext.URL,
@"id" : entry.identifier,
};
TIPLogError(@"Cached zero cost image to rendered cache %@", info);
}
[self _insertEntry:entry
transformerIdentifier:transformerIdentifier
sourceImageDimensions:sourceDims];
if (_items.count > kMaxEntriesPerRenderedCollection) {
[_items removeLastObject];
}
TIPAssert(_items.count <= kMaxEntriesPerRenderedCollection);
}
- (nullable TIPImageCacheEntry *)imageEntryMatchingDimensions:(CGSize)dimensions
contentMode:(UIViewContentMode)mode
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(out CGSize * __nullable)sourceDimsOut
dirty:(out BOOL * __nullable)dirtyOut
{
if (!TIPSizeGreaterThanZero(dimensions) || mode >= UIViewContentModeRedraw) {
return nil;
}
NSUInteger index = NSNotFound;
NSUInteger i = 0;
TIPImageCacheEntry *returnValue = nil;
CGSize returnDims = CGSizeZero;
BOOL returnDirty = NO;
for (TIPRenderedCacheItem *item in _items) {
TIPImageCacheEntry *entry = item.entry;
if ([entry.completeImage.image tip_matchesTargetDimensions:dimensions contentMode:mode]) {
if (_StringsAreEqual(item.transformerIdentifier, transformerIdentifier)) {
index = i;
returnValue = entry;
returnDims = item.sourceImageDimensions;
returnDirty = item.isDirty;
break;
}
}
i++;
}
if (sourceDimsOut) {
*sourceDimsOut = returnDims;
}
if (dirtyOut) {
*dirtyOut = returnDirty;
}
if (NSNotFound != index && returnValue) {
if (index != 0) {
// HIT, move entry to front
TIPRenderedCacheItem *item = _items[index];
[_items removeObjectAtIndex:index];
[_items insertObject:item atIndex:0];
}
return returnValue;
}
return nil;
}
- (NSArray<TIPImageCacheEntry *> *)allEntries
{
NSMutableArray<TIPImageCacheEntry *> *allEntries = [NSMutableArray arrayWithCapacity:_items.count];
for (TIPRenderedCacheItem *item in _items) {
[allEntries addObject:item.entry];
}
return [allEntries copy];
}
- (void)dirtyAllEntries
{
for (TIPRenderedCacheItem *item in _items) {
[item markDirty];
}
}
- (NSUInteger)collectionCost
{
NSUInteger cost = 0;
for (TIPRenderedCacheItem *item in _items) {
cost += item.entry.completeImage.sizeInMemory;
}
return cost;
}
- (NSString *)LRUEntryIdentifier
{
return self.identifier;
}
- (BOOL)shouldAccessMoveLRUEntryToHead
{
return YES;
}
- (void)weakifyEntries
{
for (TIPRenderedCacheItem *item in _items) {
[item weakify];
}
}
- (BOOL)strongifyEntries
{
BOOL anyStrongified = NO;
NSArray<TIPRenderedCacheItem *> *items = [_items copy];
[_items removeAllObjects];
for (TIPRenderedCacheItem *item in items) {
if ([item strongify]) {
anyStrongified = YES;
[_items addObject:item];
}
}
return anyStrongified;
}
#pragma mark Private
- (void)_insertEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceImageDimensions
{
TIPRenderedCacheItem *item = [[TIPRenderedCacheItem alloc] initWithEntry:entry
transformerIdentifier:transformerIdentifier
sourceImageDimensions:sourceImageDimensions];
[_items insertObject:item atIndex:0];
}
@end
@implementation TIPRenderedCacheItem
{
id _weakifyDescriptor;
__weak UIImage *_weakifyImage;
}
- (instancetype)initWithEntry:(TIPImageCacheEntry *)entry
transformerIdentifier:(nullable NSString *)transformerIdentifier
sourceImageDimensions:(CGSize)sourceDims
{
if (self = [super init]) {
_entry = entry;
_transformerIdentifier = [transformerIdentifier copy];
_sourceImageDimensions = sourceDims;
}
return self;
}
- (void)markDirty
{
_dirty = YES;
}
- (void)weakify
{
TIPImageContainer *container = _entry.completeImage;
TIPAssert(container != nil);
if (container) {
_weakifyDescriptor = container.descriptor;
_weakifyImage = container.image;
_entry.completeImage = nil;
}
}
- (BOOL)strongify
{
UIImage *image = _weakifyImage;
id descriptor = _weakifyDescriptor;
_weakifyDescriptor = nil;
_weakifyImage = nil;
if (!image) {
return NO;
}
TIPImageContainer *container = [TIPImageContainer imageContainerWithImage:image
descriptor:descriptor];
if (!container) {
return NO;
}
_entry.completeImage = container;
return YES;
}
@end
NS_ASSUME_NONNULL_END