As chamadas de API do Android geralmente envolvem latência e computação significativas por invocação. Portanto, o cache do lado do cliente é uma consideração importante ao projetar APIs úteis, corretas e eficientes.
Motivação
As APIs expostas aos desenvolvedores de apps no SDK do Android geralmente são implementadas como código do cliente no framework do Android que faz uma chamada de IPC do binder para um sistema serviço em um processo de plataforma, cuja função é realizar alguma computação e retornar um resultado ao cliente. A latência dessa operação é normalmente dominada por três fatores:
- Sobrecarga de IPC: uma chamada básica de IPC geralmente tem uma latência 10.000 vezes maior que uma chamada de método básica no processo.
 - Disputa do lado do servidor: o trabalho feito no serviço do sistema em resposta à solicitação do cliente pode não começar imediatamente, por exemplo, se uma linha de execução do servidor estiver ocupada processando outras solicitações que chegaram antes.
 - Computação do lado do servidor: o trabalho em si para processar a solicitação no servidor pode exigir um esforço significativo.
 
É possível eliminar todos esses três fatores de latência implementando um cache no lado do cliente, desde que ele seja:
- Correto: o cache do lado do cliente nunca retorna resultados diferentes do que o servidor teria retornado.
 - Eficaz: as solicitações do cliente geralmente são veiculadas do cache. Por exemplo, o cache tem uma alta taxa de acertos.
 - Eficiente: o cache do lado do cliente usa de maneira eficiente os recursos do lado do cliente, como representar dados armazenados em cache de forma compacta e não armazenar muitos resultados em cache ou dados desatualizados na memória do cliente.
 
Considere armazenar em cache os resultados do servidor no cliente
Se os clientes fizerem a mesma solicitação várias vezes e o valor retornado não mudar com o tempo, implemente um cache na biblioteca de cliente com chave pelos parâmetros da solicitação.
Use IpcDataCache na sua implementação:
public class BirthdayManager {
    private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
            new IpcDataCache.QueryHandler<User, Birthday>() {
                @Override
                public Birthday apply(User user) {
                    return mService.getBirthday(user);
                }
            };
    private static final int BDAY_CACHE_MAX = 8;  // Maximum birthdays to cache
    private static final String BDAY_API = "getUserBirthday";
    private final IpcDataCache<User, Birthday> mCache
            new IpcDataCache<User, Birthday>(
                BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API,  BDAY_API, mBirthdayQuery);
    /** @hide **/
    @VisibleForTesting
    public static void clearCache() {
        IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
    }
    public Birthday getBirthday(User user) {
        return mCache.query(user);
    }
}
Para um exemplo completo, consulte android.app.admin.DevicePolicyManager.
IpcDataCache está disponível para todo o código do sistema, incluindo módulos principais.
Há também PropertyInvalidatedCache, que é quase idêntico, mas só é visível para o framework. Prefira IpcDataCache sempre que possível.
Invalidar caches em mudanças do lado do servidor
Se o valor retornado do servidor puder mudar com o tempo, implemente um callback para observar as mudanças e registre um callback para invalidar o cache do lado do cliente de acordo com a situação.
Invalidar caches entre casos de teste de unidade
Em um pacote de testes de unidade, você pode testar o código do cliente em relação a um double de teste em vez do servidor real. Se for o caso, limpe todos os caches do lado do cliente entre os casos de teste. Isso mantém os casos de teste mutuamente herméticos e evita que um interfira em outro.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
Ao escrever testes do CTS que exercem um cliente de API que usa o cache internamente, o cache é um detalhe de implementação que não é exposto ao autor da API. Portanto, os testes do CTS não exigem conhecimento especial sobre o cache usado no código do cliente.
Estudar ocorrências e ausências no cache
O IpcDataCache e o PropertyInvalidatedCache podem imprimir estatísticas em tempo real:
adb shell dumpsys cacheinfo
  ...
  Cache Name: cache_key.is_compat_change_enabled
    Property: cache_key.is_compat_change_enabled
    Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
    Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
    Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
    Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
    Enabled: true
  ...
Campos
Acessos:
- Definição: o número de vezes que um dado solicitado foi encontrado com sucesso no cache.
 - Significância: indica uma recuperação eficiente e rápida de dados, reduzindo a recuperação desnecessária.
 - Contagens mais altas geralmente são melhores.
 
Limpa:
- Definição: o número de vezes que o cache foi limpo devido à invalidação.
 - Motivos para a limpeza:
- Invalidação: dados desatualizados do servidor.
 - Gerenciamento de espaço: libera espaço para novos dados quando o cache está cheio.
 
 - Contagens altas podem indicar dados que mudam com frequência e possível ineficiência.
 
Erros:
- Definição: o número de vezes que o cache não forneceu os dados solicitados.
 - Causas:
- Armazenamento em cache ineficiente: cache muito pequeno ou não armazena os dados certos.
 - Dados que mudam com frequência.
 - Solicitações iniciais.
 
 - Contagens altas sugerem possíveis problemas de cache.
 
Pulos:
- Definição: instâncias em que o cache não foi usado, mesmo que pudesse ter sido.
 - Motivos para pular:
- Corking: específico para atualizações do gerenciador de pacotes do Android, desativa deliberadamente o armazenamento em cache devido a um alto volume de chamadas durante a inicialização.
 - Não definido: o cache existe, mas não foi inicializado. O nonce não foi definido, o que significa que o cache nunca foi invalidado.
 - Ignorar: decisão intencional de pular o cache.
 
 - Contagens altas indicam possíveis ineficiências no uso do cache.
 
Invalida:
- Definição: o processo de marcar dados em cache como desatualizados ou obsoletos.
 - Significância: fornece um indicador de que o sistema trabalha com os dados mais atualizados, evitando erros e inconsistências.
 - Normalmente acionado pelo servidor proprietário dos dados.
 
Tamanho atual:
- Definição: a quantidade atual de elementos no cache.
 - Significância: indica a utilização de recursos do cache e o possível impacto no desempenho do sistema.
 - Valores mais altos geralmente significam que mais memória é usada pelo cache.
 
Tamanho máximo:
- Definição: a quantidade máxima de espaço alocado para o cache.
 - Significância: determina a capacidade do cache e a capacidade de armazenar dados.
 - Definir um tamanho máximo adequado ajuda a equilibrar a eficácia do cache com o uso da memória. Quando o tamanho máximo é atingido, um novo elemento é adicionado removendo o elemento usado menos recentemente, o que pode indicar ineficiência.
 
Marca d'água alta:
- Definição: o tamanho máximo atingido pelo cache desde a criação.
 - Significância: fornece insights sobre o uso máximo do cache e possível pressão na memória.
 - Monitorar o nível máximo pode ajudar a identificar possíveis gargalos ou áreas para otimização.
 
Transbordamentos:
- Definição: o número de vezes que o cache excedeu o tamanho máximo e precisou remover dados para abrir espaço para novas entradas.
 - Significância: indica pressão de cache e possível degradação de performance devido à remoção de dados.
 - Contagens altas de estouro sugerem que o tamanho do cache pode precisar ser ajustado ou que a estratégia de cache precisa ser reavaliada.
 
As mesmas estatísticas também podem ser encontradas em um relatório de bugs.
Ajustar o tamanho do cache
Os caches têm um tamanho máximo. Quando o tamanho máximo do cache é excedido, as entradas são removidas na ordem LRU.
- O armazenamento em cache de poucas entradas pode afetar negativamente a taxa de ocorrência em cache.
 - O armazenamento em cache de muitas entradas aumenta o uso da memória do cache.
 
Encontre o equilíbrio certo para seu caso de uso.
Eliminar chamadas de cliente redundantes
Os clientes podem fazer a mesma consulta ao servidor várias vezes em um curto período:
public void executeAll(List<Operation> operations) throws SecurityException {
    for (Operation op : operations) {
        for (Permission permission : op.requiredPermissions()) {
            if (!permissionChecker.checkPermission(permission, ...)) {
                throw new SecurityException("Missing permission " + permission);
            }
        }
        op.execute();
  }
}
Considere reutilizar os resultados de chamadas anteriores:
public void executeAll(List<Operation> operations) throws SecurityException {
    Set<Permission> permissionsChecked = new HashSet<>();
    for (Operation op : operations) {
        for (Permission permission : op.requiredPermissions()) {
            if (!permissionsChecked.add(permission)) {
                if (!permissionChecker.checkPermission(permission, ...)) {
                    throw new SecurityException(
                            "Missing permission " + permission);
                }
            }
        }
        op.execute();
  }
}
Considere a memoização do lado do cliente de respostas recentes do servidor
Os apps clientes podem consultar a API a uma taxa mais rápida do que o servidor dela pode produzir respostas significativamente novas. Nesse caso, uma abordagem eficaz é memoizar a última resposta do servidor vista no lado do cliente junto com um carimbo de data/hora e retornar o resultado memoizado sem consultar o servidor se ele for recente o suficiente. O autor do cliente da API pode determinar a duração da memoização.
Por exemplo, um app pode mostrar estatísticas de tráfego de rede ao usuário consultando as estatísticas em todos os frames renderizados:
@UiThread
private void setStats() {
    mobileRxBytesTextView.setText(
        Long.toString(TrafficStats.getMobileRxBytes()));
    mobileRxPacketsTextView.setText(
        Long.toString(TrafficStats.getMobileRxPackages()));
    mobileTxBytesTextView.setText(
        Long.toString(TrafficStats.getMobileTxBytes()));
    mobileTxPacketsTextView.setText(
        Long.toString(TrafficStats.getMobileTxPackages()));
}
O app pode renderizar frames a 60 Hz. Mas, hipoteticamente, o código do cliente em
TrafficStats pode optar por consultar o servidor para estatísticas no máximo uma vez por segundo
e, se consultado em um segundo de uma consulta anterior, retornar o último valor visto.
Isso é permitido porque a documentação da API não fornece nenhum contrato sobre a atualização dos resultados retornados.
participant App code as app
participant Client library as clib
participant Server as server
app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1
app->clib: request @ T=200ms
clib->app: response 1
app->clib: request @ T=300ms
clib->app: response 1
app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2
Considere a geração de código do lado do cliente em vez de consultas do servidor
Se os resultados da consulta forem conhecidos pelo servidor no momento da criação, considere se eles também são conhecidos pelo cliente no momento da criação e se a API pode ser implementada totalmente no lado do cliente.
Considere o seguinte código de app que verifica se o dispositivo é um relógio (ou seja, se ele está executando o Wear OS):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Essa propriedade do dispositivo é conhecida no momento da criação, especificamente quando
o framework foi criado para a imagem de inicialização do dispositivo. O código do lado do cliente
para hasSystemFeature pode retornar um resultado conhecido imediatamente, em vez de
consultar o serviço do sistema PackageManager remoto.
Eliminar a duplicação de callbacks do servidor no cliente
Por fim, o cliente da API pode registrar callbacks com o servidor da API para receber notificações de eventos.
É comum que os apps registrem vários callbacks para as mesmas informações subjacentes. Em vez de o servidor notificar o cliente uma vez por callback registrado usando IPC, a biblioteca de cliente deve ter um callback registrado usando IPC com o servidor e, em seguida, notificar cada callback registrado no app.
digraph d_front_back {
  rankdir=RL;
  node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
  server->clib
  clib->c1;
  clib->c2;
  clib->c3;
  subgraph cluster_client {
    graph [style="dashed", label="Client app process"];
    c1 [label="my.app.FirstCallback" color="#4285F4"];
    c2 [label="my.app.SecondCallback" color="#4285F4"];
    c3 [label="my.app.ThirdCallback" color="#4285F4"];
    clib [label="android.app.FooManager" color="#F4B400"];
  }
  subgraph cluster_server {
    graph [style="dashed", label="Server process"];
    server [label="com.android.server.FooManagerService" color="#0F9D58"];
  }
}