Por lo general, las llamadas a la API de Android implican una latencia y un procesamiento significativos por invocación. Por lo tanto, el almacenamiento en caché del cliente es un aspecto importante que se debe tener en cuenta al diseñar APIs útiles, correctas y con buen rendimiento.
Motivación
Las APIs expuestas a los desarrolladores de apps en el SDK de Android suelen implementarse como código de cliente en el framework de Android que realiza una llamada a IPC de Binder a un servicio del sistema en un proceso de la plataforma, cuyo trabajo es realizar algún cálculo y devolver un resultado al cliente. Por lo general, la latencia de esta operación se ve afectada principalmente por tres factores:
- Sobrecarga de IPC: Una llamada básica de IPC suele tener una latencia 10,000 veces mayor que una llamada básica a un método en el proceso.
 - Contención del servidor: El trabajo realizado en el servicio del sistema en respuesta a la solicitud del cliente puede no comenzar de inmediato, por ejemplo, si un subproceso del servidor está ocupado controlando otras solicitudes que llegaron antes.
 - Procesamiento del servidor: El trabajo en sí para controlar la solicitud en el servidor puede requerir un trabajo significativo.
 
Puedes eliminar estos tres factores de latencia implementando una caché del lado del cliente, siempre que la caché cumpla con los siguientes requisitos:
- Correcto: La caché del cliente nunca devuelve resultados que serían diferentes de los que habría devuelto el servidor.
 - Eficaz: Las solicitudes del cliente a menudo se publican desde la caché, por ejemplo, la caché tiene una alta tasa de aciertos.
 - Eficiente: La caché del cliente usa de manera eficiente los recursos del cliente, por ejemplo, representando los datos almacenados en caché de forma compacta y no almacenando demasiados resultados almacenados en caché ni datos obsoletos en la memoria del cliente.
 
Considera almacenar en caché los resultados del servidor en el cliente
Si los clientes suelen hacer la misma solicitud varias veces y el valor que se devuelve no cambia con el tiempo, debes implementar una caché en la biblioteca cliente con los parámetros de la solicitud como clave.
Considera usar IpcDataCache en tu implementación:
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 ver un ejemplo completo, consulta android.app.admin.DevicePolicyManager.
IpcDataCache está disponible para todo el código del sistema, incluidos los módulos de la línea principal.
También existe PropertyInvalidatedCache, que es casi idéntico, pero solo es visible para el framework. Prefiere IpcDataCache cuando sea posible.
Invalida las cachés en los cambios del servidor
Si el valor que devuelve el servidor puede cambiar con el tiempo, implementa una devolución de llamada para observar los cambios y registra una devolución de llamada para que puedas invalidar la caché del cliente según corresponda.
Invalida las cachés entre los casos de prueba de unidades
En un conjunto de pruebas de unidades, puedes probar el código del cliente con un doble de prueba en lugar del servidor real. Si es así, asegúrate de borrar cualquier caché del cliente entre los casos de prueba. Esto es para mantener los casos de prueba mutuamente herméticos y evitar que un caso de prueba interfiera con otro.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
Cuando se escriben pruebas del CTS que ejercitan un cliente de API que usa almacenamiento en caché de forma interna, la caché es un detalle de implementación que no se expone al autor de la API. Por lo tanto, las pruebas del CTS no deben requerir ningún conocimiento especial del almacenamiento en caché que se usa en el código del cliente.
Estudia los aciertos y errores de caché
IpcDataCache y PropertyInvalidatedCache pueden imprimir estadísticas en vivo:
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
Hits:
- Definición: Es la cantidad de veces que se encontró correctamente un fragmento de datos solicitado dentro de la caché.
 - Significancia: Indica una recuperación de datos eficiente y rápida, lo que reduce la recuperación de datos innecesarios.
 - En general, los recuentos más altos son mejores.
 
Borrar:
- Definición: Es la cantidad de veces que se borró la caché debido a la invalidación.
 - Motivos de eliminación:
- Invalidación: Datos desactualizados del servidor.
 - Administración del espacio: Se libera espacio para los datos nuevos cuando la caché está llena.
 
 - Los recuentos altos podrían indicar datos que cambian con frecuencia y una posible ineficiencia.
 
Errores:
- Definición: Es la cantidad de veces que la caché no pudo proporcionar los datos solicitados.
 - Causas:
- Almacenamiento en caché ineficiente: La caché es demasiado pequeña o no almacena los datos correctos.
 - Datos que cambian con frecuencia
 - Solicitudes por primera vez
 
 - Los conteos altos sugieren posibles problemas de almacenamiento en caché.
 
Omitir:
- Definición: Son las instancias en las que no se usó la caché en absoluto, aunque se podría haber usado.
 - Motivos para omitir:
- Corking: Específico para las actualizaciones del administrador de paquetes de Android, desactiva deliberadamente el almacenamiento en caché debido a un gran volumen de llamadas durante el inicio.
 - Sin configurar: Existe la caché, pero no se inicializó. El nonce no se estableció, lo que significa que la caché nunca se invalidó.
 - Omisión: Es una decisión intencional de omitir la caché.
 
 - Los conteos altos indican posibles ineficiencias en el uso de la caché.
 
Invalida:
- Definición: Proceso de marcar los datos almacenados en caché como desactualizados.
 - Importancia: Proporciona un indicador de que el sistema funciona con los datos más actualizados, lo que evita errores e incoherencias.
 - Por lo general, lo activa el servidor propietario de los datos.
 
Tamaño actual:
- Definición: Es la cantidad actual de elementos en la caché.
 - Significancia: Indica el uso de recursos de la caché y el posible impacto en el rendimiento del sistema.
 - En general, los valores más altos significan que la caché usa más memoria.
 
Tamaño máximo:
- Definición: Es la cantidad máxima de espacio asignado para la caché.
 - Importancia: Determina la capacidad de la caché y su habilidad para almacenar datos.
 - Establecer un tamaño máximo adecuado ayuda a equilibrar la eficacia de la caché con el uso de la memoria. Una vez que se alcanza el tamaño máximo, se agrega un elemento nuevo desalojando el elemento que se usó por última vez, lo que puede indicar ineficiencia.
 
Marca de agua alta:
- Definición: Es el tamaño máximo que alcanzó la caché desde su creación.
 - Importancia: Proporciona estadísticas sobre el uso máximo de la caché y la posible presión de la memoria.
 - Supervisar el nivel máximo puede ayudar a identificar posibles cuellos de botella o áreas para optimizar.
 
Desbordamientos:
- Definición: Es la cantidad de veces que la caché superó su tamaño máximo y tuvo que expulsar datos para liberar espacio para las entradas nuevas.
 - Significancia: Indica la presión de la caché y la posible degradación del rendimiento debido al desalojo de datos.
 - Los conteos altos de desbordamiento sugieren que es posible que se deba ajustar el tamaño de la caché o reevaluar la estrategia de almacenamiento en caché.
 
Las mismas estadísticas también se pueden encontrar en un informe de errores.
Ajusta el tamaño de la caché
Los cachés tienen un tamaño máximo. Cuando se supera el tamaño máximo de la caché, las entradas se descartan en orden LRU.
- Almacenar en caché muy pocas entradas podría afectar de forma negativa la tasa de aciertos de caché.
 - Almacenar en caché demasiadas entradas aumenta el uso de memoria de la caché.
 
Encuentra el equilibrio adecuado para tu caso de uso.
Elimina las llamadas redundantes del cliente
Los clientes pueden realizar la misma consulta al servidor varias veces en un período breve:
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();
  }
}
Considera reutilizar los resultados de llamadas 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();
  }
}
Considera la memoización del cliente de las respuestas recientes del servidor
Es posible que las apps cliente consulten la API a una velocidad mayor de la que el servidor de la API puede producir respuestas nuevas significativas. En este caso, un enfoque eficaz es almacenar en caché la última respuesta del servidor vista en el cliente junto con una marca de tiempo y devolver el resultado almacenado en caché sin consultar al servidor si el resultado almacenado en caché es lo suficientemente reciente. El autor del cliente de la API puede determinar la duración de la memoización.
Por ejemplo, una app puede mostrar estadísticas de tráfico de red al usuario consultando las estadísticas en cada fotograma dibujado:
@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()));
}
La app puede dibujar fotogramas a 60 Hz, pero, hipotéticamente, el código del cliente en TrafficStats puede optar por consultar las estadísticas del servidor como máximo una vez por segundo y, si se consulta dentro de un segundo de una consulta anterior, devolver el último valor visto.
Esto se permite, ya que la documentación de la API no proporciona ningún contrato sobre la actualidad de los resultados devueltos.
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
Considera la generación de código del cliente en lugar de las consultas del servidor
Si el servidor conoce los resultados de la búsqueda en el momento de la compilación, considera si el cliente también los conoce en ese momento y si la API se podría implementar por completo en el cliente.
Considera el siguiente código de la app que verifica si el dispositivo es un reloj (es decir, si ejecuta Wear OS):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Esta propiedad del dispositivo se conoce en el momento de la compilación, específicamente cuando se compiló el framework para la imagen de arranque de este dispositivo. El código del cliente para hasSystemFeature podría devolver un resultado conocido de inmediato, en lugar de consultar el servicio del sistema PackageManager remoto.
Elimina las devoluciones de llamada del servidor duplicadas en el cliente
Por último, el cliente de la API puede registrar devoluciones de llamada con el servidor de la API para recibir notificaciones de eventos.
Es habitual que las apps registren varias devoluciones de llamada para la misma información subyacente. En lugar de que el servidor notifique al cliente una vez por cada devolución de llamada registrada con IPC, la biblioteca cliente debería tener una devolución de llamada registrada con IPC con el servidor y, luego, notificar cada devolución de llamada registrada en la 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"];
  }
}