Le chiamate API Android in genere comportano latenza e calcoli significativi per invocazione. La memorizzazione nella cache lato client è quindi un aspetto importante da considerare nella progettazione di API utili, corrette e performanti.
Motivazione
Le API esposte agli sviluppatori di app nell'SDK Android vengono spesso implementate come codice client nel framework Android che effettua una chiamata IPC Binder a un servizio di sistema in un processo della piattaforma, il cui compito è eseguire alcuni calcoli e restituire un risultato al client. La latenza di questa operazione è in genere dominata da tre fattori:
- Overhead IPC: una chiamata IPC di base ha in genere una latenza 10.000 volte superiore a quella di una chiamata di metodo in-process di base.
 - Contesa lato server: il lavoro svolto nel servizio di sistema in risposta alla richiesta del client potrebbe non iniziare immediatamente, ad esempio se un thread del server è occupato a gestire altre richieste arrivate in precedenza.
 - Calcolo lato server: il lavoro stesso per gestire la richiesta nel server potrebbe richiedere un lavoro significativo.
 
Puoi eliminare tutti e tre questi fattori di latenza implementando una cache sul lato client, a condizione che la cache:
- Corretto: la cache lato client non restituisce mai risultati diversi da quelli che avrebbe restituito il server.
 - Efficace: le richieste del client vengono spesso gestite dalla cache, ad esempio la cache ha un alto tasso di hit.
 - Efficiente: la cache lato client utilizza in modo efficiente le risorse lato client, ad esempio rappresentando i dati memorizzati nella cache in modo compatto e non memorizzando troppi risultati memorizzati nella cache o dati obsoleti nella memoria del client.
 
Valuta la memorizzazione nella cache dei risultati del server nel client
Se i client spesso effettuano più volte la stessa richiesta esatta e il valore restituito non cambia nel tempo, devi implementare una cache nella libreria client in base ai parametri della richiesta.
Valuta l'utilizzo di IpcDataCache nella tua implementazione:
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);
    }
}
Per un esempio completo, vedi android.app.admin.DevicePolicyManager.
IpcDataCache è disponibile per tutto il codice di sistema, inclusi i moduli mainline.
Esiste anche PropertyInvalidatedCache, che è quasi identico, ma è visibile solo al framework. Se possibile, preferisci IpcDataCache.
Invalidare le cache in caso di modifiche lato server
Se il valore restituito dal server può cambiare nel tempo, implementa un callback per osservare le modifiche e registrare un callback in modo da poter invalidare la cache lato client di conseguenza.
Invalidare le cache tra gli scenari di test delle unità
In una suite di test unitari, potresti testare il codice client rispetto a un test double anziché al server reale. In questo caso, assicurati di svuotare le cache lato client tra i casi di test. per mantenere gli scenari di test reciprocamente isolati ed evitare che uno interferisca con l'altro.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
Quando scrivi test CTS che esercitano un client API che utilizza la memorizzazione nella cache internamente, la cache è un dettaglio di implementazione che non è esposto all'autore dell'API, pertanto i test CTS non devono richiedere alcuna conoscenza speciale della memorizzazione nella cache utilizzata nel codice client.
Studiare gli hit e i mancati della cache
IpcDataCache e PropertyInvalidatedCache possono stampare le statistiche in tempo reale:
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
  ...
Campi
Hit:
- Definizione: il numero di volte in cui un dato richiesto è stato trovato correttamente nella cache.
 - Significato: indica un recupero dei dati efficiente e veloce, riducendo il recupero di dati non necessari.
 - In genere, i valori più elevati sono migliori.
 
Cancella:
- Definizione: il numero di volte in cui la cache è stata svuotata a causa dell'invalidazione.
 - Motivi della cancellazione:
- Invalidazione: dati obsoleti dal server.
 - Gestione dello spazio: fare spazio per nuovi dati quando la cache è piena.
 
 - Conteggi elevati potrebbero indicare dati che cambiano di frequente e una potenziale inefficienza.
 
Errori:
- Definizione: il numero di volte in cui la cache non è riuscita a fornire i dati richiesti.
 - Cause:
- Memorizzazione nella cache inefficiente: la cache è troppo piccola o non memorizza i dati giusti.
 - Dati che cambiano frequentemente.
 - Richieste per la prima volta.
 
 - Numeri elevati suggeriscono potenziali problemi di memorizzazione nella cache.
 
Salti:
- Definizione: istanze in cui la cache non è stata utilizzata, anche se avrebbe potuto esserlo.
 - Motivi dell'omissione:
- Corking: specifico per gli aggiornamenti di Android Package Manager, disattivazione deliberata della memorizzazione nella cache a causa di un volume elevato di chiamate durante l'avvio.
 - Non impostato: la cache esiste, ma non è inizializzata. Il nonce non è stato impostato, il che significa che la cache non è mai stata invalidata.
 - Ignora: decisione intenzionale di ignorare la cache.
 
 - Conteggi elevati indicano potenziali inefficienze nell'utilizzo della cache.
 
Invalida:
- Definizione: il processo di contrassegnare i dati memorizzati nella cache come obsoleti.
 - Significato: fornisce un indicatore che il sistema funziona con i dati più aggiornati, evitando errori e incoerenze.
 - In genere attivato dal server proprietario dei dati.
 
Dimensioni attuali:
- Definizione: la quantità attuale di elementi nella cache.
 - Significatività: indica l'utilizzo delle risorse della cache e il potenziale impatto sulle prestazioni del sistema.
 - Valori più alti in genere significano che la cache utilizza più memoria.
 
Dimensione massima:
- Definizione: la quantità massima di spazio allocata per la cache.
 - Significato: determina la capacità della cache e la sua capacità di archiviare dati.
 - L'impostazione di una dimensione massima appropriata consente di bilanciare l'efficacia della cache con l'utilizzo della memoria. Una volta raggiunta la dimensione massima, viene aggiunto un nuovo elemento eliminando l'elemento utilizzato meno di recente, il che può indicare inefficienza.
 
High Water Mark:
- Definizione: la dimensione massima raggiunta dalla cache dalla sua creazione.
 - Significato: fornisce informazioni sull'utilizzo massimo della cache e sulla potenziale pressione della memoria.
 - Il monitoraggio del livello massimo può aiutarti a identificare potenziali colli di bottiglia o aree di ottimizzazione.
 
Overflow:
- Definizione: il numero di volte in cui la cache ha superato le dimensioni massime e ha dovuto eliminare i dati per fare spazio a nuove voci.
 - Significato: indica la pressione della cache e il potenziale degrado delle prestazioni a causa dell'eliminazione dei dati.
 - Un numero elevato di overflow suggerisce che potrebbe essere necessario modificare le dimensioni della cache o rivalutare la strategia di memorizzazione nella cache.
 
Le stesse statistiche possono essere trovate anche in una segnalazione di bug.
Ottimizzare le dimensioni della cache
Le cache hanno una dimensione massima. Quando viene superata la dimensione massima della cache, le voci vengono eliminate in ordine LRU.
- La memorizzazione nella cache di un numero troppo ridotto di voci potrebbe influire negativamente sul tasso di hit della cache.
 - La memorizzazione nella cache di troppe voci aumenta la memoria utilizzata dalla cache.
 
Trova il giusto equilibrio per il tuo caso d'uso.
Elimina le chiamate ridondanti dei clienti
I client potrebbero inviare la stessa query al server più volte in un breve periodo di tempo:
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();
  }
}
Valuta la possibilità di riutilizzare i risultati delle chiamate precedenti:
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();
  }
}
Valuta la memorizzazione lato client delle risposte recenti del server
Le app client potrebbero eseguire query sull'API a una velocità superiore a quella con cui il server dell'API può produrre risposte nuove significative. In questo caso, un approccio efficace consiste nel memorizzare l'ultima risposta del server visualizzata sul lato client insieme a un timestamp e nel restituire il risultato memorizzato senza interrogare il server se il risultato memorizzato è abbastanza recente. L'autore del client API può determinare la durata della memorizzazione.
Ad esempio, un'app può mostrare all'utente le statistiche sul traffico di rete interrogando le statistiche in ogni frame disegnato:
@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()));
}
L'app potrebbe disegnare frame a 60 Hz. Ma, ipoteticamente, il codice client in
TrafficStats potrebbe scegliere di eseguire query sul server per le statistiche al massimo una volta al secondo
e, se la query viene eseguita entro un secondo da una query precedente, restituire l'ultimo valore visualizzato.
Ciò è consentito in quanto la documentazione API non prevede alcun contratto
relativo alla freschezza dei risultati restituiti.
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
Prendi in considerazione la generazione di codice lato client anziché le query del server
Se i risultati della query sono noti al server al momento della compilazione, valuta se sono noti anche al client al momento della compilazione e se l'API può essere implementata interamente lato client.
Considera il seguente codice dell'app che controlla se il dispositivo è uno smartwatch (ovvero, il dispositivo esegue Wear OS):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Questa proprietà del dispositivo è nota al momento della compilazione, in particolare quando
è stato creato il framework per l'immagine di avvio del dispositivo. Il codice lato client
per hasSystemFeature potrebbe restituire immediatamente un risultato noto, anziché
interrogare il servizio di sistema remoto PackageManager.
Rimuovere i callback del server duplicati nel client
Infine, il client API può registrare i callback con il server API per ricevere notifiche degli eventi.
È normale che le app registrino più callback per le stesse informazioni sottostanti. Anziché far sì che il server invii una notifica al client una volta per ogni callback registrato utilizzando IPC, la libreria client deve avere un callback registrato utilizzando IPC con il server e poi inviare una notifica a ogni callback registrato nell'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"];
  }
}