Skip to content

Cache

The Cache module exposes methods for storing and retrieving data in a cache. The cache is a key/value store that provides a temporary storage of items that are expensive to create or load, for example, using remote network calls. The maximum storage capacity can be specified by the user.

Because the cache has limited storage capacity, any new item added to a full cache requires an item be removed first. The cache implementation and cache replacement policy specifies which item is to be removed.

The SDK provides the following implementations:

  • MemoryCache: A fixed-size, in-memory cache that uses weak reference keys to store a set of values
  • CompositeCache: A multi-level cache container that implements the Cache interface
  • SecureStoreCache: A fixed size cache that supports storing key-value data in an encrypted persistence store

All implementations provides the following methods:

  • Creation - Uses the constructor to create an instance of the cache
  • Configuration - Configures the features and options of the cache and enables operations for storing, updating, retrieving, and deleting data
  • Operations for:
    • Storing (create or update) - V put(K key, V value)
    • Retrieving - V get(K key), CacheEntry getEntry(K key), int getEntryCount(), List<K> keys()
    • Deleting - void remove(K key), void removeAll()

The cache Creation and Configuration methods are Cache implementation dependent. The data operations are common but with additional implementation specific semantics.

MemoryCache

MemoryCache is a fixed size in-memory cache that uses weak reference keys to store a set of values. It supports the following features and options:

  • Least Recently Used Cache Replacement Policy: Each time a value is accessed, it is moved to the end of a queue in descending Least Recently Used (LRU) order.

    When an entry is added to a full cache, the entry at the head of that queue, that is, the Least Recently Used entry is evicted to make room for the new entry.

  • Clear on Memory Error Option: When this option is enabled, a low memory situation detected by the application causes the entire cache to be cleared.

  • Cost Factor Option: The cache can be configured with a "maximum cost". A cost provided by the user is associated with each cache entry during the put operation.

    In addition to the LRU Replacement Policy, when the aggregated cost exceeds the maximum cost, one or more cache entries based on system or user-defined eviction criteria are removed to make room for the current entry, then adds the current entry to the cache:

    • System Eviction Criteria: The current entry with the highest cost is removed.
    • Customized Eviction Criteria: A list of existing entries sorted in ascending cost order is presented to the user using a callback interface CacheCostFactor. The user-defined callback method List<K> onExceedMaxCost(final List<K> existingKeys) can select and return a list of one or more entries from the input list to evict.

Before interacting with the MemoryCache, you need to instantiate and configure the MemoryCache. Any operation performed before the cache is properly configured will incur an error.

Creating

class Employee {
    private String firstName;
    private String lastName;
    private String id;

    Employee(String firstName, String lastName, String id) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.id = id;
    }
}

final int maxSize = 32;

// Specifies the key and value type, Android application context and maximum size.
MemoryCache<String, Employee>  cache = new MemoryCache<>(context, maxSize);
internal inner class Employee(private val firstName: String, private val lastName: String, private val id: String)

val maxSize = 32

// Specifies the key and value type, Android application context and maximum size.
val cache = MemoryCache<String, Employee>(context, maxSize)

Configuring

  • With No Options

    Configure a memory cache with no "on low memory error" and "cost factor" options.

    cache.build();
    
    cache.build()
    
  • Clear On Memory Error Option

    Enable the Clear On Memory Error option. The user must invoke public void onLowMemory() in Android ComponentCallbacks onLowMemory() life cycle method.

    cache.clearOnMemoryError().build();
    
    cache.clearOnMemoryError().build()
    
  • Cost Factor Option with System Default Eviction Criteria

    Enable and specify the maximum total cost, and use the system default eviction criteria.

    cache.maxCost(5.0).build();
    
    cache.maxCost(5.0).build()
    
  • Cost Factor Option with Custom Eviction Criteria

    In addition to the maximum total cost, you can provide custom eviction criteria using the CacheCostFactor interface.

    cache.maxCost(5.0)
        .addCostFactor(new CacheCostFactor<String>() {
            @Override
            public List<String> onExceedMaxCost(List<String> existingKeys) {
                // This custom Eviction policy removes the two least costly entries if available.
                ArrayList<String> removeList = new ArrayList<>();
                int size = existingKeys.size();
    
                if (size > 0)
                removeList.add(existingKeys.get(size - 1));// Remove the most costly entry
                if (size >= 2)
                removeList.add(existingKeys.get(size - 2));// Remove the second most costly entry
                return removeList;
            }
        })
        .build();
    
    Employee employee1 = new Employee("John", "Lee", "E1034");
    Employee employee2 = new Employee("Erin", "Johnson", "E1028");
    Employee employee3 = new Employee("Mary", "Smith", "E1109");
    Employee employee4 = new Employee("Joe", "Lin", "E1203");
    
    // Adds entries with cost.
    cache.put("E1034", employee1, 0.8);
    cache.put("E1028", employee2, 2.3); // Most costly.
    cache.put("E1109", employee3, 1.8); // Second most costly.
    
    // Aggregated cost so far is 4.9.
    // By adding the entry 'E1203' below, the aggregated cost will become 6.2,
    // and is greater than the max cost 5.0.
    // Thus, based on the custom Eviction criteria, the two most costly
    // entries will be removed first before adding the entry below.
    cache.put("E1203", employee4, 1.3);
    
    cache.maxCost(5.0)
        .addCostFactor(CacheCostFactor<String> { existingKeys ->
            // This custom Eviction policy removes the two least costly entries if available.
            val removeList = ArrayList<String>()
            val size = existingKeys.size
    
            if (size > 0)
                removeList.add(existingKeys[size - 1])// Remove the most costly entry
            if (size >= 2)
                removeList.add(existingKeys[size - 2])// Remove the second most costly entry
            removeList
        })
        .build()
    
    val employee1 = Employee("John", "Lee", "E1034")
    val employee2 = Employee("Erin", "Johnson", "E1028")
    val employee3 = Employee("Mary", "Smith", "E1109")
    val employee4 = Employee("Joe", "Lin", "E1203")
    
    // Adds entries with cost.
    cache.put("E1034", employee1, 0.8)
    cache.put("E1028", employee2, 2.3) // Most costly.
    cache.put("E1109", employee3, 1.8) // Second most costly.
    
    // Aggregated cost so far is 4.9.
    // By adding the entry 'E1203' below, the aggregated cost will become 6.2,
    // and is greater than the max cost 5.0.
    // Thus, based on the custom Eviction criteria, the two most costly
    // entries will be removed first before adding the entry below.
    cache.put("E1203", employee4, 1.3)
    

SecureStoreCache

SecureStoreCache is a fixed-size cache that supports storing key-value data in an encrypted persistence store. It uses an LRU (Least Recently Used) policy for removing data when the size limit is reached.

  • Key Type - String
  • Value Types - One of Boolean, Byte, Double, Float, Integer, Long, Short, String, byte[], or any class that implements the Serializable interface.

Creating and Opening the SecureStoreCache

To create a SecureStoreCache, specify the:

  • Android application context
  • Size of the cache
  • Name of the cache

The persistence store for the cache is created when it doesn't already exist. Subsequent calls return a reference to the existing cache.

After a SecureStoreCache is created, open the cache with an encryption key which is used to encrypt the underlying persistence store. It is recommended that you use the Encryption Utility to obtain the encryption key. If it is the first time the cache is being opened and null is provided as the encryption key, a key is generated transparently and will be used for subsequent open calls.

See Encryption Utility for more information.

SecureStoreCache<String> cache =
new SecureStoreCache<>(
    context,       // Android application context
    32,            // size of the cache
    "myStoreName");// name of the secure store cache

try {
    final byte[] encryptionKey = EncryptionUtil.getEncryptionKey("aliasForSecureStoreCache", myPasscode);
    cache.open(encryptionKey);

    // Or passes null and the cache will generate an encryption key
    // which can be used for subsequent open calls, e.g., cache.open(null);
} catch (OpenFailureException ex) {
    // Possibly due to incorrect encryption key.
    logger.error("Failed to open persistence store.", ex);
} catch (EncryptionError ex) {
    logger.error("Failed to get encryption key.", ex);
}
val cache = SecureStoreCache<String>(
    context!!,     // Android application context
    32,            // size of the cache
    "myStoreName") // name of the secure store cache

try {
    val encryptionKey = EncryptionUtil.getEncryptionKey("aliasForSecureStoreCache", myPasscode)
    cache.open(encryptionKey)

    // Or passes null and the cache will generate an encryption key
    // which can be used for subsequent open calls, e.g., cache.open(null)
} catch (ex: OpenFailureException) {
    // Possibly due to incorrect encryption key
    logger.error("Failed to open persistence store.", ex)
} catch (ex: EncryptionError) {
    logger.error("Failed to get encryption key.", ex)
}

Cache Data Operations

// Create operations
cache.put("myKey", "myStringValue"); // adds a Serializable object

// Read operations
String retrievedValue = cache.get("myKey");

// Iterates through existing keys
List<String> keyList = cache.keys();
for (String key : keyList) {
    logger.debug("Secure Store Key: {} ", key);
}

// Retrieves the number of entries in SecureStoreCache
int totalEntries = cache.getEntryCount();

// Update operations
cache.put("myKey", "newStringValue");

// Delete (remove) operations
cache.remove("myKey"); // removes an entry by its key

cache.removeAll();     // removes all entries
// Create operations
cache.put("myKey", "myStringValue") // adds a Serializable object

// Read operations
val retrievedValue = cache.get("myKey")

// Iterates through existing keys
val keyList = cache.keys()
for (key in keyList) {
    logger.debug("Secure Store Key: {} ", key)
}

// Retrieves the number of entries in SecureStoreCache
val totalEntries = cache.entryCount

// Update operations
cache.put("myKey", "newStringValue")

// Delete (remove) operations
cache.remove("myKey") // removes an entry by its key

cache.removeAll()     // removes all entries

Closing

When you finish executing cache operations, close the SecureStoreCache so that the underlying persistence store relinquishes any resources it has acquired during its operation.

cache.close();
cache.close()

CompositeCache

CompositeCache is a multi-level cache container that implements the Cache interface. The lowest level is usually configured with a persistent store.

  • The cache at each level is appended to a chain from highest level to lowest level. Order matters, and the user should add the backing store last.
  • All levels of caches share the same Key and Value types.
  • The sizes of cache levels should be from the smallest to the largest.
  • Write Policy - uses Write-through Policy which uses a put operation to add the entry to all levels of the cache.
  • Read-miss Policy:

    • Only when a cache entry is not found at one level, the next lower level is queried.
    • The item found at a lower level is propagated to all the cache levels above.
  • An entry is cleared from the cache to free up space based on the Replacement Policy of the individual cache.

Creating and Configuring CompositeCache

To create a composite cache, you need to construct cache for each level, instantiate a CompositeCache, then add the caches to the composite cache.

// Instantiates the top level memory cache.
MemoryCache<String, String> memoryCache =
new MemoryCache<String, String>(context, 50).build();

// Instantiates the bottom level secure store cache.
SecureStoreCache<String> secureStoreCache =
new SecureStoreCache<>(context, 200, "mySecureStore");

// Needs to open the secure store cache first
try {
    byte[] encryptionKey = EncryptionUtil.getEncryptionKey("secureStoreKeyAlias", myPasscode);
    secureStoreCache.open(encryptionKey);
} catch (OpenFailureException ex) {
    // Possibly due to incorrect encryption key
    logger.error("Failed to open persistence store.", ex);
} catch (EncryptionError ex) {
    logger.error("Failed to get encryption key.", ex);
}

// Forms the composite cache
CompositeCache<String, String> compositeCache = new CompositeCache<String, String>(context)
    .add(memoryCache) // Uses memory cache as the top level cache for optimal read performance.
    .add(secureStoreCache)// Bottom level-- persistence layer.
    .build();

// Performs normal cache operations on the composite cache...
compositeCache.put("key1", "value1");
compositeCache.put("key2", "value2");

String key1Value = compositeCache.get("key1");
String key2Value = compositeCache.get("key2");

compositeCache.put("key1", "newValue1");
compositeCache.put("key2", "newValue2");

compositeCache.remove("key1");
compositeCache.remove("key2");


// Need to close the secureStoreCache when the cache is no longer used.
secureStoreCache.close();
// Instantiates the top level memory cache.
val memoryCache = MemoryCache<String, String>(context, 50).build()

// Instantiates the bottom level secure store cache.
val secureStoreCache = SecureStoreCache<String>(context!!, 200, "mySecureStore")

// Needs to open the secure store cache first
try {
    val encryptionKey = EncryptionUtil.getEncryptionKey("secureStoreKeyAlias", myPasscode)
    secureStoreCache.open(encryptionKey)
} catch (ex: OpenFailureException) {
    // Possibly due to incorrect encryption key
    logger.error("Failed to open persistence store.", ex)
} catch (ex: EncryptionError) {
    logger.error("Failed to get encryption key.", ex)
}

// Forms the composite cache
val compositeCache = CompositeCache<String, String>(context)
    .add(memoryCache) // Uses memory cache as the top level cache for optimal read performance.
    .add(secureStoreCache)// Bottom level-- persistence layer.
    .build()

// Performs normal cache operations on the composite cache...
compositeCache.put("key1", "value1")
compositeCache.put("key2", "value2")

val key1Value = compositeCache.get("key1")
val key2Value = compositeCache.get("key2")

compositeCache.put("key1", "newValue1")
compositeCache.put("key2", "newValue2")

compositeCache.remove("key1")
compositeCache.remove("key2")


// Need to close the secureStoreCache when the cache is no longer used.
secureStoreCache.close()

Last update: June 7, 2022