Entrevistador: ¿Cómo implementar un LruCache, cuál es el principio?

1. Introducción

LRU es el algoritmo menos utilizado recientemente menos utilizado recientemente.

Una vez, cuando los principales marcos de fotos en caché no eran populares. Existe una tecnología de memoria caché muy común: SoftReference y  WeakReference(referencia suave y referencia débil). Pero en la era de Android 2.3 (Nivel 9), el mecanismo de recolección de basura está más inclinado a reciclar  SoftReference u  WeakReferenceobjetos. Más tarde, llegué a Android3.0, y la imagen estaba almacenada en caché en el contenido, porque no sabía cuándo liberar la memoria, no había estrategia, y no había uso en una ocasión previsible para liberarla. Esto causó un desbordamiento de memoria.

2. Cómo usar

Se puede usar como un mapa, pero solo implementa la estrategia de caché LRU.

Solo recuerda algunos puntos cuando uses:

  • 1.(必填)Debe proporcionar una capacidad de caché como parámetro de construcción.
  • 2.(必填) Tamaño de sobrescritura Del método, diseñe a medida una pieza de datos para calcular la capacidad. Si no sobrescribe, no puede predecir la capacidad de datos y no puede garantizar que la capacidad de caché se limite a la capacidad máxima.
  • 3.(选填)entryRemoved Método de  sobrescritura  , puede conocer los datos cuando se borra el caché menos utilizado (desalojado, clave, oldValue, newVaule).
  • 4.(记住)LruCache es seguro para subprocesos, y los métodos internos get, put, remove, incluyendo trimToSize, son todos seguros (porque están bloqueados).
  • 5.(选填) También existe el create método de sobrescritura  .

Generalmente, 1, 2, 3, 4 son suficientes, 5 pueden ignorarse.

El siguiente es un Bitmap caso entryRemoved en el que LruCache implementa un  pequeño caché  . La lógica personalizada puede ser ignorada. Si desea verlo, puede ir a mi demo de demostración para ver la entryRemoved lógica personalizada  .

private static final float ONE_MIB = 1024 * 1024;
// 7MB
private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
private LruCache<String, Bitmap> bitmapCache;
this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
    }

    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        ...
    }
};

3. Análisis del código fuente

3.1 Análisis principal de LruCache

LruCache Es  LinkedHashMap una característica (  accessOrder=true basada en la secuencia de acceso) más  LinkedHashMap una estrategia de almacenamiento en caché para bloquear operaciones de datos.

El caché de datos de LruCache está en la memoria.

  • 1. Primero configure los LinkedHashMap parámetros de la estructura  interna  accessOrder=truepara lograr la clasificación de datos de acuerdo con el orden de acceso.

  • 2. Luego, cada vez que  LruCache.get(K key)`` 方法里都会调用LinkedHashMap.get (clave de objeto) `.

  • 3. accessOrder=true Después de la configuración anterior  , se LinkedHashMap.get(Object key) realizará  cada vez  LinkedHashMap.makeTail(LinkedEntry<K, V> e).

  • 4. LinkedHashMap es una lista enlazada doblemente circular, y cada vez que los datos de LruCache.get-> LinkedHashMap.get se ponen al final.

  • 5. Bajo la implementación de put y trimToSize, si se elimina el volumen de datos, los primeros datos se eliminarán primero (porque los datos a los que se accedió más recientemente están al final).

El análisis específico es: 3.2, 3.3, 3.4, 3.5.

3.2 La única forma de construir LruCache
/**
 * LruCache的构造方法:需要传入最大缓存个数
 */
public LruCache(int maxSize) {

    ...

    this.maxSize = maxSize;
    /*
     * 初始化LinkedHashMap
     * 第一个参数:initialCapacity,初始大小
     * 第二个参数:loadFactor,负载因子=0.75f
     * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
     */
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

El primer parámetro se  initialCapacity usa para inicializar el  LinkedHashMap tamaño.

Primero, introduzca brevemente el segundo parámetro  loadFactor. El parámetro de construcción real en HashMap implica el problema de expansión. Por ejemplo, la capacidad máxima de HashMap es 100. Si se establece aquí 0.75f, la capacidad se expandirá cuando alcance la capacidad de 75.

Principalmente es el tercer parámetro  accessOrder=true , por lo que la LinkedHashMap ordenación de los  datos se basará en el orden de acceso a los datos, y así se realizará el  LruCache principio de funcionamiento central.

3.3 LruCache.get (tecla K)
/**
 * 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
 * 如果值返回了,那么它将被移动到双向循环链表的的尾部。
 * 如果如果没有缓存的值,则返回 null。
 */
public final V get(K key) {

    ...

    V mapValue;
    synchronized (this) {
        // 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
        mapValue = map.get(key);
        // 计算 命中次数
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 计算 丢失次数
        missCount++;
    }

    /*
     * 官方解释:
     * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
     * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    /***************************
     * 不覆写create方法走不到下面 *
     ***************************/

    /*
     * 正常情况走不到这里
     * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
     * 因为默认的 create(K key) 逻辑为null
     */
    synchronized (this) {
        // 记录 create 的次数
        createCount++;
        // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
        mapValue = map.put(key, createdValue);

        // 如果之前存在相同key的value,即有冲突。
        if (mapValue != null) {
            /*
             * 有冲突
             * 所以 撤销 刚才的 操作
             * 将 之前相同key 的值 重新放回去
             */
            map.put(key, mapValue);
        } else {
            // 拿到键值对,计算出在容量中的相对长度,然后加上
            size += safeSizeOf(key, createdValue);
        }
    }

    // 如果上面 判断出了 将要放入的值发生冲突
    if (mapValue != null) {
        /*
         * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
         * 告诉 自定义 的 entryRemoved 方法
         */
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 上面 进行了 size += 操作 所以这里要重整长度
        trimToSize(maxSize);
        return createdValue;
    }
}

El método get anterior no parece ver dónde hay una estrategia de caché que implementa LRU. Principalmente en  mapValue = map.get(key);, LinkedHashMap el  get método llamado  , más la configuración predeterminada LinkedHashMap en la  estructura LruCache  accessOrder=true.

3.4 LinkedHashMap.get (clave de objeto)
/**
 * Returns the value of the mapping with the specified key.
 *
 * @param key
 *            the key.
 * @return the value of the mapping with the specified key, or {@code null}
 *         if no mapping for the specified key is found.
 */
@Override public V get(Object key) {
    /*
     * This method is overridden to eliminate the need for a polymorphic
     * invocation in superclass at the expense of code duplication.
     */
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null)
            return null;
        if (accessOrder)
            makeTail((LinkedEntry<K, V>) e);
        return e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
         e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

De hecho, solo mire if (accessOrder) la lógica cuidadosamente  : si  accessOrder=true se ejecuta N veces cada vez  makeTail(LinkedEntry<K, V> e) .

Próximo vistazo:

3.5 LinkedHashMap.makeTail (LinkedEntry <K, V> e)
/**
 * Relinks the given entry to the tail of the list. Under access ordering,
 * this method is invoked whenever the value of a  pre-existing entry is
 * read by Map.get or modified by Map.put.
 */
private void makeTail(LinkedEntry<K, V> e) {
    // Unlink e
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;

    // Relink e as tail
    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    modCount++;
}

// Desvincular e

// Vuelva a vincular e como cola

LinkedHashMap es una lista enlazada doblemente circular, y luego los datos de LruCache.get-> LinkedHashMap.get se ponen al final.

Lo anterior es el principio de funcionamiento central de LruCache.

A continuación, presentamos la estrategia de desbordamiento de capacidad de LruCache.

4.6 LruCache.put (tecla K, valor V)
public final V put(K key, V value) {
    ...
    synchronized (this) {
        ...
        // 拿到键值对,计算出在容量中的相对长度,然后加上
        size += safeSizeOf(key, value);
        ...
    }
	...
    trimToSize(maxSize);
    return previous;
}

Recuerda algunos puntos:

  • 1.``put Al principio, el valor se pone efectivamente  LinkedHashMap , sin importar si excede la capacidad de búfer establecida.
  • 2.Luego, según el  safeSizeOf método, calcule la capacidad de los datos agregados esta vez y agréguelos  size.
  • 3. Cuando se trata de safeSizeOf, se dice que sizeOf (clave K, valor V) calculará el tamaño de los datos agregados.
  • 4.Hasta que finalice la colocación, se  trimToSize juzga  size si es mayor  maxSize y luego se realiza la eliminación de los datos a los que rara vez se ha accedido recientemente.
4.7 LruCache.trimToSize (int maxSize)
public void trimToSize(int maxSize) {
    /*
     * 这是一个死循环,
     * 1.只有 扩容 的情况下能立即跳出
     * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
     */
    while (true) {
        K key;
        V value;
        synchronized (this) {
            // 在重新调整容量大小前,本身容量就为空的话,会出异常的。
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(
                        getClass().getName() + ".sizeOf() is reporting inconsistent results!");
            }
            // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
            if (size <= maxSize || map.isEmpty()) {
                break;
            }

            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 拿到键值对,计算出在容量中的相对长度,然后减去。
            size -= safeSizeOf(key, value);
            // 添加一次收回次数
            evictionCount++;
        }
        /*
         * 将最后一次删除的最少访问数据回调出去
         */
        entryRemoved(true, key, value, null);
    }
}

Breve descripción: juzgará si size es mayor que  antes  maxSize . En caso afirmativo, no haga nada después de saltar. Si no, demuestra que la capacidad se ha desbordado. De la makeTail figura se  sabe que los datos a los que se accede con mayor frecuencia están al final. Obtenga un conjunto que almacene la clave y luego elimínelo desde el principio, elimine uno para determinar si se desborda, hasta que no haya desbordamiento.

Última mirada:

4.8 Anulación del efecto de entrada eliminado

entryRemovedLruCacheEscena que se llama:

  • 1. (put)  put se llama cuando la llave evicted=falsede conflictos , key= esto  put es  key, oldValue= conflictos están cubiertos  value, newValue= esto  put es  value.

  • 2. (trimToSizetrimToSize , solo se llamará una vez, es decir, se recuperarán los datos de menor acceso que se eliminaron la última vez. evicted=true, Clave = clave eliminada por última vez, valor antiguo = última eliminación  value, newValue=null(no hay conflicto esta vez, solo elimine).

  • 3. (remove) Al eliminar, la clave correspondiente existe y se llama después de que se elimina con éxito. evicted=false, Clave = clave de este put, oldValue= valor de esta eliminación, newValue=null(no hay conflicto esta vez, solo elimine).

  • 4. (En la segunda mitad de get, el escenario de procesamiento se pierde después de que se pierde la consulta, pero se recomienda ignorarlo) Cuando get, si la creación personalizada no se implementa normalmente, el método get solo irá a la mitad en el código. Si implementa el create(K key) método personalizado  , Y se llamará cuando el valor creado por usted se ponga en LruCache cuando haya un conflicto clave. una  key-value.

El cuarto punto explicarlo: <1>.第四点是这样的,先 get(key),然后没拿到,丢失。<2>.如果你提供了 自定义的 create(key) 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。<3>.此时,会将那个冲突的值再让回去覆盖,此时调用上述4.的 entryRemoved.

Debido a que la cantidad de datos en el caso HashMap, toman los datos pueden perderse, lo que resulta en la primera mitad de averiguar, su encargo  create(key)fue encontrado en cuando se enteraron (有冲突). Luego, apresuradamente, devuelva el valor original, en este momento creará un viaje en vano, no hará nada y volverá a ir entryRemoved.

En resumen, es como el comentario:

/**
 * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
 * 或者替换条目值时put调用,默认实现什么都没做。
 * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
 * 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove)  evicted=false:put冲突后 或 get里成功create后
 * 导致
 * 4.newValue!=null,那么则被put()或get()调用。
 */
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}

Puedo hacer referencia a demo la  entryRemoved .

4.9 Bloqueo de sincronización local de LruCache

En  getputtrimToSizeremove cuatro métodos en los  entryRemoved métodos no son en bloques de sincronización en. Como los  entryRemoved parámetros de devolución de llamada son todos parámetros de dominio de método, no son seguros para subprocesos.

La pila de métodos locales y el contador de programas son áreas de datos aisladas de hilos

4. Resuma los puntos importantes de LruCache

  • 1.LruCache A través del  LinkedHashMap tercer constructor sin argumentos  accessOrder=truese dio cuenta  LinkedHashMap basa en el orden de acceso (los datos se ha accedido recientemente, aparecerá una lista de la cola), cuando la capacidad de la abundancia, la cabeza de la lista enlazada de pedidos de datos de eliminación de datos. Por lo tanto, se implementa el mecanismo de caché de datos LRU.

  • 2. LruCache internamente get, put, removeque incluye  trimToSize todos los seguros (porque todo cerrado).

  1. LruCache No libera la memoria en sí,  LinkedHashMaplos datos se eliminan, si los datos aún se mencionan en otro lugar, todavía hay un problema de fuga, debe liberar manualmente la memoria.
  • 4. El entryRemovedmétodo de sobrescritura  puede saber si existe un conflicto en la eliminación de datos de LruCache, o puede liberar manualmente los recursos.

  • 5 maxSize y  sizeOf(K key, V value) método de reemplazo está estrechamente relacionada con, la unidad debe ser el mismo. (Tales como  maxSize Shi 7MB, la costumbre de  sizeOf calcular el tamaño de cada uno de datos debe ser capaz de calcular el tiempo asociado con unidades de MB)

Comparta especialmente el análisis de las preguntas reales de la entrevista de byte beating, agregue VX: q1607947758 para obtenerlo gratis

Publicado 488 artículos originales · elogiado 85 · 230,000 vistas +

Supongo que te gusta

Origin blog.csdn.net/Coo123_/article/details/105143502
Recomendado
Clasificación