Entrevistador: Como implementar um LruCache, qual é o princípio?

1. Introdução

LRU é o algoritmo menos usado recentemente, menos usado recentemente.

Uma vez, quando os principais porta-retratos em cache não eram populares. Existe uma tecnologia de armazenamento em cache de memória muito comum: SoftReference e  WeakReference(referência suave e referência fraca). Mas na era do Android 2.3 (nível 9), o mecanismo de coleta de lixo é mais inclinado a reciclar  SoftReference ou  WeakReferenceobjetos. Mais tarde, cheguei ao Android3.0 e a imagem estava em cache no conteúdo, porque não sabia quando liberar a memória, não havia estratégia e não havia utilidade para uma ocasião previsível de liberá-la. Isso causou um estouro de memória.

2. Como usar

Ele pode ser usado como um mapa, mas implementa apenas a estratégia de cache LRU.

Lembre-se de alguns pontos ao usar:

  • 1.(必填)Você precisa fornecer uma capacidade de cache como parâmetro de construção.
  • 2.(必填) Substituir sizeOf do método, projete uma parte dos dados para calcular a capacidade. Se você não substituir, não poderá prever a capacidade dos dados e não garantirá que a capacidade do cache seja limitada à capacidade máxima.
  • 3.(选填) Sobrescrevendo o  entryRemoved método, você pode conhecer os dados quando o cache menos utilizado é limpo (despejado, chave, oldValue, newVaule).
  • 4.(记住)O LruCache é seguro para threads, e os get, put, remove internos, incluindo trimToSize, são todos seguros (porque estão bloqueados).
  • 5.(选填) Há também o create método de substituição  .

Geralmente, 1, 2, 3, 4 são suficientes, 5 podem ser ignorados.

A seguir, é apresentado um Bitmap caso em entryRemoved que o LruCache implementa um pequeno  cache.A  lógica personalizada pode ser ignorada.Se você quiser vê-lo, pode ir à minha demonstração demo para ver a 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álise do código fonte

3.1 Análise de princípios do LruCache

LruCache É  LinkedHashMap um recurso (  accessOrder=true baseado na sequência de acesso) mais  LinkedHashMap uma estratégia de armazenamento em cache para operações de bloqueio de dados.

O cache de dados do LruCache está na memória.

  • 1. Primeiro defina os LinkedHashMap parâmetros da estrutura  interna  accessOrder=truepara obter a classificação dos dados de acordo com a ordem de acesso.

  • 2. Então, toda vez que  LruCache.get(K key)`` 方法里都会调用LinkedHashMap.get (chave do objeto) `.

  • 3. accessOrder=true Após as configurações acima  , ele LinkedHashMap.get(Object key) será executado  sempre  LinkedHashMap.makeTail(LinkedEntry<K, V> e).

  • 4. LinkedHashMap é uma lista vinculada duplamente circular e, toda vez que os dados de LruCache.get-> LinkedHashMap.get são colocados no final.

  • 5. Sob a implementação de put e trimToSize, se o volume de dados for removido, os primeiros dados serão removidos primeiro (porque os dados acessados ​​mais recentemente estão no final).

A análise específica é: 3.2, 3.3, 3.4, 3.5.

3.2 A única maneira de construir o 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);
}

O primeiro parâmetro é  initialCapacity usado para inicializar o  LinkedHashMap tamanho.

Primeiro, introduza brevemente o segundo parâmetro.O  loadFactorparâmetro de construção real no HashMap envolve o problema de expansão.Por exemplo, a capacidade máxima do HashMap é 100. Se for definido aqui 0.75f, a capacidade será expandida quando atingir 75 de capacidade.

Principalmente é o terceiro parâmetro  accessOrder=true , portanto, a LinkedHashMap classificação dos  dados será baseada na ordem de acesso aos dados, realizando assim o  LruCache principal princípio de funcionamento.

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;
    }
}

O método get acima parece não ver onde há uma estratégia de cache que implementa o LRU. Principalmente em  mapValue = map.get(key);, LinkedHashMap o  get método chamado  , mais a configuração padrão LinkedHashMap na  estrutura LruCache  accessOrder=true.

3.4 LinkedHashMap.get (chave do 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;
}

Na verdade, basta olhar atentamente para  if (accessOrder) a lógica: se  accessOrder=true ela for executada N vezes cada vez  makeTail(LinkedEntry<K, V> e) .

Próximo visual:

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

// Revincular e como cauda

LinkedHashMap é uma lista vinculada duplamente circular e, em seguida, os dados de LruCache.get-> LinkedHashMap.get são colocados no final.

O acima é o principal princípio de trabalho do LruCache.

A seguir, apresentamos a estratégia de capacidade excedente do LruCache.

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

Lembre-se de alguns pontos:

  • 1.``put No início, o valor é realmente inserido  LinkedHashMap , independentemente de exceder a capacidade do buffer que você definiu.
  • 2.De acordo com o  safeSizeOf método, calcule a capacidade dos dados adicionados desta vez e adicione-os a  sizeeles.
  • 3. Quando se trata de safeSizeOf, diz-se que sizeOf (chave K, valor V) calculará o tamanho dos dados adicionados.
  • 4.Até a conclusão da colocação,  trimToSize é julgado  size se é maior que  maxSize e, em seguida, é executada a remoção dos dados que raramente foram acessados ​​recentemente.
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 descrição: Julgará se size é maior do que  antes  maxSize . Se sim, não faça nada depois de pular. Caso contrário, isso prova que a capacidade foi excedida. É makeTail sabido pela  figura que os dados acessados ​​com mais frequência estão no final. Obtenha um conjunto que armazene a chave e exclua-a desde o início, exclua uma para determinar se ela excede, até que não ocorra excesso.

Último olhar:

4.8 Substituindo o efeito de entryRemoved

entryRemovedLruCacheCena sendo chamada:

  • 1. (put)  put é chamado quando a chave de evicted=falseconflito , key= esta  put é  key, oldValue= conflitos são cobertos  value, newValue= esta  put é  value.

  • 2. (trimToSizetrimToSize , ele será chamado apenas uma vez, ou seja, os dados de menor acesso excluídos da última vez que forem recuperados. evicted=true, Key = estava última chave suprimido, oldValue = foi o último removido  value, newValue=null(a ausência de conflito, mas remove).

  • 3. (remove) Ao remover, a chave correspondente existe e é chamada após ser excluída com sucesso. evicted=false, Key = chave desta opção, oldValue= valor desta exclusão newValue=null(sem conflito desta vez, basta remover).

  • 4. (Na segunda metade do get, o cenário de processamento é perdido depois que a consulta é perdida, mas é recomendável ignorá-lo) Quando get, se a criação personalizada não for implementada normalmente, o método get passará apenas para a metade no código. Se você implementar o create(K key) método personalizado  , E será chamado quando o valor criado por você for colocado no LruCache quando houver um conflito importante. Of  key-value.

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

Como no caso de uma grande quantidade de dados, o HashMap pode causar perda de dados, resultando na primeira metade da pesquisa não encontrada.Quando você o  create(key)coloca, o encontra novamente (有冲突). Em seguida, devolva rapidamente o valor original; nesse momento, você criará uma viagem em vão, não fará nada e continuará entryRemoved.

Em resumo, é como o comentário:

/**
 * 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) {
}

I pode se referir a demo no  entryRemoved .

4.9 Bloqueio de sincronização local do LruCache

Em  getputtrimToSizeremove quatro métodos nos  entryRemoved métodos não são em blocos de sincronização em. Como os  entryRemoved parâmetros de retorno de chamada são todos parâmetros de domínio do método, eles não são seguros para threads.

Pilha de método local e contador de programa são áreas de dados isoladas por encadeamento

4. Resuma os pontos importantes do LruCache

  • 1.LruCache Através do  LinkedHashMap terceiro construtor argumento  accessOrder=truepercebeu  LinkedHashMap com base na ordem de acesso (dados recentemente acessados irá listar a cauda), quando a capacidade do estouro, a cabeça da lista ligada dados remoção de dados de encomenda. Assim, o mecanismo de cache de dados LRU é implementado.

  • 2. LruCache internamente get, put, removeincluindo  trimToSize todos os seguros (porque todos trancados).

  1. LruCache Ele não libera a memória em si,  LinkedHashMapos dados são removidos, se os dados ainda forem referenciados em outros lugares, ainda há um problema de vazamento, é necessário liberar manualmente a memória.
  • 4. O entryRemovedmétodo de substituição  pode saber se há um conflito na remoção dos dados do LruCache ou você pode liberar manualmente os recursos.

  • 5 maxSize e  sizeOf(K key, V value) método de substituição está intimamente relacionado com, o aparelho tem de ser o mesmo. (Tais como  maxSize Shi 7MB, o costume de  sizeOf calcular o tamanho de cada dados deve ser capaz de calcular o tempo associado com unidades MB)

Compartilhe especialmente a análise das perguntas reais da entrevista de batimento de bytes, adicione VX: q1607947758 para obtê-lo gratuitamente

Publicado 488 artigos originais · elogiou 85 · 230.000 visualizações +

Acho que você gosta

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