TwitterImagePipeline/Project/TIPImageMemoryCache.m (353 lines of code) (raw):
//
// TIPImageMemoryCache.m
// TwitterImagePipeline
//
// Created on 3/3/15.
// Copyright (c) 2015 Twitter, Inc. All rights reserved.
//
#import <UIKit/UIApplication.h>
#import "TIP_Project.h"
#import "TIPGlobalConfiguration+Project.h"
#import "TIPImageCacheEntry.h"
#import "TIPImageMemoryCache.h"
#import "TIPImagePipeline+Project.h"
#import "TIPImagePipelineInspectionResult+Project.h"
#import "TIPLRUCache.h"
NS_ASSUME_NONNULL_BEGIN
@interface TIPImageMemoryCache () <TIPLRUCacheDelegate>
@property (tip_atomic_direct) SInt64 atomicTotalCost;
@end
TIP_OBJC_DIRECT_MEMBERS
@interface TIPImageMemoryCache (Private)
- (BOOL)_memoryCache_updateEntry:(TIPImageMemoryCacheEntry *)entry
withPartialImage:(TIPPartialImage *)partialImage
context:(TIPPartialImageEntryContext *)context;
- (BOOL)_memoryCache_updateEntry:(TIPImageMemoryCacheEntry *)entry
withCompleteImageData:(NSData *)completeImageData
context:(TIPCompleteImageEntryContext *)context;
- (void)_memoryCache_didEvictEntry:(TIPImageMemoryCacheEntry *)entry;
- (void)_memoryCache_inspect:(TIPInspectableCacheCallback)callback;
- (void)_memoryCache_updateByteCountsAdded:(UInt64)bytesAdded
removed:(UInt64)bytesRemoved;
@end
@implementation TIPImageMemoryCache
{
TIPGlobalConfiguration *_globalConfig;
TIPLRUCache *_manifest;
}
@synthesize manifest = _manifest;
- (NSUInteger)totalCost
{
return (NSUInteger)self.atomicTotalCost;
}
- (TIPImageCacheType)cacheType
{
return TIPImageCacheTypeMemory;
}
- (instancetype)init
{
if (self = [super init]) {
_globalConfig = [TIPGlobalConfiguration sharedInstance];
_manifest = [[TIPLRUCache alloc] initWithEntries:nil delegate:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_tip_memoryCache_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 = self.atomicTotalCost;
const SInt16 totalCount = (SInt16)_manifest.numberOfEntries;
TIPGlobalConfiguration *config = _globalConfig;
tip_dispatch_async_autoreleasing(config.queueForMemoryCaches, ^{
config.internalTotalBytesForAllMemoryCaches -= totalSize;
config.internalTotalCountForAllMemoryCaches -= totalCount;
});
}
- (void)_tip_memoryCache_didReceiveMemoryWarning:(NSNotification *)note
{
[self clearAllImages:NULL];
}
- (nullable TIPImageMemoryCacheEntry *)imageEntryForIdentifier:(NSString *)identifier
targetDimensions:(CGSize)targetDimensions
targetContentMode:(UIViewContentMode)targetContentMode
decoderConfigMap:(nullable NSDictionary<NSString *,id> *)configMap
{
TIPAssert(identifier != nil);
if (!identifier) {
return nil;
}
__block TIPImageMemoryCacheEntry *entry;
tip_dispatch_sync_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
// Get entry
entry = (TIPImageMemoryCacheEntry *)[self->_manifest entryWithIdentifier:identifier];
if (entry) {
// Validate TTL
NSDate *now = [NSDate date];
NSDate *lastAccess = nil;
NSUInteger oldCost = entry.memoryCost;
lastAccess = entry.partialImageContext.lastAccess;
if (lastAccess && [now timeIntervalSinceDate:lastAccess] > entry.partialImageContext.TTL) {
TIPAssert(entry.partialImageContext.TTL > 0.0);
entry.partialImageContext = nil;
entry.partialImage = nil;
}
lastAccess = entry.completeImageContext.lastAccess;
if (lastAccess && [now timeIntervalSinceDate:lastAccess] > entry.completeImageContext.TTL) {
TIPAssert(entry.completeImageContext.TTL > 0.0);
entry.completeImageContext = nil;
entry.completeImage = nil;
entry.completeImageData = nil;
}
// Resolve changes to entry
NSUInteger newCost = entry.memoryCost;
if (!newCost) {
[self->_manifest removeEntry:entry];
entry = nil;
} else {
[self _memoryCache_updateByteCountsAdded:newCost
removed:oldCost];
TIPAssert(newCost <= oldCost); // removing the cache image and/or partial image only ever removes bytes
}
// Retrieve the image based on target sizing
if (entry.completeImageData != nil) {
entry.completeImage = [TIPImageContainer imageContainerWithData:entry.completeImageData
targetDimensions:targetDimensions
targetContentMode:targetContentMode
decoderConfigMap:configMap
codecCatalogue:nil];
}
// Update entry
if (entry) {
if (entry.partialImageContext.updateExpiryOnAccess) {
entry.partialImageContext.lastAccess = now;
}
if (entry.completeImageContext.updateExpiryOnAccess) {
entry.completeImageContext.lastAccess = now;
}
}
}
entry = [entry copy]; // return a copy for thread safety
});
return entry;
}
- (void)updateImageEntry:(TIPImageCacheEntry *)entry forciblyReplaceExisting:(BOOL)force
{
TIPAssert(entry);
if (!entry) {
return;
}
tip_dispatch_async_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
if (![entry isValid:NO]) {
return;
}
NSString *identifier = entry.identifier;
TIPImageMemoryCacheEntry *currentEntry = (TIPImageMemoryCacheEntry *)[self->_manifest entryWithIdentifier:identifier];
BOOL updatedCompleteImage = NO, updatedPartialImage = NO;
if (currentEntry && !force) {
if (entry.completeImage) {
updatedCompleteImage = [self _memoryCache_updateEntry:currentEntry
withCompleteImageData:entry.completeImageData
context:entry.completeImageContext];
} else if (entry.partialImage) {
updatedPartialImage = [self _memoryCache_updateEntry:currentEntry
withPartialImage:entry.partialImage
context:entry.partialImageContext];
}
} else {
if (currentEntry) {
[self->_manifest removeEntry:currentEntry];
currentEntry = nil;
}
if (entry.completeImageData || entry.partialImage) {
// extract entry
currentEntry = [[TIPImageMemoryCacheEntry alloc] init];
currentEntry.identifier = identifier;
currentEntry.completeImageData = entry.completeImageData;
currentEntry.completeImageContext = [entry.completeImageContext copy];
currentEntry.completeImage = nil; // no image, just data
currentEntry.partialImage = entry.partialImage;
currentEntry.partialImageContext = [entry.partialImageContext copy];
updatedPartialImage = updatedCompleteImage = YES;
TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance];
// Cap our entry size
BOOL didClear = NO;
NSUInteger cost = currentEntry.memoryCost;
const SInt64 max = [globalConfig internalMaxBytesForCacheEntryOfType:self.cacheType];
if ((SInt64)cost > max && currentEntry.partialImageContext) {
currentEntry.partialImageContext = nil;
currentEntry.partialImage = nil;
didClear = YES;
cost = currentEntry.memoryCost;
}
if ((SInt64)cost > max && currentEntry.completeImageContext) {
currentEntry.completeImageContext = nil;
currentEntry.completeImage = nil;
didClear = YES;
cost = currentEntry.memoryCost;
}
if (cost > 0 || !didClear) {
globalConfig.internalTotalCountForAllMemoryCaches += 1;
[self _memoryCache_updateByteCountsAdded:cost
removed:0];
if (gTwitterImagePipelineAssertEnabled && 0 == cost) {
NSDictionary *info = @{
@"dimension" : NSStringFromCGSize([currentEntry.completeImageContext ?: currentEntry.partialImageContext dimensions]),
@"URL" : currentEntry.completeImageContext.URL ?: currentEntry.partialImageContext.URL,
@"id" : currentEntry.identifier,
};
TIPLogError(@"Cached zero cost image to memory cache %@", info);
}
[self->_manifest addEntry:currentEntry];
NSDate *now = [NSDate date];
currentEntry.partialImageContext.lastAccess = now;
currentEntry.completeImageContext.lastAccess = now;
}
}
}
if (updatedCompleteImage || updatedPartialImage) {
[[TIPGlobalConfiguration sharedInstance] pruneAllCachesOfType:self.cacheType
withPriorityCache:self];
}
});
}
- (void)touchImageWithIdentifier:(NSString *)identifier
{
TIPAssert(identifier != nil);
if (!identifier) {
return;
}
tip_dispatch_async_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
(void)[self->_manifest entryWithIdentifier:identifier];
});
}
- (void)clearImageWithIdentifier:(NSString *)identifier
{
TIPAssert(identifier != nil);
if (!identifier) {
return;
}
tip_dispatch_async_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
TIPImageMemoryCacheEntry *entry = (TIPImageMemoryCacheEntry *)[self->_manifest entryWithIdentifier:identifier];
[self->_manifest removeEntry:entry];
});
}
- (void)clearAllImages:(nullable void (^)(void))completion
{
tip_dispatch_async_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
const SInt16 totalCount = (SInt16)self->_manifest.numberOfEntries;
[self->_manifest clearAllEntries];
[TIPGlobalConfiguration sharedInstance].internalTotalCountForAllMemoryCaches -= totalCount;
[self _memoryCache_updateByteCountsAdded:0
removed:(UInt64)self.atomicTotalCost];
TIPLogInformation(@"Cleared all images in %@", self);
if (completion) {
completion();
}
});
}
#pragma mark Delegate
- (void)tip_cache:(TIPLRUCache *)manifest didEvictEntry:(TIPImageMemoryCacheEntry *)entry
{
[TIPGlobalConfiguration sharedInstance].internalTotalCountForAllMemoryCaches -= 1;
[self _memoryCache_updateByteCountsAdded:0 removed:entry.memoryCost];
[self _memoryCache_didEvictEntry:entry];
}
#pragma mark Inspect
- (void)inspect:(TIPInspectableCacheCallback)callback
{
tip_dispatch_async_autoreleasing(_globalConfig.queueForMemoryCaches, ^{
[self _memoryCache_inspect:callback];
});
}
@end
#pragma mark Private
@implementation TIPImageMemoryCache (Private)
- (void)_memoryCache_updateByteCountsAdded:(UInt64)bytesAdded
removed:(UInt64)bytesRemoved
{
TIP_UPDATE_BYTES(self.atomicTotalCost, bytesAdded, bytesRemoved, @"Memory Cache Size");
TIP_UPDATE_BYTES([TIPGlobalConfiguration sharedInstance].internalTotalBytesForAllMemoryCaches, bytesAdded, bytesRemoved, @"All Memory Caches Size");
}
- (BOOL)_memoryCache_updateEntry:(TIPImageMemoryCacheEntry *)entry
withPartialImage:(TIPPartialImage *)partialImage
context:(TIPPartialImageEntryContext *)context
{
if (!partialImage || !context) {
return NO;
}
if (context.treatAsPlaceholder) {
return NO;
}
const CGSize newDimensions = context.dimensions;
const CGSize oldDimensions = (entry.partialImageContext) ? entry.partialImageContext.dimensions : CGSizeZero;
// IMPORTANT: We use "last in wins" logic.
// It is easier for clients to detect larger varients matching smaller varients
// than smaller variants matching larger variants.
// This way, clients can load the smaller variant first, load the larger variant second and
// (next time they access smaller or larger variant) the larger variant is cached.
if ((newDimensions.width * newDimensions.height) < (oldDimensions.width * oldDimensions.height)) {
return NO;
}
// Update
const NSUInteger oldCost = entry.memoryCost;
entry.partialImageContext = context;
entry.partialImage = partialImage;
const NSUInteger newCost = entry.memoryCost;
[self _memoryCache_updateByteCountsAdded:newCost
removed:oldCost];
TIPAssert(entry.partialImage != nil);
return YES;
}
- (BOOL)_memoryCache_updateEntry:(TIPImageMemoryCacheEntry *)entry
withCompleteImageData:(NSData *)completeImageData
context:(TIPCompleteImageEntryContext *)context
{
if (!completeImageData || !context) {
return NO;
}
BOOL skipAhead = NO;
if (entry.completeImageContext.treatAsPlaceholder != context.treatAsPlaceholder) {
if (entry.completeImageContext.treatAsPlaceholder) {
skipAhead = YES;
} else if (context.treatAsPlaceholder) {
return NO;
}
}
const CGSize newDimensions = context.dimensions;
CGSize oldDimensions = (entry.completeImageContext) ? entry.completeImageContext.dimensions : CGSizeZero;
if (!skipAhead) {
// IMPORTANT: We use "last in wins" logic.
// It is easier for clients to detect larger varients matching smaller varients
// than smaller variants matching larger variants.
// This way, clients can load the smaller variant first, load the larger variant second and
// (next time they access smaller or larger variant) the larger variant is cached.
if ((newDimensions.width * newDimensions.height) < (oldDimensions.width * oldDimensions.height)) {
return NO;
}
// Don't update if identical
if (CGSizeEqualToSize(newDimensions, oldDimensions) && [entry.completeImageContext.URL isEqual:context.URL]) {
return NO;
}
}
// Update
const NSUInteger oldCost = entry.memoryCost;
entry.completeImageContext = context;
entry.completeImageData = completeImageData;
entry.completeImage = nil; // just keep the data, not the image
if (skipAhead || entry.partialImageContext) {
oldDimensions = entry.partialImageContext.dimensions;
if (skipAhead || (oldDimensions.height * oldDimensions.width) <= (newDimensions.height * newDimensions.width)) {
// latest is larger than partial
entry.partialImageContext = nil;
entry.partialImage = nil;
}
}
const NSUInteger newCost = entry.memoryCost;
[self _memoryCache_updateByteCountsAdded:newCost
removed:oldCost];
TIPAssert(entry.completeImageData != nil);
return YES;
}
- (void)_memoryCache_didEvictEntry:(TIPImageMemoryCacheEntry *)entry
{
TIPLogDebug(@"%@ Evicted '%@', complete:'%@', partial:'%@'", NSStringFromClass([self class]), entry.identifier, entry.completeImageContext.URL, entry.partialImageContext.URL);
}
- (void)_memoryCache_inspect:(TIPInspectableCacheCallback)callback
{
NSMutableArray *completedEntries = [[NSMutableArray alloc] init];
NSMutableArray *partialEntries = [[NSMutableArray alloc] init];
for (TIPImageMemoryCacheEntry *cacheEntry in _manifest) {
TIPImagePipelineInspectionResultEntry *entry;
entry = [TIPImagePipelineInspectionResultEntry entryWithCacheEntry:cacheEntry
class:[TIPImagePipelineInspectionResultCompleteMemoryEntry class]];
if (entry) {
[completedEntries addObject:entry];
}
entry = [TIPImagePipelineInspectionResultEntry entryWithCacheEntry:cacheEntry
class:[TIPImagePipelineInspectionResultPartialMemoryEntry class]];
if (entry) {
[partialEntries addObject:entry];
}
}
callback(completedEntries, partialEntries);
}
@end
NS_ASSUME_NONNULL_END