Por lo general, las llamadas a la API de Android implican una latencia y un procesamiento significativos por invocacin. Por lo tanto, el almacenamiento en caché del cliente es una consideración importante a la hora de diseñar APIs que sean útiles, correctas y de alto rendimiento.
Motivación
Las APIs expuestas a los desarrolladores de apps en el SDK de Android suelen implementarse como código cliente en el framework de Android que realiza una llamada de IPC de Binder a un servicio del sistema en un proceso de plataforma, cuya tarea es realizar algunos cálculos y mostrar un resultado al cliente. Por lo general, la latencia de esta operación está dominada por tres factores:
- Sobrecarga de IPC: Por lo general, una llamada de IPC básica tiene 10,000 veces la latencia de una llamada de método en proceso básica.
- Contención del servidor: Es posible que el trabajo realizado en el servicio del sistema en respuesta a la solicitud del cliente no comience de inmediato, por ejemplo, si un subproceso del servidor está ocupado controlando otras solicitudes que llegaron antes.
- Cálculo del servidor: El trabajo en sí para controlar la solicitud en el servidor puede requerir un trabajo significativo.
Puedes eliminar los tres factores de latencia implementando una caché en el lado del cliente, siempre que la caché cumpla con los siguientes requisitos:
- Correcto: La caché del cliente nunca muestra resultados que sean diferentes de los que mostraría el servidor.
- Eficaz: Las solicitudes del cliente suelen entregarse desde la caché. Por ejemplo, la caché tiene una alta tasa de hits.
- Eficiencia: La caché del cliente usa de manera eficiente los recursos del cliente, por ejemplo, representa los datos almacenados en caché de forma compacta y no almacena demasiados resultados almacenados en caché ni datos inactivos en la memoria del cliente.
Considera almacenar en caché los resultados del servidor en el cliente
Si los clientes suelen realizar la misma solicitud varias veces y el valor que se muestra no cambia con el tiempo, debes implementar una caché en la biblioteca cliente con la clave de los parámetros de solicitud.
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 principales.
También hay PropertyInvalidatedCache
, que es casi idéntico, pero solo es visible para el framework. Prefiere IpcDataCache
siempre que sea posible.
Invalida las cachés en los cambios del servidor
Si el valor que se muestra desde 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 las cachés del cliente entre los casos de prueba. Esto permite que los casos de prueba sean herméticos entre sí y evita que un caso de prueba interfiera en otro.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Cuando se escriben pruebas de 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 de CTS no deberían 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 dato solicitado en la caché.
- Significado: Indica una recuperación de datos eficiente y rápida, lo que reduce la recuperación de datos innecesarios.
- Por lo general, los recuentos más altos son mejores.
Limpia lo siguiente:
- Definición: Es la cantidad de veces que se borró la caché debido a la invalidación.
- Motivos de la anulación:
- Invalidación: Datos desactualizados del servidor.
- Administración del espacio: Se crea espacio para datos nuevos cuando la caché está llena.
- Los recuentos altos podrían indicar datos que cambian con frecuencia y una posible ineficiencia.
Pérdidas:
- 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 recuentos altos sugieren posibles problemas de almacenamiento en caché.
Omisiones:
- Definición: Son instancias en las que no se usó la caché en absoluto, aunque se podría haber hecho.
- Motivos para omitir:
- Corking: Es específico de las actualizaciones del Administrador de paquetes de Android y desactiva la caché de forma deliberada debido a un gran volumen de llamadas durante el inicio.
- Sin definir: La caché existe, pero no se inicializó. No se configuró el nonce, lo que significa que la caché nunca se invalidó.
- Omisión: Es la decisión intencional de omitir la caché.
- Los recuentos altos indican posibles ineficiencias en el uso de la caché.
Invalida lo siguiente:
- Definición: Es el proceso de marcar los datos almacenados en caché como desactualizados o inactivos.
- Significado: Proporciona un indicador de que el sistema funciona con los datos más actualizados, lo que evita errores y discrepancias.
- Por lo general, es el servidor propietario de los datos el que lo activa.
Tamaño actual:
- Definición: Es la cantidad actual de elementos en la caché.
- Significado: Indica el uso de recursos de la caché y el posible impacto en el rendimiento del sistema.
- Los valores más altos suelen significar que la caché usa más memoria.
Tamaño máximo:
- Definición: Es la cantidad máxima de espacio asignado para la caché.
- Significado: Determina la capacidad de la caché y su capacidad 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 expulsando el elemento que se usó menos recientemente, lo que puede indicar ineficiencia.
Marca de agua alta:
- Definición: Es el tamaño máximo que alcanza la caché desde su creación.
- Relevancia: Proporciona estadísticas sobre el uso máximo de la caché y la posible presión de la memoria.
- Supervisar el límite máximo puede ayudar a identificar posibles cuellos de botella o áreas que se pueden optimizar.
Desbordamientos:
- Definición: Es la cantidad de veces que la caché superó su tamaño máximo y tuvo que desalojar datos para liberar espacio para entradas nuevas.
- Significado: Indica la presión de la caché y la posible degradación del rendimiento debido a la expulsión de datos.
- Los recuentos de desbordamiento altos sugieren que es posible que debas 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é
Las cachés tienen un tamaño máximo. Cuando se supera el tamaño máximo de la caché, las entradas se desalojan en orden de LRU.
- Almacenar en caché demasiadas 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.
Cómo eliminar llamadas de cliente redundantes
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 memorización del cliente de las respuestas recientes del servidor
Las apps cliente pueden consultar la API a una velocidad más rápida que 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 que se vio en el lado del cliente junto con una marca de tiempo y mostrar el resultado almacenado en caché sin consultar el 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 memorización.
Por ejemplo, una app puede mostrarle estadísticas de tráfico de red al usuario mediante la consulta de 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. Sin embargo, de manera hipotética, el código cliente en TrafficStats
puede optar por consultar al servidor estadísticas como máximo una vez por segundo y, si se consulta dentro de un segundo de una consulta anterior, mostrar el último valor visto.
Esto se permite porque la documentación de la API no proporciona ningún contrato con respecto a la actualización de los resultados que se muestran.
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 puede conocer los resultados de la consulta en el momento de la compilación, considera si el cliente también puede conocerlos en ese momento y si la API se puede implementar por completo en el cliente.
Considera el siguiente código de app que comprueba si el dispositivo es un reloj (es decir, si se 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 tiempo de compilación, específicamente en el momento en que se compiló el Framework para la imagen de arranque de este dispositivo. El código del cliente para hasSystemFeature
podría mostrar un resultado conocido de inmediato, en lugar de consultar el servicio del sistema PackageManager
remoto.
Cómo eliminar 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 sobre los 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 devolución de llamada registrada con el IPC, la biblioteca cliente debe tener una devolución de llamada registrada con el 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"];
}
}