TwitterImagePipeline/Project/TIPLRUCache.m (244 lines of code) (raw):
//
// TIPLRUCache.m
// TwitterImagePipeline
//
// Created on 10/27/15.
// Copyright © 2020 Twitter. All rights reserved.
//
#import "TIP_Project.h"
#import "TIPLRUCache.h"
NS_ASSUME_NONNULL_BEGIN
@interface TIPLRUCache ()
@property (nonatomic, readonly, nonnull) NSMutableDictionary<NSString *, id<TIPLRUEntry>> *cache;
- (void)clearEntry:(nonnull id<TIPLRUEntry>)entry;
- (void)moveEntryToFront:(nonnull id<TIPLRUEntry>)entry;
@end
NS_INLINE void TIPLRUCacheAssertHeadAndTail(TIPLRUCache *cache)
{
if (gTwitterImagePipelineAssertEnabled) {
TIPAssert(!cache.headEntry == !cache.tailEntry);
if (cache.headEntry) {
TIPAssert(cache.cache[cache.headEntry.LRUEntryIdentifier] == cache.headEntry);
}
if (cache.tailEntry) {
TIPAssert(cache.cache[cache.tailEntry.LRUEntryIdentifier] == cache.tailEntry);
}
}
}
@implementation TIPLRUCache
{
struct {
BOOL delegateSupportsDidEvictSelector;
BOOL delegateSupportsCanEvictSelector;
} _flags;
NSInteger _mutationCheckInteger;
}
- (instancetype)init
{
return [self initWithEntries:nil delegate:nil];
}
- (instancetype)initWithEntries:(nullable NSArray *)arrayOfLRUEntries delegate:(nullable id<TIPLRUCacheDelegate>)delegate
{
if (self = [super init]) {
_cache = [[NSMutableDictionary alloc] init];
[self internalSetDelegate:delegate];
for (id<TIPLRUEntry> entry in arrayOfLRUEntries) {
[self appendEntry:entry];
}
}
return self;
}
- (void)dealloc
{
[self nullifyEntryLinks];
}
- (void)internalSetDelegate:(nullable id<TIPLRUCacheDelegate>)delegate
{
_flags.delegateSupportsDidEvictSelector = (NO != [delegate respondsToSelector:@selector(tip_cache:didEvictEntry:)]);
_flags.delegateSupportsCanEvictSelector = (NO != [delegate respondsToSelector:@selector(tip_cache:canEvictEntry:)]);
_delegate = delegate;
}
- (void)setDelegate:(nullable id<TIPLRUCacheDelegate>)delegate
{
[self internalSetDelegate:delegate];
}
#pragma mark Setting
- (void)addEntry:(id<TIPLRUEntry>)entry
{
TIPAssert(entry != nil);
if (entry == nil) {
return;
}
if (entry == _headEntry) {
return;
}
NSString *identifier = entry.LRUEntryIdentifier;
TIPAssert(identifier != nil);
#ifndef __clang_analyzer__
// clang analyzer reports identifier can be nil for the check if (... && _cache[identifier]) {
// just below. (and then, if we ignore only that with #ifndef __clang_analyzer__, then it reports
// _cache[identifier] within the TIPAssert() protected by the if stmt.)
// however, in real life, the TIPAssert(identifier != nil) just above will prevent control from getting to
// the if stmt at all when gTwitterImagePipelineAssertEnabled is true, and when it is false, then the 2nd
// part of the condition (and the stmts protected by the condition) will never be executed and won't crash.
if (gTwitterImagePipelineAssertEnabled && _cache[identifier]) {
TIPAssert((id)_cache[identifier] == (id)entry);
}
#endif
[self moveEntryToFront:entry];
#ifndef __clang_analyzer__ // reports identifier can be nil; we prefer to crash if it is
_cache[identifier] = entry;
#endif
TIPLRUCacheAssertHeadAndTail(self);
}
- (void)appendEntry:(id<TIPLRUEntry>)entry
{
TIPAssert(entry != nil);
if (entry == nil) {
return;
}
if (entry == _tailEntry) {
return;
}
NSString *identifier = entry.LRUEntryIdentifier;
TIPAssert(identifier != nil);
_tailEntry.nextLRUEntry = entry;
entry.previousLRUEntry = _tailEntry;
entry.nextLRUEntry = nil;
_tailEntry = entry;
#ifndef __clang_analyzer__ // reports identifier can be nil; we prefer to crash if it is
_cache[identifier] = entry;
#endif
if (!_headEntry) {
_headEntry = _tailEntry;
}
_mutationCheckInteger++;
TIPLRUCacheAssertHeadAndTail(self);
}
#pragma mark Getting
- (NSUInteger)numberOfEntries
{
return _cache.count;
}
- (nullable id<TIPLRUEntry>)entryWithIdentifier:(NSString *)identifier canMutate:(BOOL)canMutate
{
id<TIPLRUEntry> entry = _cache[identifier];
if (canMutate && entry) {
[self moveEntryToFront:entry];
}
return entry;
}
- (nullable id<TIPLRUEntry>)entryWithIdentifier:(NSString *)identifier
{
return [self entryWithIdentifier:identifier canMutate:YES];
}
- (NSArray *)allEntries
{
NSMutableArray *entries = [[NSMutableArray alloc] initWithCapacity:self.numberOfEntries];
id<TIPLRUEntry> current = self.headEntry;
while (current != nil) {
[entries addObject:current];
current = current.nextLRUEntry;
}
return entries;
}
#pragma mark Removal
- (void)removeEntry:(nullable id<TIPLRUEntry>)entry
{
if (!entry) {
return;
}
NSString *identifier = entry.LRUEntryIdentifier;
TIPAssert(identifier != nil);
if (!identifier) {
return;
}
TIPAssert(_cache[identifier] == entry);
[self clearEntry:entry];
[_cache removeObjectForKey:identifier];
TIPLRUCacheAssertHeadAndTail(self);
if (_flags.delegateSupportsDidEvictSelector) {
[_delegate tip_cache:self didEvictEntry:entry];
}
}
- (nullable id<TIPLRUEntry>)removeTailEntry
{
id<TIPLRUEntry> entry = _tailEntry;
id<TIPLRUCacheDelegate> delegate = self.delegate;
while (entry && _flags.delegateSupportsCanEvictSelector && ![delegate tip_cache:self canEvictEntry:entry]) {
entry = entry.previousLRUEntry;
}
[self removeEntry:entry];
return entry;
}
#pragma mark Other
- (void)clearAllEntries
{
[self nullifyEntryLinks];
_tailEntry = nil;
_headEntry = nil;
[_cache removeAllObjects];
_mutationCheckInteger++;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained __nullable [__nonnull])buffer count:(NSUInteger)len
{
// Prep the number of enumerations made in this pass
NSUInteger count = 0;
// Initialization
if (!state->state && !state->extra[0]) {
// Track mutations using the current "start" location
state->mutationsPtr = (void *)&_mutationCheckInteger;
// Set the state as the current item to iterate on
state->state = (unsigned long)_headEntry;
// Flag that we started
state->extra[0] = 1UL;
}
// Set the items pointer to the provided convenience buffer
state->itemsPtr = buffer;
// Now we provide items
for ( ; state->state != 0UL && count < len; count++) {
// Get the current entry
__unsafe_unretained id<TIPLRUEntry> entry = (__bridge id<TIPLRUEntry>)(void *)state->state;
// Add the current entry to the buffer
buffer[count] = entry;
// Get the next entry
entry = entry.nextLRUEntry;
// Update the state to the next entry
state->state = (unsigned long)entry;
}
return count; // count of 0 ends the enumeration
}
#pragma mark Private
- (void)clearEntry:(id<TIPLRUEntry>)entry
{
id<TIPLRUEntry> prev = entry.previousLRUEntry;
id<TIPLRUEntry> next = entry.nextLRUEntry;
prev.nextLRUEntry = next;
next.previousLRUEntry = prev;
entry.previousLRUEntry = nil;
entry.nextLRUEntry = nil;
if (entry == _tailEntry) {
_tailEntry = prev;
}
if (entry == _headEntry) {
_headEntry = next;
}
_mutationCheckInteger++;
}
- (void)moveEntryToFront:(id<TIPLRUEntry>)entry
{
if (_headEntry == entry) {
return;
}
id<TIPLRUEntry> previous = entry.previousLRUEntry;
if (previous) {
// in the linked list
BOOL update = entry.shouldAccessMoveLRUEntryToHead;
if (!update) {
// don't update in LRU
return;
}
}
if (entry == _tailEntry) {
_tailEntry = previous;
}
previous.nextLRUEntry = entry.nextLRUEntry;
entry.nextLRUEntry.previousLRUEntry = previous;
entry.previousLRUEntry = nil;
entry.nextLRUEntry = _headEntry;
_headEntry.previousLRUEntry = entry;
_headEntry = entry;
if (!_tailEntry) {
_tailEntry = entry;
TIPAssert(entry.nextLRUEntry == nil);
TIPAssert(previous == nil);
}
_mutationCheckInteger++;
TIPAssert(!_headEntry == !_tailEntry);
}
- (void)nullifyEntryLinks
{
// removing all entries via weak dealloc chaining
// can yield a stack overflow!
// use iterative removal instead
id<TIPLRUEntry> entryToRemove = _headEntry;
while (entryToRemove) {
id<TIPLRUEntry> nextEntry = entryToRemove.nextLRUEntry;
entryToRemove.nextLRUEntry = nil;
entryToRemove.previousLRUEntry = nil;
entryToRemove = nextEntry;
}
}
@end
NS_ASSUME_NONNULL_END