LRU Cache (Least Recently Used Cache) est un algorithme de mise en cache qui évince les éléments qui n'ont pas été utilisés depuis le plus longtemps lorsque la taille du cache atteint sa limite. À chaque lecture ou écriture, l'élément se déplace au début de la file d'attente et, en cas de débordement, l'élément de la fin est supprimé. Selon la documentation Android Developers (2026), LruCache dans Android utilise LinkedHashMap en mode access-order et fournit une complexité O(1) pour les opérations get et put.
Points Clés
LRU Cache (Least Recently Used Cache) est une structure de données de taille fixe qui stocke un nombre limité d'éléments et supprime automatiquement ceux qui ont été le moins fréquemment accédés. Lorsqu'une application demande un élément, celui-ci se déplace vers la partie « fraîche » du cache, tandis que les éléments inutilisés depuis longtemps se déplacent vers la fin et sont supprimés lorsque la limite est atteinte.
Le nom « Least Recently Used » décrit la politique d'éviction : l'élément qui n'a pas été utilisé depuis le plus longtemps parmi tous les éléments stockés est supprimé. Ceci repose sur l'hypothèse de localité de référence (locality of reference) — les données récemment demandées ont une forte probabilité d'être à nouveau nécessaires. C'est pourquoi LRU est considéré comme l'une des stratégies de mise en cache les plus efficaces pour la plupart des applications.
L'implémentation classique de LRU Cache nécessite deux structures de données : une table de hachage pour un accès O(1) à tout élément par clé et une liste doublement chaînée pour suivre l'ordre d'utilisation. La table de hachage stocke des références vers les nœuds de la liste, et la liste maintient l'ordre de l'élément le plus récent (tête) au plus ancien (queue).
L'opération get(key) vérifie si la clé existe dans la table de hachage. Si l'élément est trouvé, il se déplace en tête de liste (devient le plus récent) et sa valeur est retournée. S'il n'est pas trouvé, null est retourné ou une exception est levée. L'opération put(key, value) insère un nouvel élément : si la clé existe déjà, la valeur est mise à jour et l'élément se déplace en tête. Si le cache est plein, l'élément de queue est supprimé avant l'insertion. Toutes les opérations s'exécutent en temps constant O(1).
L'algorithme LRU Cache repose sur deux principes : comptage d'accès ordonné dans le temps et le mécanisme d'éviction par débordement. Chaque élément est stocké dans un nœud de la liste doublement chaînée, et les pointeurs vers ces nœuds sont conservés dans la table de hachage. À chaque accès, l'élément est détaché de sa position actuelle et inséré en tête de liste.
Lorsque la taille du cache atteint sa valeur maximale (maxSize) et qu'une demande d'insertion d'un nouvel élément arrive, l'algorithme supprime l'élément de queue de la liste doublement chaînée — c'est l'élément le moins récemment utilisé. Après la suppression, de l'espace est libéré pour le nouvel élément, qui est inséré en tête de liste. La table de hachage est mise à jour en conséquence : l'ancienne clé est supprimée, une nouvelle est ajoutée.
Une caractéristique de LRU est sa sensibilité aux schémas d'accès à répétition cyclique. Si l'application accède périodiquement à un ensemble de données plus grand que la taille du cache, LRU peut souffrir de thrashing — remplacement fréquent d'éléments où chaque nouvelle requête évince la précédente. Dans de tels scénarios, LFU (Least Frequently Used) ou les algorithmes adaptatifs peuvent être plus efficaces.
Choisir la taille du LRU Cache est un compromis entre la consommation mémoire et le hit-ratio (pourcentage d'accès réussis). Valeurs typiques pour les applications mobiles : 10–20% de la mémoire disponible pour le cache d'images et 50–200 entrées pour le cache de réponses réseau. Un hit-ratio de 80–95% est considéré comme bon, où le cache justifie les coûts mémoire. Pour la surveillance, les compteurs hitCount et missCount sont utilisés, disponibles dans l'implémentation LruCache dans Android.
L'implémentation canonique de LRU Cache utilise une combinaison d'une table de hachage et d'une liste doublement chaînée. La table de hachage fournit un accès O(1) à tout nœud par clé, tandis que la liste doublement chaînée permet de déplacer un nœud en tête et de le supprimer de la queue en O(1). Il est crucial que la liste soit doublement chaînée : cela permet de détacher un nœud du milieu de la liste sans itérer sur tous les éléments.
class LruCache<K, V>(
private val maxSize: Int
) {
private val map = mutableMapOf<K, Node<V>>()
private val head = Node<V>(null)
private val tail = Node<V>(null)
init {
head.next = tail
tail.prev = head
}
fun get(key: K): V? {
val node = map[key] ?: return null
removeNode(node)
addToHead(node)
return node.value
}
fun put(key: K, value: V) {
map[key]?.let { node ->
removeNode(node)
node.value = value
addToHead(node)
return
}
if (map.size >= maxSize) {
tail.prev?.let { toRemove ->
removeNode(toRemove)
removeKeyByValue(toRemove)
}
}
val newNode = Node(value)
addToHead(newNode)
}
}
Dans l'implémentation, chaque nœud (Node) stocke une valeur et des références vers les nœuds précédent et suivant. Les nœuds sentinelles head et tail simplifient les cas limites — pas besoin de vérifier null lors de l'insertion et de la suppression. La méthode get déplace le nœud trouvé en tête, et put supprime l'élément de queue en cas de débordement. Une méthode séparée removeKeyByValue trouve la clé dans la table de hachage par référence au nœud et la supprime.
Le SDK Android fournit une classe LruCache prête dans le paquet android.util, qui implémente l'algorithme LRU en utilisant LinkedHashMap en mode access-order. La classe est thread-safe, supporte le comptage hit/miss et fournit le callback entryRemoved pour le nettoyage des ressources lors de l'éviction d'un élément. La taille du cache est définie en unités arbitraires (octets, nombre d'éléments) — il suffit de surcharger la méthode sizeOf.
Les trois algorithmes — LRU, FIFO et LIFO — résolvent le même problème : limiter la consommation mémoire en évincant les éléments en cas de débordement. Cependant, ils utilisent des critères fondamentalement différents pour sélectionner la victime, ce qui détermine leur efficacité dans différents scénarios.
| Paramètre | LRU | FIFO | LIFO |
|---|---|---|---|
| Critère d'éviction | Le moins récemment utilisé | Premier ajouté | Dernier ajouté |
| Structure de données | HashMap + Liste doublement chaînée | File d'attente (Queue) | Pile (Stack) |
| Complexité get/put | O(1) | O(1) | O(1) |
| Résistance aux schémas | Élevée | Moyenne | Faible |
| Cas d'usage typique | Cache d'images et de données | Tampon de flux | Annulation (undo) |
FIFO évince l'élément le plus ancien par temps d'insertion, indépendamment de la fréquence d'accès. Cela peut être inefficace si un élément ancien est toujours pertinent. LRU évite cet inconvénient en tenant compte du schéma d'accès. LIFO évince l'élément le plus récemment ajouté — utile pour les scénarios d'annulation, mais inadapté à la mise en cache, car les nouvelles données sont souvent plus nécessaires que les anciennes. LRU est considéré comme l'équilibre optimal entre complexité d'implémentation et hit-ratio pour la plupart des applications.
Considérons l'utilisation de la classe intégrée LruCache du SDK Android pour mettre en cache les images téléchargées. L'exemple montre l'initialisation du cache à 1/8 de la mémoire disponible de l'application, ce qui est la recommandation standard de Google pour la mise en cache d'images.
import android.util.LruCache
class ImageCache(context: Context) {
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
private val cacheSize = maxMemory / 8
private val lruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.rowBytes * bitmap.height / 1024
}
}
fun getBitmap(key: String): Bitmap? {
return lruCache.get(key)
}
fun putBitmap(key: String, bitmap: Bitmap) {
lruCache.put(key, bitmap)
}
}
La méthode sizeOf retourne la taille de l'élément dans les mêmes unités que cacheSize. Ici, la taille du Bitmap en kilo-octets est utilisée (rowBytes × height / 1024). Lorsque la somme des sizeOf de tous les éléments dépasse cacheSize, LruCache évince automatiquement les Bitmaps les moins récemment utilisés. Le callback entryRemoved peut être utilisé pour appeler bitmap.recycle() — libérer la mémoire avant l'éviction.
iOS n'a pas de classe LRU Cache intégrée, mais il est facile de l'implémenter en utilisant NSCache (qui utilise une politique d'éviction similaire mais non documentée) ou via une implémentation personnalisée utilisant Dictionary + liste doublement chaînée, comme montré ci-dessous.
class LRUCache<Key: Hashable, Value> {
private let maxSize: Int
private var dict = [Key: Node<Value>]()
private var head: Node<Value>?
private var tail: Node<Value>?
init(maxSize: Int) {
self.maxSize = maxSize
}
func get(key: Key) -> Value? {
guard let node = dict[key] else { return nil }
moveToHead(node)
return node.value
}
func put(key: Key, value: Value) {
if let node = dict[key] {
node.value = value
moveToHead(node)
return
}
if dict.count >= maxSize {
tail.map { removeNode($0) }
}
let node = Node(value: value)
dict[key] = node
addToHead(node)
}
}
Dans cette implémentation Swift, Node est une classe interne avec les champs value, next et prev. La méthode moveToHead détache un nœud de sa position actuelle et l'insère en tête de liste. En cas de débordement, la queue — l'élément le moins récemment utilisé — est supprimée. Pour la production, il est recommandé d'ajouter la sécurité des threads via NSLock ou une file DispatchQueue.
Questions Fréquentes
HashMap n'a pas de mécanisme de limitation de taille — il croît indéfiniment jusqu'à épuisement de la mémoire. LRU Cache ajoute une politique d'éviction (suppression des éléments les moins récemment utilisés) lorsque la limite est atteinte, ce qui est nécessaire pour prévenir OutOfMemoryError dans les applications mobiles aux ressources limitées.
Google recommande d'allouer 1/8 de la mémoire disponible pour le cache d'images (Runtime.maxMemory() / 8). Pour les applications avec des graphismes lourds, jusqu'à 1/4 est acceptable. Considérez également le cache disque (DiskLruCache), qui peut stocker 2 à 5 fois plus de données grâce à un stockage plus lent mais moins cher.
LRU évince l'élément qui n'a pas été utilisé depuis le plus longtemps (par temps du dernier accès). LFU évince l'élément qui a été utilisé le moins fréquemment (par fréquence d'accès). LFU est meilleur pour les scénarios avec une fréquence d'accès inégale, mais est plus complexe à implémenter et consomme plus de mémoire pour stocker les compteurs.
NSCache ne documente pas sa politique d'éviction, mais en pratique utilise une approche hybride proche de LRU avec certains éléments de LFU. NSCache évince automatiquement les objets lorsque la mémoire est faible et supporte la priorisation basée sur le coût. Cependant, pour un comportement LRU garanti, une implémentation personnalisée est recommandée.
Thrashing est un état où le cache évince et charge constamment des éléments sans bénéfice réel. Cela se produit lorsque l'ensemble de données de travail de l'application est plus grand que la taille du cache et que l'accès aux données est cyclique. Les solutions incluent l'augmentation de la taille du cache, l'utilisation de LFU ou l'application de l'algorithme adaptatif ARC (Adaptive Replacement Cache).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi